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,