From 62871d30263ca9dde2877b08d14b31a2e9cbfd8e Mon Sep 17 00:00:00 2001 From: Rui <102453770+ruixhuang@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:13:32 -0700 Subject: [PATCH] CLI-663 Vault deposit/withdraw screen (#233) ![Screenshot_1727802557](https://github.com/user-attachments/assets/696d385b-09e9-4733-acc1-6c7fd5951631) ![Screenshot_1727802554](https://github.com/user-attachments/assets/f829bc5c-879d-4ac3-9ab7-1592b1cde8c3) --------- Co-authored-by: Prashan Dharmasena <163016611+prashanDYDX@users.noreply.github.com> --- v4/build.gradle | 2 +- .../trading/common/navigation/DydxRoutes.kt | 1 + .../feature/receipt/DydxReceiptView.kt | 2 +- .../main/res/drawable/icon_right_arrow_2.xml | 18 ++ .../main/res/drawable/vault_account_token.xml | 79 ++++++ .../main/res/drawable/vault_cross_token.xml | 16 ++ .../main/res/drawable/vault_usdc_token.xml | 16 ++ .../transfer/DydxTransferSectionsView.kt | 3 + v4/feature/vault/build.gradle | 1 + .../trading/feature/vault/DydxVaultRouter.kt | 18 +- .../trading/feature/vault/DydxVaultView.kt | 6 +- .../feature/vault/DydxVaultViewModel.kt | 1 - .../trading/feature/vault/VaultInputState.kt | 95 ++++++++ .../components/DydxVaultButtonsViewModel.kt | 28 +-- .../DydxVaultDepositWithdrawSelectionView.kt | 94 ++++++++ ...xVaultDepositWithdrawSelectionViewModel.kt | 56 +++++ .../DydxVaultDepositWithdrawView.kt | 67 +++++- .../DydxVaultDepositWithdrawViewModel.kt | 43 +++- .../components/VaultAmountBox.kt | 225 ++++++++++++++++++ .../confirmation/DydxVaultConfirmationView.kt | 189 +++++++++++++++ .../DydxVaultConfirmationViewModel.kt | 120 ++++++++++ .../deposit/DydxVaultDepositView.kt | 102 ++++++++ .../deposit/DydxVaultDepositViewModel.kt | 88 +++++++ .../withdraw/DydxVaultWithdrawView.kt | 102 ++++++++ .../withdraw/DydxVaultWithdrawViewModel.kt | 133 +++++++++++ .../vault/receipt/DydxVaultReceiptView.kt | 154 ++++++++++++ .../receipt/DydxVaultReceiptViewModel.kt | 176 ++++++++++++++ .../components/buttons/PlatformPillButton.kt | 4 +- .../changes/PlatformDirectionArrow.kt | 9 +- .../tabgroups/PlatformPillTextGroup.kt | 5 + 30 files changed, 1800 insertions(+), 53 deletions(-) create mode 100644 v4/feature/shared/src/main/res/drawable/icon_right_arrow_2.xml create mode 100644 v4/feature/shared/src/main/res/drawable/vault_account_token.xml create mode 100644 v4/feature/shared/src/main/res/drawable/vault_cross_token.xml create mode 100644 v4/feature/shared/src/main/res/drawable/vault_usdc_token.xml create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/VaultInputState.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawSelectionView.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawSelectionViewModel.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/components/VaultAmountBox.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/confirmation/DydxVaultConfirmationView.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/confirmation/DydxVaultConfirmationViewModel.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/deposit/DydxVaultDepositView.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/deposit/DydxVaultDepositViewModel.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/withdraw/DydxVaultWithdrawView.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/withdraw/DydxVaultWithdrawViewModel.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/receipt/DydxVaultReceiptView.kt create mode 100644 v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/receipt/DydxVaultReceiptViewModel.kt diff --git a/v4/build.gradle b/v4/build.gradle index 7645767f..82c75529 100644 --- a/v4/build.gradle +++ b/v4/build.gradle @@ -88,7 +88,7 @@ ext { compileSdkVersion = 34 // App dependencies - abacusVersion = '1.12.7' + abacusVersion = '1.12.14' carteraVersion = '0.1.15' kollectionsVersion = '2.0.16' diff --git a/v4/common/src/main/java/exchange/dydx/trading/common/navigation/DydxRoutes.kt b/v4/common/src/main/java/exchange/dydx/trading/common/navigation/DydxRoutes.kt index 0956a75e..2b8913e4 100644 --- a/v4/common/src/main/java/exchange/dydx/trading/common/navigation/DydxRoutes.kt +++ b/v4/common/src/main/java/exchange/dydx/trading/common/navigation/DydxRoutes.kt @@ -73,4 +73,5 @@ object VaultRoutes { const val main = "vault" const val deposit = "vault/deposit" const val withdraw = "vault/withdraw" + const val confirmation = "vault/confirmation" } 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 f990388c..cb6708cd 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 @@ -69,7 +69,7 @@ object DydxReceiptView : DydxComponent { TransferDuration, Slippage, PositionMargin, - LiquidationPrice; + LiquidationPrice, } data class ViewState( diff --git a/v4/feature/shared/src/main/res/drawable/icon_right_arrow_2.xml b/v4/feature/shared/src/main/res/drawable/icon_right_arrow_2.xml new file mode 100644 index 00000000..3535b136 --- /dev/null +++ b/v4/feature/shared/src/main/res/drawable/icon_right_arrow_2.xml @@ -0,0 +1,18 @@ + + + + diff --git a/v4/feature/shared/src/main/res/drawable/vault_account_token.xml b/v4/feature/shared/src/main/res/drawable/vault_account_token.xml new file mode 100644 index 00000000..8ab05ed6 --- /dev/null +++ b/v4/feature/shared/src/main/res/drawable/vault_account_token.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v4/feature/shared/src/main/res/drawable/vault_cross_token.xml b/v4/feature/shared/src/main/res/drawable/vault_cross_token.xml new file mode 100644 index 00000000..5646738f --- /dev/null +++ b/v4/feature/shared/src/main/res/drawable/vault_cross_token.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/v4/feature/shared/src/main/res/drawable/vault_usdc_token.xml b/v4/feature/shared/src/main/res/drawable/vault_usdc_token.xml new file mode 100644 index 00000000..37702f68 --- /dev/null +++ b/v4/feature/shared/src/main/res/drawable/vault_usdc_token.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/DydxTransferSectionsView.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/DydxTransferSectionsView.kt index 7535c090..53d0702d 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/DydxTransferSectionsView.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/DydxTransferSectionsView.kt @@ -1,9 +1,11 @@ package exchange.dydx.trading.feature.transfer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.platformui.components.tabgroups.PlatformPillTextGroup @@ -81,6 +83,7 @@ object DydxTransferSectionsView : DydxComponent { selectedItemStyle = TextStyle.dydxDefault .themeColor(ThemeColor.SemanticColor.text_primary) .themeFont(fontType = ThemeFont.FontType.book, fontSize = ThemeFont.FontSize.large), + padding = PaddingValues(horizontal = 14.dp, vertical = 8.dp), currentSelection = state.selections.indexOf(state.currentSelection), scrollingEnabled = true, onSelectionChanged = { index -> diff --git a/v4/feature/vault/build.gradle b/v4/feature/vault/build.gradle index 1e5701a1..3be832d2 100644 --- a/v4/feature/vault/build.gradle +++ b/v4/feature/vault/build.gradle @@ -67,6 +67,7 @@ dependencies { api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" implementation project(':v4:integration:cosmos') + implementation project(':v4:feature:receipt') kapt "com.google.dagger:hilt-compiler:$hiltVersion" diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultRouter.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultRouter.kt index 5144e4dd..e7b9a4ee 100644 --- a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultRouter.kt +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultRouter.kt @@ -4,10 +4,12 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavGraphBuilder import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.VaultRoutes +import exchange.dydx.trading.common.navigation.VaultRoutes.confirmation import exchange.dydx.trading.common.navigation.dydxComposable import exchange.dydx.trading.feature.shared.bottombar.DydxBottomBarScaffold import exchange.dydx.trading.feature.vault.DydxVaultView import exchange.dydx.trading.feature.vault.depositwithdraw.DydxVaultDepositWithdrawView +import exchange.dydx.trading.feature.vault.depositwithdraw.confirmation.DydxVaultConfirmationView import exchange.dydx.utilities.utils.Logging fun NavGraphBuilder.vaultGraph( @@ -29,9 +31,7 @@ fun NavGraphBuilder.vaultGraph( route = VaultRoutes.deposit, deepLinks = appRouter.deeplinks(VaultRoutes.deposit), ) { navBackStackEntry -> - DydxBottomBarScaffold(Modifier) { - DydxVaultDepositWithdrawView.Content(Modifier, type = DydxVaultDepositWithdrawView.DepositWithdrawType.DEPOSIT) - } + DydxVaultDepositWithdrawView.Content(Modifier, type = DydxVaultDepositWithdrawView.DepositWithdrawType.DEPOSIT) } dydxComposable( @@ -39,8 +39,14 @@ fun NavGraphBuilder.vaultGraph( route = VaultRoutes.withdraw, deepLinks = appRouter.deeplinks(VaultRoutes.withdraw), ) { navBackStackEntry -> - DydxBottomBarScaffold(Modifier) { - DydxVaultDepositWithdrawView.Content(Modifier, type = DydxVaultDepositWithdrawView.DepositWithdrawType.WITHDRAW) - } + DydxVaultDepositWithdrawView.Content(Modifier, type = DydxVaultDepositWithdrawView.DepositWithdrawType.WITHDRAW) + } + + dydxComposable( + router = appRouter, + route = VaultRoutes.confirmation, + deepLinks = appRouter.deeplinks(VaultRoutes.confirmation), + ) { navBackStackEntry -> + DydxVaultConfirmationView.Content(Modifier) } } diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultView.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultView.kt index dc9232ad..9d53799d 100644 --- a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultView.kt +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultView.kt @@ -94,8 +94,10 @@ object DydxVaultView : DydxComponent { } stickyHeader(key = "positions_header") { DydxVaultPositionsHeaderView.Content( - Modifier, - DydxVaultPositionsHeaderView.ViewState( + modifier = Modifier + .fillParentMaxWidth() + .themeColor(ThemeColor.SemanticColor.layer_2), + state = DydxVaultPositionsHeaderView.ViewState( localizer = state.localizer, positionCount = state.items.count(), ), diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultViewModel.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultViewModel.kt index c11da3cd..862375bc 100644 --- a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultViewModel.kt +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/DydxVaultViewModel.kt @@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject import kotlin.math.absoluteValue -import kotlin.math.sign @HiltViewModel class DydxVaultViewModel @Inject constructor( diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/VaultInputState.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/VaultInputState.kt new file mode 100644 index 00000000..0e536f6a --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/VaultInputState.kt @@ -0,0 +1,95 @@ +package exchange.dydx.trading.feature.vault + +import dagger.hilt.android.scopes.ActivityRetainedScoped +import exchange.dydx.abacus.functional.vault.VaultDepositWithdrawFormValidator +import exchange.dydx.abacus.functional.vault.VaultFormAccountData +import exchange.dydx.abacus.functional.vault.VaultFormAction +import exchange.dydx.abacus.functional.vault.VaultFormData +import exchange.dydx.abacus.functional.vault.VaultFormValidationResult +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import indexer.models.chain.OnChainVaultDepositWithdrawSlippageResponse +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +enum class VaultInputType { + DEPOSIT, + WITHDRAW +} + +enum class VaultInputStage { + EDIT, + CONFIRM, + SUBMIT +} + +@ActivityRetainedScoped +class VaultInputState @Inject constructor( + private val abacusStateManager: AbacusStateManagerProtocol, +) { + val type: MutableStateFlow = MutableStateFlow(null) + val amount: MutableStateFlow = MutableStateFlow(null) + val stage: MutableStateFlow = MutableStateFlow(VaultInputStage.EDIT) + val slippageAcked: MutableStateFlow = MutableStateFlow(false) + val slippageResponse: MutableStateFlow = MutableStateFlow(null) + + private val vaultFormData: Flow = + combine( + type, + amount, + stage, + slippageAcked, + ) { type, amount, stage, slippageAcked -> + val type = type ?: return@combine null + val amount = amount ?: return@combine null + VaultFormData( + action = when (type) { + VaultInputType.DEPOSIT -> VaultFormAction.DEPOSIT + VaultInputType.WITHDRAW -> VaultFormAction.WITHDRAW + }, + amount = amount, + acknowledgedSlippage = slippageAcked, + inConfirmationStep = stage == VaultInputStage.CONFIRM, + ) + } + .distinctUntilChanged() + + private val vaultFormAccountData: Flow = + abacusStateManager.state.selectedSubaccount + .map { + VaultFormAccountData( + marginUsage = it?.marginUsage?.current, + freeCollateral = it?.freeCollateral?.current, + canViewAccount = it != null, + ) + } + .distinctUntilChanged() + + val result: Flow = + combine( + vaultFormData, + vaultFormAccountData, + abacusStateManager.state.vault.map { it?.account }, + slippageResponse, + ) { formData, accountData, account, slippageResponse -> + val formData = formData ?: return@combine null + VaultDepositWithdrawFormValidator.validateVaultForm( + formData = formData, + accountData = accountData, + vaultAccount = account, + slippageResponse = slippageResponse, + ) + } + .distinctUntilChanged() + + fun reset() { + type.value = null + amount.value = null + stage.value = VaultInputStage.EDIT + slippageAcked.value = false + slippageResponse.value = null + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/components/DydxVaultButtonsViewModel.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/components/DydxVaultButtonsViewModel.kt index 27f30f21..fd4ef999 100644 --- a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/components/DydxVaultButtonsViewModel.kt +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/components/DydxVaultButtonsViewModel.kt @@ -7,7 +7,7 @@ import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.common.navigation.DydxRouter -import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol +import exchange.dydx.trading.common.navigation.VaultRoutes import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import javax.inject.Inject @@ -18,7 +18,6 @@ class DydxVaultButtonsViewModel @Inject constructor( private val abacusStateManager: AbacusStateManagerProtocol, private val formatter: DydxFormatter, private val router: DydxRouter, - private val cosmosClient: CosmosV4WebviewClientProtocol, ) : ViewModel(), DydxViewModel { val state: Flow = flowOf(createViewState()) @@ -27,31 +26,10 @@ class DydxVaultButtonsViewModel @Inject constructor( return DydxVaultButtonsView.ViewState( localizer = localizer, depositAction = { - cosmosClient.depositToMegavault( - subaccountNumber = 0, - amountUsdc = 1.0, - completion = { response -> - print(response) - }, - ) - // router.navigateTo(route = VaultRoutes.deposit, presentation = Presentation.Modal) + router.navigateTo(route = VaultRoutes.deposit, presentation = DydxRouter.Presentation.Modal) }, withdrawAction = { - cosmosClient.getMegavaultWithdrawalInfo( - shares = 2, - completion = { response -> - print(response) - }, - ) -// cosmosClient.withdrawFromMegavault( -// subaccountNumber = 0, -// shares = 2, -// minAmount = 0, -// completion = { response -> -// print(response) -// } -// ) - // router.navigateTo(route = VaultRoutes.withdraw, presentation = Presentation.Modal) + router.navigateTo(route = VaultRoutes.withdraw, presentation = DydxRouter.Presentation.Modal) }, ) } diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawSelectionView.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawSelectionView.kt new file mode 100644 index 00000000..c2909070 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawSelectionView.kt @@ -0,0 +1,94 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.tabgroups.PlatformPillTextGroup +import exchange.dydx.platformui.compose.collectAsStateWithLifecycle +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.platformui.theme.DydxThemedPreviewSurface +import exchange.dydx.platformui.theme.MockLocalizer +import exchange.dydx.trading.common.component.DydxComponent + +@Preview +@Composable +fun Preview_DydxVaultDepositWithdrawSelectionView() { + DydxThemedPreviewSurface { + DydxVaultDepositWithdrawSelectionView.Content( + Modifier, + DydxVaultDepositWithdrawSelectionView.ViewState.preview, + ) + } +} + +object DydxVaultDepositWithdrawSelectionView : DydxComponent { + enum class Selection { + Deposit, Withdrawal; + + val stringKey: String + get() = when (this) { + Deposit -> "APP.GENERAL.DEPOSIT" + Withdrawal -> "APP.GENERAL.WITHDRAW" + } + } + + data class ViewState( + val localizer: LocalizerProtocol, + val selections: List = listOf( + Selection.Deposit, + Selection.Withdrawal, + ), + val currentSelection: Selection = Selection.Deposit, + val onSelectionChanged: (Selection) -> Unit = {}, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxVaultDepositWithdrawSelectionViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) return + + val items = state.selections.map { selection -> + state.localizer.localize(selection.stringKey) + } + + PlatformPillTextGroup( + modifier = modifier, + items = items, + selectedItems = items, + itemStyle = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_secondary) + .themeFont(fontType = ThemeFont.FontType.book, fontSize = ThemeFont.FontSize.large), + selectedItemStyle = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_primary) + .themeFont(fontType = ThemeFont.FontType.book, fontSize = ThemeFont.FontSize.large), + padding = PaddingValues(horizontal = 14.dp, vertical = 8.dp), + currentSelection = state.selections.indexOf(state.currentSelection), + scrollingEnabled = true, + onSelectionChanged = { index -> + state.onSelectionChanged(state.selections[index]) + }, + ) + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawSelectionViewModel.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawSelectionViewModel.kt new file mode 100644 index 00000000..2559ace4 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawSelectionViewModel.kt @@ -0,0 +1,56 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +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.vault.VaultInputState +import exchange.dydx.trading.feature.vault.VaultInputType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import javax.inject.Inject + +@HiltViewModel +class DydxVaultDepositWithdrawSelectionViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val inputState: VaultInputState, +) : ViewModel(), DydxViewModel { + + val state: Flow = + inputState.type + .mapNotNull { it } + .map { + createViewState(it) + } + .distinctUntilChanged() + + private fun createViewState( + selection: VaultInputType + ): DydxVaultDepositWithdrawSelectionView.ViewState { + return DydxVaultDepositWithdrawSelectionView.ViewState( + localizer = localizer, + selections = listOf( + DydxVaultDepositWithdrawSelectionView.Selection.Deposit, + DydxVaultDepositWithdrawSelectionView.Selection.Withdrawal, + ), + currentSelection = when (selection) { + VaultInputType.DEPOSIT -> DydxVaultDepositWithdrawSelectionView.Selection.Deposit + VaultInputType.WITHDRAW -> DydxVaultDepositWithdrawSelectionView.Selection.Withdrawal + }, + onSelectionChanged = { selection -> + inputState.type.value = when (selection) { + DydxVaultDepositWithdrawSelectionView.Selection.Deposit -> VaultInputType.DEPOSIT + DydxVaultDepositWithdrawSelectionView.Selection.Withdrawal -> VaultInputType.WITHDRAW + } + inputState.amount.value = null + inputState.slippageAcked.value = false + }, + ) + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawView.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawView.kt index aa672044..2744b897 100644 --- a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawView.kt +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawView.kt @@ -1,15 +1,31 @@ package exchange.dydx.trading.feature.vault.depositwithdraw -import androidx.compose.material.Text +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.dividers.PlatformDivider import exchange.dydx.platformui.compose.collectAsStateWithLifecycle +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.ThemeShapes +import exchange.dydx.platformui.designSystem.theme.themeColor import exchange.dydx.platformui.theme.DydxThemedPreviewSurface import exchange.dydx.platformui.theme.MockLocalizer import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.common.navigation.DydxAnimation +import exchange.dydx.trading.feature.shared.views.HeaderViewCloseBotton +import exchange.dydx.trading.feature.vault.depositwithdraw.deposit.DydxVaultDepositView +import exchange.dydx.trading.feature.vault.depositwithdraw.withdraw.DydxVaultWithdrawView @Preview @Composable @@ -30,12 +46,12 @@ object DydxVaultDepositWithdrawView : DydxComponent { data class ViewState( val localizer: LocalizerProtocol, - val text: String?, + val closeAction: (() -> Unit)? = null, + val selection: DydxVaultDepositWithdrawSelectionView.Selection? = null, ) { companion object { val preview = ViewState( localizer = MockLocalizer(), - text = "1.0M", ) } } @@ -48,6 +64,9 @@ object DydxVaultDepositWithdrawView : DydxComponent { @Composable fun Content(modifier: Modifier, type: DepositWithdrawType) { val viewModel: DydxVaultDepositWithdrawViewModel = hiltViewModel() + LaunchedEffect(Unit) { + viewModel.type = type + } val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value Content(modifier, state) @@ -58,6 +77,46 @@ object DydxVaultDepositWithdrawView : DydxComponent { if (state == null) { return } - Text(text = state?.text ?: "") + Column( + modifier = modifier + .animateContentSize() + .fillMaxSize() + .themeColor(ThemeColor.SemanticColor.layer_3), + ) { + Row( + Modifier + .fillMaxWidth() + .padding(vertical = ThemeShapes.VerticalPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + DydxVaultDepositWithdrawSelectionView.Content( + modifier = Modifier + .weight(1f) + .padding(vertical = ThemeShapes.VerticalPadding), + ) + + HeaderViewCloseBotton( + closeAction = state.closeAction, + ) + } + + PlatformDivider() + + DydxAnimation.AnimateFadeInOut( + visible = state.selection == DydxVaultDepositWithdrawSelectionView.Selection.Deposit, + ) { + DydxVaultDepositView.Content( + modifier = Modifier, + ) + } + DydxAnimation.AnimateFadeInOut( + visible = state.selection == DydxVaultDepositWithdrawSelectionView.Selection.Withdrawal, + ) { + DydxVaultWithdrawView.Content( + modifier = Modifier, + ) + } + } } } diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawViewModel.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawViewModel.kt index 07221942..9422d0f9 100644 --- a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawViewModel.kt +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/DydxVaultDepositWithdrawViewModel.kt @@ -2,14 +2,18 @@ package exchange.dydx.trading.feature.vault.depositwithdraw import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import exchange.dydx.abacus.output.PerpetualMarketSummary 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.common.navigation.DydxRouter +import exchange.dydx.trading.feature.vault.VaultInputState +import exchange.dydx.trading.feature.vault.VaultInputType +import exchange.dydx.trading.feature.vault.depositwithdraw.DydxVaultDepositWithdrawView.DepositWithdrawType import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import javax.inject.Inject @HiltViewModel @@ -17,20 +21,41 @@ class DydxVaultDepositWithdrawViewModel @Inject constructor( private val localizer: LocalizerProtocol, private val abacusStateManager: AbacusStateManagerProtocol, private val formatter: DydxFormatter, + private val inputState: VaultInputState, + private val router: DydxRouter, ) : ViewModel(), DydxViewModel { - val state: Flow = - abacusStateManager.state.marketSummary - .map { - createViewState(it) + var type: DepositWithdrawType? = null + set(value) { + if (value != null && inputState.type.value == null) { + inputState.reset() + inputState.type.value = when (value) { + DepositWithdrawType.DEPOSIT -> VaultInputType.DEPOSIT + DepositWithdrawType.WITHDRAW -> VaultInputType.WITHDRAW + } } - .distinctUntilChanged() + field = value + } - private fun createViewState(marketSummary: PerpetualMarketSummary?): DydxVaultDepositWithdrawView.ViewState { - val volume = formatter.dollarVolume(marketSummary?.volume24HUSDC) + val state: Flow = inputState.type + .mapNotNull { it } + .map { + createViewState(it) + } + .distinctUntilChanged() + + private fun createViewState( + selection: VaultInputType, + ): DydxVaultDepositWithdrawView.ViewState { return DydxVaultDepositWithdrawView.ViewState( localizer = localizer, - text = volume, + selection = when (selection) { + VaultInputType.DEPOSIT -> DydxVaultDepositWithdrawSelectionView.Selection.Deposit + VaultInputType.WITHDRAW -> DydxVaultDepositWithdrawSelectionView.Selection.Withdrawal + }, + closeAction = { + router.navigateBack() + }, ) } } diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/components/VaultAmountBox.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/components/VaultAmountBox.kt new file mode 100644 index 00000000..f22b7309 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/components/VaultAmountBox.kt @@ -0,0 +1,225 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.utils.Parser +import exchange.dydx.platformui.components.buttons.PlatformPillButton +import exchange.dydx.platformui.components.changes.PlatformAmountChange +import exchange.dydx.platformui.components.changes.PlatformDirection +import exchange.dydx.platformui.components.inputs.PlatformTextInput +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.ThemeFont +import exchange.dydx.platformui.designSystem.theme.ThemeShapes +import exchange.dydx.platformui.designSystem.theme.color +import exchange.dydx.platformui.designSystem.theme.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.designSystem.theme.themeFont +import exchange.dydx.platformui.theme.DydxThemedPreviewSurface +import exchange.dydx.platformui.theme.MockLocalizer +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold +import exchange.dydx.trading.feature.shared.views.AmountText + +@Preview +@Composable +fun Preview_VaultAmountBox() { + DydxThemedPreviewSurface(background = ThemeColor.SemanticColor.layer_3) { + VaultAmountBox.Content(Modifier, VaultAmountBox.ViewState.preview) + } +} + +object VaultAmountBox { + data class ViewState( + val localizer: LocalizerProtocol, + val formatter: DydxFormatter, + val parser: ParserProtocol, + val title: String? = null, + val value: String? = null, + val placeholder: String? = null, + val maxAmount: Double? = null, + val stepSize: Int? = null, + val maxAction: (() -> Unit)? = null, + val onEditAction: ((String) -> Unit)? = null, + val footer: String? = null, + val footerBefore: AmountText.ViewState? = null, + val footerAfter: AmountText.ViewState? = null, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + formatter = DydxFormatter(), + parser = Parser(), + placeholder = "0.000", + maxAmount = 1000.0, + stepSize = 3, + maxAction = {}, + footer = "Available", + footerBefore = AmountText.ViewState.preview, + footerAfter = AmountText.ViewState.preview, + ) + } + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + + Column(modifier) { + InputFieldScaffold(Modifier.zIndex(1f)) { + TopContent(modifier, state) + } + val shape = RoundedCornerShape(0.dp, 0.dp, 8.dp, 8.dp) + Column( + modifier = modifier + .offset(y = (-4).dp) + .background(color = ThemeColor.SemanticColor.layer_1.color, shape = shape) + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(vertical = ThemeShapes.VerticalPadding) + .padding(top = 4.dp), + ) { + BottomContent(modifier, state) + } + } + } + + @Composable + private fun TopContent( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = modifier + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(vertical = ThemeShapes.VerticalPadding), + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = state.title ?: state.localizer.localize("APP.GENERAL.AMOUNT"), + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.mini), + ) + + PlatformTextInput( + modifier = Modifier.fillMaxWidth(), + value = state.value ?: "", + textStyle = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_primary) + .themeFont(fontSize = ThemeFont.FontSize.medium), + placeHolder = if (state.value == null) { + state.formatter.raw(0.0, state.stepSize ?: 0) + } else { + null + }, + onValueChange = { state.onEditAction?.invoke(it) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + } + + val focusManager = LocalFocusManager.current + + if (state.maxAction != null) { + PlatformPillButton( + action = { + state.maxAction.invoke() + focusManager.clearFocus() + }, + content = { + Text( + text = state.localizer.localize("APP.GENERAL.MAX"), + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.mini), + ) + }, + ) + } + } + } + + @Composable + private fun BottomContent( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = state.footer ?: state.localizer.localize("APP.GENERAL.AVAILABLE"), + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.small), + ) + + Spacer(modifier = Modifier.weight(1f)) + + AmountChange(modifier = Modifier, state = state) + } + } + + @Composable + private fun AmountChange( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = modifier, + ) { + PlatformAmountChange( + modifier = Modifier.weight(1f), + before = if (state.footerBefore != null) { { + AmountText.Content( + state = state.footerBefore, + textStyle = TextStyle.dydxDefault + .themeFont(fontType = ThemeFont.FontType.number, fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } } else { + null + }, + after = if (state.footerAfter != null) { { + AmountText.Content( + state = state.footerAfter, + textStyle = TextStyle.dydxDefault + .themeFont(fontType = ThemeFont.FontType.number, fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_primary), + ) + } } else { + null + }, + direction = PlatformDirection.from(state.footerBefore?.amount, state.footerAfter?.amount), + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/confirmation/DydxVaultConfirmationView.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/confirmation/DydxVaultConfirmationView.kt new file mode 100644 index 00000000..62f3ece0 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/confirmation/DydxVaultConfirmationView.kt @@ -0,0 +1,189 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw.confirmation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.dividers.PlatformDivider +import exchange.dydx.platformui.components.icons.PlatformImage +import exchange.dydx.platformui.compose.collectAsStateWithLifecycle +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.ThemeFont +import exchange.dydx.platformui.designSystem.theme.ThemeShapes +import exchange.dydx.platformui.designSystem.theme.color +import exchange.dydx.platformui.designSystem.theme.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.designSystem.theme.themeFont +import exchange.dydx.platformui.theme.DydxThemedPreviewSurface +import exchange.dydx.platformui.theme.MockLocalizer +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.feature.shared.R +import exchange.dydx.trading.feature.shared.views.HeaderView +import exchange.dydx.trading.feature.shared.views.InputCtaButton +import exchange.dydx.trading.feature.vault.receipt.DydxVaultReceiptView + +@Preview +@Composable +fun Preview_DydxVaultConfirmationView() { + DydxThemedPreviewSurface { + DydxVaultConfirmationView.Content(Modifier, DydxVaultConfirmationView.ViewState.preview) + } +} + +object DydxVaultConfirmationView : DydxComponent { + enum class Direction { + Deposit, + Withdraw + } + data class ViewState( + val localizer: LocalizerProtocol, + val headerTitle: String? = null, + val sourceLabel: String? = null, + val sourceValue: String? = null, + val destinationValue: String? = null, + val destinationIcon: Any? = null, + val ctaButton: InputCtaButton.ViewState? = null, + val backAction: (() -> Unit)? = null, + val direction: Direction? = null, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + headerTitle = "Confirm Deposit", + sourceLabel = "Amount to deposit", + sourceValue = "$1,000.00", + destinationValue = "Vault", + ctaButton = InputCtaButton.ViewState.preview, + direction = Direction.Deposit, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxVaultConfirmationViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + + Column( + modifier = modifier + .fillMaxSize(), + ) { + HeaderView( + title = state.headerTitle ?: "", + backAction = state.backAction, + ) + + PlatformDivider() + + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(0.dp), + ) { + ItemContent( + modifier = Modifier + .weight(1f) + .padding(ThemeShapes.HorizontalPadding), + label = state.sourceLabel, + value = state.sourceValue, + icon = R.drawable.vault_usdc_token, + ) + + PlatformImage( + modifier = Modifier + .padding(top = 16.dp) + .size(28.dp), + icon = R.drawable.icon_right_arrow_2, + ) + + ItemContent( + modifier = Modifier + .weight(1f) + .padding(ThemeShapes.HorizontalPadding), + label = state.localizer.localize("APP.GENERAL.DESTINATION"), + value = state.destinationValue, + icon = state.destinationIcon, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + DydxVaultReceiptView.Content( + modifier = Modifier.offset(y = ThemeShapes.VerticalPadding), + ) + + InputCtaButton.Content( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(bottom = ThemeShapes.VerticalPadding * 2), + state = state.ctaButton, + ) + } + } + + @Composable + private fun ItemContent(modifier: Modifier, label: String?, value: String?, icon: Any?) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(ThemeShapes.VerticalPadding), + ) { + Text( + text = label ?: "-", + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.small), + ) + + val shape = RoundedCornerShape(10.dp) + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = ThemeColor.SemanticColor.layer_4.color, shape = shape) + .clip(shape) + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(ThemeShapes.VerticalPadding), + ) { + PlatformImage( + modifier = Modifier + .size(32.dp), + icon = icon, + ) + Text( + text = value ?: "-", + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_secondary), + ) + } + } + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/confirmation/DydxVaultConfirmationViewModel.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/confirmation/DydxVaultConfirmationViewModel.kt new file mode 100644 index 00000000..7e9147c7 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/confirmation/DydxVaultConfirmationViewModel.kt @@ -0,0 +1,120 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw.confirmation + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.functional.vault.VaultDepositData +import exchange.dydx.abacus.functional.vault.VaultFormValidationResult +import exchange.dydx.abacus.functional.vault.VaultWithdrawData +import exchange.dydx.abacus.output.Vault +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +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.shared.R +import exchange.dydx.trading.feature.shared.views.InputCtaButton +import exchange.dydx.trading.feature.vault.VaultInputStage +import exchange.dydx.trading.feature.vault.VaultInputState +import exchange.dydx.trading.feature.vault.VaultInputType +import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import javax.inject.Inject + +@HiltViewModel +class DydxVaultConfirmationViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val cosmosClient: CosmosV4WebviewClientProtocol, + private val inputState: VaultInputState, + private val router: DydxRouter, + private val parser: ParserProtocol, +) : ViewModel(), DydxViewModel { + + val state: Flow = + combine( + abacusStateManager.state.vault, + inputState.amount, + inputState.type.filterNotNull(), + inputState.result, + ) { vault, amount, type, result -> + createViewState(vault, amount, type, result) + } + + private fun createViewState( + vault: Vault?, + amount: Double?, + type: VaultInputType, + result: VaultFormValidationResult? + ): DydxVaultConfirmationView.ViewState { + return DydxVaultConfirmationView.ViewState( + localizer = localizer, + direction = when (type) { + VaultInputType.DEPOSIT -> DydxVaultConfirmationView.Direction.Deposit + VaultInputType.WITHDRAW -> DydxVaultConfirmationView.Direction.Withdraw + }, + headerTitle = when (type) { + VaultInputType.DEPOSIT -> localizer.localize("APP.VAULTS.CONFIRM_DEPOSIT_CTA") + VaultInputType.WITHDRAW -> localizer.localize("APP.VAULTS.CONFIRM_WITHDRAW_CTA") + }, + sourceLabel = when (type) { + VaultInputType.DEPOSIT -> localizer.localize("APP.VAULTS.AMOUNT_TO_DEPOSIT") + VaultInputType.WITHDRAW -> localizer.localize("APP.VAULTS.AMOUNT_TO_WITHDRAW") + }, + sourceValue = formatter.dollar(amount, digits = 2), + destinationValue = when (type) { + VaultInputType.DEPOSIT -> localizer.localize("APP.VAULTS.VAULT") + VaultInputType.WITHDRAW -> localizer.localize("APP.VAULTS.CROSS_ACCOUNT") + }, + destinationIcon = when (type) { + VaultInputType.DEPOSIT -> R.drawable.vault_account_token + VaultInputType.WITHDRAW -> R.drawable.vault_cross_token + }, + backAction = { + router.navigateBack() + inputState.stage.value = VaultInputStage.EDIT + }, + ctaButton = InputCtaButton.ViewState( + localizer = localizer, + ctaButtonState = InputCtaButton.State.Enabled( + when (type) { + VaultInputType.DEPOSIT -> localizer.localize("APP.VAULTS.CONFIRM_DEPOSIT_CTA") + VaultInputType.WITHDRAW -> localizer.localize("APP.VAULTS.CONFIRM_WITHDRAW_CTA") + }, + ), + ctaAction = { + when (type) { + VaultInputType.DEPOSIT -> submitDeposit(result?.submissionData?.deposit) + VaultInputType.WITHDRAW -> submitWithdraw(result?.submissionData?.withdraw) + } + }, + ), + ) + } + + private fun submitDeposit(depositData: VaultDepositData?) { + val depositData = depositData ?: return + cosmosClient.depositToMegavault( + subaccountNumber = parser.asInt(depositData.subaccountFrom) ?: 0, + amountUsdc = depositData.amount, + completion = { response -> + print(response) + }, + ) + } + + private fun submitWithdraw(withdrawData: VaultWithdrawData?) { + val withdrawData = withdrawData ?: return + cosmosClient.withdrawFromMegavault( + subaccountNumber = parser.asInt(withdrawData.subaccountTo) ?: 0, + shares = withdrawData.shares.toLong(), + minAmount = withdrawData.minAmount.toLong(), + completion = { response -> + print(response) + }, + ) + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/deposit/DydxVaultDepositView.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/deposit/DydxVaultDepositView.kt new file mode 100644 index 00000000..751a0302 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/deposit/DydxVaultDepositView.kt @@ -0,0 +1,102 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw.deposit + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +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.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.compose.collectAsStateWithLifecycle +import exchange.dydx.platformui.designSystem.theme.ThemeShapes +import exchange.dydx.platformui.theme.DydxThemedPreviewSurface +import exchange.dydx.platformui.theme.MockLocalizer +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.feature.receipt.validation.DydxValidationView +import exchange.dydx.trading.feature.shared.views.InputCtaButton +import exchange.dydx.trading.feature.vault.depositwithdraw.components.VaultAmountBox +import exchange.dydx.trading.feature.vault.receipt.DydxVaultReceiptView + +@Preview +@Composable +fun Preview_DydxVaultDepositView() { + DydxThemedPreviewSurface { + DydxVaultDepositView.Content(Modifier, DydxVaultDepositView.ViewState.preview) + } +} + +object DydxVaultDepositView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val transferAmount: VaultAmountBox.ViewState? = null, + val ctaButton: InputCtaButton.ViewState? = null, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + transferAmount = VaultAmountBox.ViewState.preview, + ctaButton = InputCtaButton.ViewState.preview, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxVaultDepositViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + + Column( + modifier = modifier + .fillMaxSize(), + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(vertical = 16.dp) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + VaultAmountBox.Content( + modifier = Modifier.animateItemPlacement(), + state = state.transferAmount, + ) + } + + item { + DydxValidationView.Content(Modifier.animateItemPlacement()) + } + } + + DydxVaultReceiptView.Content( + modifier = Modifier.offset(y = ThemeShapes.VerticalPadding), + ) + + InputCtaButton.Content( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(bottom = ThemeShapes.VerticalPadding * 2), + state = state.ctaButton, + ) + } + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/deposit/DydxVaultDepositViewModel.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/deposit/DydxVaultDepositViewModel.kt new file mode 100644 index 00000000..28d4ff36 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/deposit/DydxVaultDepositViewModel.kt @@ -0,0 +1,88 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw.deposit + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.functional.vault.VaultFormValidationResult +import exchange.dydx.abacus.output.account.Subaccount +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.common.navigation.DydxRouter +import exchange.dydx.trading.common.navigation.VaultRoutes +import exchange.dydx.trading.feature.shared.views.AmountText +import exchange.dydx.trading.feature.shared.views.InputCtaButton +import exchange.dydx.trading.feature.vault.VaultInputStage +import exchange.dydx.trading.feature.vault.VaultInputState +import exchange.dydx.trading.feature.vault.depositwithdraw.components.VaultAmountBox +import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import javax.inject.Inject + +@HiltViewModel +class DydxVaultDepositViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val parser: ParserProtocol, + private val cosmosClient: CosmosV4WebviewClientProtocol, + private val inputState: VaultInputState, + private val router: DydxRouter, +) : ViewModel(), DydxViewModel { + + val state: Flow = + combine( + abacusStateManager.state.selectedSubaccount, + inputState.result, + ) { subaccount, result -> + createViewState(subaccount, result) + } + + private fun createViewState( + subaccount: Subaccount?, + result: VaultFormValidationResult? + ): DydxVaultDepositView.ViewState { + return DydxVaultDepositView.ViewState( + localizer = localizer, + transferAmount = VaultAmountBox.ViewState( + localizer = localizer, + formatter = formatter, + parser = parser, + value = parser.asString(inputState.amount.value), + maxAmount = subaccount?.freeCollateral?.current, + maxAction = { + inputState.amount.value = subaccount?.freeCollateral?.current + }, + title = localizer.localize("APP.VAULTS.ENTER_AMOUNT_TO_DEPOSIT"), + footer = localizer.localize("APP.GENERAL.CROSS_FREE_COLLATERAL"), + footerBefore = AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = subaccount?.freeCollateral?.current, + tickSize = 2, + requiresPositive = true, + ), + footerAfter = AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = result?.summaryData?.freeCollateral, + tickSize = 2, + requiresPositive = true, + ), + onEditAction = { amount -> + inputState.amount.value = parser.asDouble(amount) + }, + ), + ctaButton = InputCtaButton.ViewState( + localizer = localizer, + ctaButtonState = InputCtaButton.State.Enabled(localizer.localize("APP.VAULTS.PREVIEW_DEPOSIT")), + ctaAction = { + inputState.stage.value = VaultInputStage.CONFIRM + router.navigateTo(route = VaultRoutes.confirmation, presentation = DydxRouter.Presentation.Push) + }, + ), + ) + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/withdraw/DydxVaultWithdrawView.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/withdraw/DydxVaultWithdrawView.kt new file mode 100644 index 00000000..23edac17 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/withdraw/DydxVaultWithdrawView.kt @@ -0,0 +1,102 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw.withdraw + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +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.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.compose.collectAsStateWithLifecycle +import exchange.dydx.platformui.designSystem.theme.ThemeShapes +import exchange.dydx.platformui.theme.DydxThemedPreviewSurface +import exchange.dydx.platformui.theme.MockLocalizer +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.feature.receipt.validation.DydxValidationView +import exchange.dydx.trading.feature.shared.views.InputCtaButton +import exchange.dydx.trading.feature.vault.depositwithdraw.components.VaultAmountBox +import exchange.dydx.trading.feature.vault.receipt.DydxVaultReceiptView + +@Preview +@Composable +fun Preview_DydxVaultWithdrawView() { + DydxThemedPreviewSurface { + DydxVaultWithdrawView.Content(Modifier, DydxVaultWithdrawView.ViewState.preview) + } +} + +object DydxVaultWithdrawView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val transferAmount: VaultAmountBox.ViewState? = null, + val ctaButton: InputCtaButton.ViewState? = null, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + transferAmount = VaultAmountBox.ViewState.preview, + ctaButton = InputCtaButton.ViewState.preview, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxVaultWithdrawViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + + Column( + modifier = modifier + .fillMaxSize(), + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(vertical = 16.dp) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + VaultAmountBox.Content( + modifier = Modifier.animateItemPlacement(), + state = state.transferAmount, + ) + } + + item { + DydxValidationView.Content(Modifier.animateItemPlacement()) + } + } + + DydxVaultReceiptView.Content( + modifier = Modifier.offset(y = ThemeShapes.VerticalPadding), + ) + + InputCtaButton.Content( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(bottom = ThemeShapes.VerticalPadding * 2), + state = state.ctaButton, + ) + } + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/withdraw/DydxVaultWithdrawViewModel.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/withdraw/DydxVaultWithdrawViewModel.kt new file mode 100644 index 00000000..28218013 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/withdraw/DydxVaultWithdrawViewModel.kt @@ -0,0 +1,133 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw.withdraw + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.functional.vault.VaultAccount +import exchange.dydx.abacus.functional.vault.VaultDepositWithdrawFormValidator +import exchange.dydx.abacus.functional.vault.VaultFormValidationResult +import exchange.dydx.abacus.output.Vault +import exchange.dydx.abacus.output.account.Subaccount +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.di.CoroutineScopes +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.common.navigation.DydxRouter +import exchange.dydx.trading.common.navigation.VaultRoutes +import exchange.dydx.trading.feature.shared.views.AmountText +import exchange.dydx.trading.feature.shared.views.InputCtaButton +import exchange.dydx.trading.feature.vault.VaultInputStage +import exchange.dydx.trading.feature.vault.VaultInputState +import exchange.dydx.trading.feature.vault.depositwithdraw.components.VaultAmountBox +import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import java.util.Timer +import javax.inject.Inject + +@HiltViewModel +class DydxVaultWithdrawViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val parser: ParserProtocol, + private val cosmosClient: CosmosV4WebviewClientProtocol, + private val inputState: VaultInputState, + private val router: DydxRouter, + @CoroutineScopes.ViewModel private val coroutineScope: CoroutineScope +) : ViewModel(), DydxViewModel { + + private var slippageRequestTimer: Timer? = null + + val state: Flow = + combine( + abacusStateManager.state.selectedSubaccount, + abacusStateManager.state.vault, + inputState.result, + ) { subaccount, vault, result -> + createViewState(subaccount, vault, result) + } + + private var slippageDebounce: Job? = null + + private fun createViewState( + subaccount: Subaccount?, + vault: Vault?, + result: VaultFormValidationResult? + ): DydxVaultWithdrawView.ViewState { + return DydxVaultWithdrawView.ViewState( + localizer = localizer, + transferAmount = VaultAmountBox.ViewState( + localizer = localizer, + formatter = formatter, + parser = parser, + value = parser.asString(inputState.amount.value), + maxAmount = vault?.account?.withdrawableUsdc, + maxAction = { + updateAmount(value = vault?.account?.withdrawableUsdc, vaultAccount = vault?.account) + }, + title = localizer.localize("APP.VAULTS.ENTER_AMOUNT_TO_WITHDRAW"), + footer = localizer.localize("APP.VAULTS.YOUR_VAULT_BALANCE"), + footerBefore = AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = vault?.account?.withdrawableUsdc, + tickSize = 2, + requiresPositive = true, + ), + footerAfter = AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = result?.summaryData?.withdrawableVaultBalance, + tickSize = 2, + requiresPositive = true, + ), + onEditAction = { amount -> + updateAmount(value = parser.asDouble(amount), vaultAccount = vault?.account) + }, + ), + ctaButton = InputCtaButton.ViewState( + localizer = localizer, + ctaButtonState = InputCtaButton.State.Enabled(localizer.localize("APP.VAULTS.PREVIEW_WITHDRAW")), + ctaAction = { + inputState.stage.value = VaultInputStage.CONFIRM + router.navigateTo(route = VaultRoutes.confirmation, presentation = DydxRouter.Presentation.Push) + }, + ), + ) + } + + private fun updateAmount( + value: Double?, + vaultAccount: VaultAccount? + ) { + val shareValue = vaultAccount?.shareValue + if (value != null && shareValue != null && shareValue > 0) { + val shares = value / shareValue + requestSlippage(shares.toLong()) + } + inputState.amount.value = value + } + + private fun requestSlippage(shares: Long) { + slippageDebounce?.cancel() + slippageDebounce = coroutineScope.launch { + delay(500L) + cosmosClient.getMegavaultWithdrawalInfo( + shares = shares, + completion = { response -> + if (response != null) { + inputState.slippageResponse.value = VaultDepositWithdrawFormValidator.getVaultDepositWithdrawSlippageResponse(response) + } else { + inputState.slippageResponse.value = null + } + }, + ) + } + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/receipt/DydxVaultReceiptView.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/receipt/DydxVaultReceiptView.kt new file mode 100644 index 00000000..f7d53010 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/receipt/DydxVaultReceiptView.kt @@ -0,0 +1,154 @@ +package exchange.dydx.trading.feature.vault.receipt + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +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.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.compose.collectAsStateWithLifecycle +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.ThemeShapes +import exchange.dydx.platformui.designSystem.theme.color +import exchange.dydx.platformui.theme.DydxThemedPreviewSurface +import exchange.dydx.platformui.theme.MockLocalizer +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.feature.receipt.components.DydxReceiptItemView +import exchange.dydx.trading.feature.receipt.components.buyingpower.DydxReceiptFreeCollateralView +import exchange.dydx.trading.feature.receipt.components.marginusage.DydxReceiptMarginUsageView +import exchange.dydx.trading.feature.receipt.components.slippage.DydxReceiptSlippageView + +@Preview +@Composable +fun Preview_DydxVaultReceiptView() { + DydxThemedPreviewSurface { + DydxVaultReceiptView.Content(Modifier, DydxVaultReceiptView.ViewState.preview) + } +} + +object DydxVaultReceiptView : DydxComponent { + enum class VaultReceiptLineType { + FreeCollateral, + MarginUsage, + Balance, + Slippage, + AmountReceived, + } + + data class ViewState( + val localizer: LocalizerProtocol, + val lineTypes: List = emptyList(), + val freeCollateral: DydxReceiptFreeCollateralView.ViewState? = null, + val marginUsage: DydxReceiptMarginUsageView.ViewState? = null, + val slippage: DydxReceiptItemView.ViewState? = null, + val balance: DydxReceiptFreeCollateralView.ViewState? = null, + val amountReceived: DydxReceiptItemView.ViewState? = null, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + lineTypes = listOf( + VaultReceiptLineType.FreeCollateral, + VaultReceiptLineType.MarginUsage, + VaultReceiptLineType.Balance, + ), + freeCollateral = DydxReceiptFreeCollateralView.ViewState.preview, + marginUsage = DydxReceiptMarginUsageView.ViewState.preview, + slippage = DydxReceiptItemView.ViewState.preview, + balance = DydxReceiptFreeCollateralView.ViewState.preview, + amountReceived = DydxReceiptItemView.ViewState.preview, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxVaultReceiptViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + Box( + modifier = modifier + .heightIn(max = 210.dp) + .fillMaxWidth() + .padding(horizontal = ThemeShapes.HorizontalPadding) + .background( + color = ThemeColor.SemanticColor.layer_1.color, + shape = RoundedCornerShape(10.dp), + ), + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = ThemeShapes.HorizontalPadding, + vertical = ThemeShapes.VerticalPadding * 2, + ), + verticalArrangement = Arrangement.spacedBy(ThemeShapes.VerticalPadding), + ) { + items(state.lineTypes, key = { it }) { lineType -> + when (lineType) { + VaultReceiptLineType.FreeCollateral -> { + DydxReceiptFreeCollateralView.Content( + modifier = Modifier.animateItemPlacement(), + state = state.freeCollateral, + ) + } + + VaultReceiptLineType.MarginUsage -> { + DydxReceiptMarginUsageView.Content( + modifier = Modifier.animateItemPlacement(), + state = state.marginUsage, + ) + } + + VaultReceiptLineType.Balance -> { + DydxReceiptFreeCollateralView.Content( + modifier = Modifier.animateItemPlacement(), + state = state.balance, + ) + } + + VaultReceiptLineType.Slippage -> { + DydxReceiptSlippageView.Content( + modifier = Modifier.animateItemPlacement(), + state = state.slippage, + ) + } + + VaultReceiptLineType.AmountReceived -> { + DydxReceiptItemView.Content( + modifier = Modifier.animateItemPlacement(), + state = state.amountReceived, + ) + } + } + } + + item { + Spacer(modifier = Modifier.height(ThemeShapes.VerticalPadding)) + } + } + } + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/receipt/DydxVaultReceiptViewModel.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/receipt/DydxVaultReceiptViewModel.kt new file mode 100644 index 00000000..7959fdd8 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/receipt/DydxVaultReceiptViewModel.kt @@ -0,0 +1,176 @@ +package exchange.dydx.trading.feature.vault.receipt + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.functional.vault.VaultFormValidationResult +import exchange.dydx.abacus.output.Vault +import exchange.dydx.abacus.output.account.Subaccount +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.feature.receipt.components.DydxReceiptItemView +import exchange.dydx.trading.feature.receipt.components.buyingpower.DydxReceiptFreeCollateralView +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 exchange.dydx.trading.feature.vault.VaultInputStage +import exchange.dydx.trading.feature.vault.VaultInputState +import exchange.dydx.trading.feature.vault.VaultInputType +import exchange.dydx.trading.feature.vault.receipt.DydxVaultReceiptView.VaultReceiptLineType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import javax.inject.Inject + +@HiltViewModel +class DydxVaultReceiptViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val inputState: VaultInputState, + private val parser: ParserProtocol, +) : ViewModel(), DydxViewModel { + + val state: Flow = + combine( + abacusStateManager.state.selectedSubaccount, + abacusStateManager.state.vault, + inputState.type, + inputState.stage, + inputState.result, + ) { subaccount, vault, type, stage, result -> + when (type) { + VaultInputType.DEPOSIT -> createDepositViewState(subaccount, vault, stage, result) + VaultInputType.WITHDRAW -> createWithdrawViewState(subaccount, vault, stage, result) + else -> null + } + } + + private fun createDepositViewState( + subaccount: Subaccount?, + vault: Vault?, + stage: VaultInputStage, + result: VaultFormValidationResult? + ): DydxVaultReceiptView.ViewState { + return DydxVaultReceiptView.ViewState( + localizer = localizer, + lineTypes = listOfNotNull( + if (stage == VaultInputStage.CONFIRM) VaultReceiptLineType.FreeCollateral else null, + VaultReceiptLineType.MarginUsage, + VaultReceiptLineType.Balance, + ), + freeCollateral = createFreeCollateralViewState(subaccount, result), + marginUsage = createMarginUsageViewState(subaccount, result), + balance = createBalanceViewState(vault, result), + ) + } + + private fun createWithdrawViewState( + subaccount: Subaccount?, + vault: Vault?, + stage: VaultInputStage, + result: VaultFormValidationResult? + ): DydxVaultReceiptView.ViewState { + return DydxVaultReceiptView.ViewState( + localizer = localizer, + lineTypes = listOfNotNull( + VaultReceiptLineType.FreeCollateral, + if (stage == VaultInputStage.CONFIRM) VaultReceiptLineType.Balance else null, + VaultReceiptLineType.Slippage, + VaultReceiptLineType.AmountReceived, + ), + freeCollateral = createFreeCollateralViewState(subaccount, result), + balance = createBalanceViewState(vault, result), + slippage = DydxReceiptItemView.ViewState( + localizer = localizer, + title = localizer.localize("APP.VAULTS.EST_SLIPPAGE"), + value = formatter.percent(result?.summaryData?.estimatedSlippage, digits = 2), + ), + amountReceived = DydxReceiptItemView.ViewState( + localizer = localizer, + title = localizer.localize("APP.WITHDRAW_MODAL.EXPECTED_AMOUNT_RECEIVED"), + value = formatter.dollar(result?.summaryData?.estimatedAmountReceived, digits = 2), + ), + ) + } + + private fun createFreeCollateralViewState( + subaccount: Subaccount?, + result: VaultFormValidationResult? + ): DydxReceiptFreeCollateralView.ViewState { + return DydxReceiptFreeCollateralView.ViewState( + localizer = localizer, + before = subaccount?.freeCollateral?.current?.let { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = it, + tickSize = 2, + requiresPositive = true, + ) + }, + after = result?.summaryData?.freeCollateral?.let { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = it, + tickSize = 2, + requiresPositive = true, + ) + }, + ) + } + + private fun createBalanceViewState( + vault: Vault?, + result: VaultFormValidationResult? + ): DydxReceiptFreeCollateralView.ViewState { + return DydxReceiptFreeCollateralView.ViewState( + localizer = localizer, + label = localizer.localize("APP.VAULTS.YOUR_VAULT_BALANCE"), + before = vault?.account?.balanceUsdc?.let { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = it, + tickSize = 2, + requiresPositive = true, + ) + }, + after = result?.summaryData?.vaultBalance?.let { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = it, + tickSize = 2, + requiresPositive = true, + ) + }, + ) + } + + private fun createMarginUsageViewState( + subaccount: Subaccount?, + result: VaultFormValidationResult? + ): DydxReceiptMarginUsageView.ViewState { + return DydxReceiptMarginUsageView.ViewState( + localizer = localizer, + formatter = formatter, + before = subaccount?.marginUsage?.current?.let { + MarginUsageView.ViewState( + localizer = localizer, + percent = it, + displayOption = MarginUsageView.DisplayOption.IconAndValue, + ) + }, + after = result?.summaryData?.marginUsage?.let { + MarginUsageView.ViewState( + localizer = localizer, + percent = it, + displayOption = MarginUsageView.DisplayOption.IconAndValue, + ) + }, + ) + } +} diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/buttons/PlatformPillButton.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/buttons/PlatformPillButton.kt index 8b1476c7..b9e72717 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/buttons/PlatformPillButton.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/buttons/PlatformPillButton.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonDefaults @@ -47,6 +48,7 @@ fun PlatformPillItem( modifier: Modifier = Modifier, backgroundColor: ThemeColor.SemanticColor = ThemeColor.SemanticColor.layer_5, borderColor: ThemeColor.SemanticColor = ThemeColor.SemanticColor.layer_6, + padding: PaddingValues = PaddingValues(horizontal = 10.dp, vertical = 8.dp), content: @Composable () -> Unit ) { val shape = RoundedCornerShape(50) @@ -62,7 +64,7 @@ fun PlatformPillItem( ) .clip(shape) .then(modifier) - .padding(horizontal = 10.dp, vertical = 8.dp), + .padding(padding), ) { content() diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/changes/PlatformDirectionArrow.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/changes/PlatformDirectionArrow.kt index 9323cfb4..9613daf1 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/changes/PlatformDirectionArrow.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/changes/PlatformDirectionArrow.kt @@ -11,13 +11,14 @@ import exchange.dydx.platformui.designSystem.theme.negativeColor import exchange.dydx.platformui.designSystem.theme.positiveColor enum class PlatformDirection { - Up, Down, None; + Up, Down, None, Hide; companion object { fun from(value1: Double?, value2: Double?): PlatformDirection { return when { - (value1 ?: 0.0) > (value2 ?: 0.0) -> Down - (value1 ?: 0.0) < (value2 ?: 0.0) -> Up + value1 == null || value2 == null -> Hide + value1 > value2 -> Down + value1 < value2 -> Up else -> None } } @@ -29,6 +30,7 @@ fun PlatformDirectionArrow( modifier: Modifier = Modifier, direction: PlatformDirection = PlatformDirection.None, ) { + if (direction == PlatformDirection.Hide) return PlatformImage( icon = R.drawable.icon_arrow, modifier = modifier, @@ -36,6 +38,7 @@ fun PlatformDirectionArrow( PlatformDirection.Up -> ColorFilter.tint(color = ThemeColor.SemanticColor.positiveColor.color) PlatformDirection.Down -> ColorFilter.tint(color = ThemeColor.SemanticColor.negativeColor.color) PlatformDirection.None -> ColorFilter.tint(color = ThemeColor.SemanticColor.text_tertiary.color) + else -> null }, ) } diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/tabgroups/PlatformPillTextGroup.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/tabgroups/PlatformPillTextGroup.kt index 2ec6afce..150284a5 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/tabgroups/PlatformPillTextGroup.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/tabgroups/PlatformPillTextGroup.kt @@ -1,9 +1,11 @@ package exchange.dydx.platformui.components.tabgroups +import androidx.compose.foundation.layout.PaddingValues 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.unit.dp import exchange.dydx.platformui.components.buttons.PlatformPillItem import exchange.dydx.platformui.designSystem.theme.ThemeColor import exchange.dydx.platformui.designSystem.theme.ThemeFont @@ -22,6 +24,7 @@ fun PlatformPillTextGroup( selectedItemStyle: TextStyle = TextStyle.dydxDefault .themeColor(ThemeColor.SemanticColor.text_primary) .themeFont(fontType = ThemeFont.FontType.plus, fontSize = ThemeFont.FontSize.extra), + padding: PaddingValues = PaddingValues(horizontal = 10.dp, vertical = 8.dp), currentSelection: Int? = null, scrollingEnabled: Boolean = false, onSelectionChanged: (Int) -> Unit = {}, @@ -33,6 +36,7 @@ fun PlatformPillTextGroup( PlatformPillItem( modifier = modifier, backgroundColor = ThemeColor.SemanticColor.layer_5, + padding = padding, ) { Text( text = item, @@ -47,6 +51,7 @@ fun PlatformPillTextGroup( PlatformPillItem( modifier = modifier, backgroundColor = ThemeColor.SemanticColor.layer_2, + padding = padding, ) { Text( text = item,