diff --git a/v4/build.gradle b/v4/build.gradle index 82c75529..34e7cf8d 100644 --- a/v4/build.gradle +++ b/v4/build.gradle @@ -88,7 +88,7 @@ ext { compileSdkVersion = 34 // App dependencies - abacusVersion = '1.12.14' + abacusVersion = '1.12.16' carteraVersion = '0.1.15' kollectionsVersion = '2.0.16' 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 index b410f41d..9fef77de 100644 --- 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 @@ -115,4 +115,4 @@ val VaultFormValidationResult.canDeposit: Boolean get() = submissionData?.deposit != null val VaultFormValidationResult.canWithdraw: Boolean - get() = submissionData?.withdraw != null \ No newline at end of file + get() = submissionData?.withdraw != null diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/ValidationError+Vault.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/ValidationErrorVaultExt.kt similarity index 94% rename from v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/ValidationError+Vault.kt rename to v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/ValidationErrorVaultExt.kt index 47e63446..a4f86c2a 100644 --- a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/ValidationError+Vault.kt +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/ValidationErrorVaultExt.kt @@ -5,7 +5,7 @@ import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.trading.feature.receipt.validation.DydxValidationView -internal fun ValidationError.createViewModel( +internal fun ValidationError.createViewModel( localizer: LocalizerProtocol ): DydxValidationView.ViewState { return DydxValidationView.ViewState( @@ -18,4 +18,4 @@ internal fun ValidationError.createViewModel( title = this.resources.title?.localized ?: this.resources.title?.stringKey, message = this.resources.text?.localized ?: this.resources.text?.stringKey, ) -} \ No newline at end of file +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/components/VaultSlippageCheckbox.kt b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/components/VaultSlippageCheckbox.kt new file mode 100644 index 00000000..a2344ce7 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/trading/feature/vault/depositwithdraw/components/VaultSlippageCheckbox.kt @@ -0,0 +1,77 @@ +package exchange.dydx.trading.feature.vault.depositwithdraw.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.icons.PlatformSelectedIcon +import exchange.dydx.platformui.components.icons.PlatformUnselectedIcon +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 + +@Preview +@Composable +fun Preview_VaultSlippageCheckbox() { + DydxThemedPreviewSurface { + VaultSlippageCheckbox.Content(Modifier, VaultSlippageCheckbox.ViewState.preview) + } +} + +object VaultSlippageCheckbox { + data class ViewState( + val localizer: LocalizerProtocol, + val text: String?, + val checked: Boolean = false, + val onCheckedChange: (Boolean) -> Unit = {}, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + text = "1.0M", + ) + } + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + + Row( + modifier = modifier + .clickable { + state.onCheckedChange(!state.checked) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (state.checked) { + PlatformSelectedIcon(size = 24.dp) + } else { + PlatformUnselectedIcon(size = 24.dp) + } + + Text( + text = state.text ?: "", + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_secondary) + .themeFont(fontSize = ThemeFont.FontSize.base), + modifier = Modifier.padding(start = 8.dp).weight(1f), + ) + } + } +} 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 index 62f3ece0..ce0f4701 100644 --- 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 @@ -37,6 +37,7 @@ 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.depositwithdraw.components.VaultSlippageCheckbox import exchange.dydx.trading.feature.vault.receipt.DydxVaultReceiptView @Preview @@ -62,6 +63,7 @@ object DydxVaultConfirmationView : DydxComponent { val ctaButton: InputCtaButton.ViewState? = null, val backAction: (() -> Unit)? = null, val direction: Direction? = null, + val slippage: VaultSlippageCheckbox.ViewState? = null, ) { companion object { val preview = ViewState( @@ -72,6 +74,7 @@ object DydxVaultConfirmationView : DydxComponent { destinationValue = "Vault", ctaButton = InputCtaButton.ViewState.preview, direction = Direction.Deposit, + slippage = VaultSlippageCheckbox.ViewState.preview, ) } } @@ -139,6 +142,13 @@ object DydxVaultConfirmationView : DydxComponent { modifier = Modifier.offset(y = ThemeShapes.VerticalPadding), ) + if (state.slippage != null) { + VaultSlippageCheckbox.Content( + modifier = Modifier.padding(ThemeShapes.HorizontalPadding), + state = state.slippage, + ) + } + InputCtaButton.Content( modifier = Modifier .fillMaxWidth() 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 index b64987e7..5e8135e9 100644 --- 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 @@ -1,6 +1,7 @@ package exchange.dydx.trading.feature.vault.depositwithdraw.confirmation import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import exchange.dydx.abacus.functional.vault.VaultDepositData import exchange.dydx.abacus.functional.vault.VaultFormValidationResult @@ -9,6 +10,7 @@ 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.dydxstatemanager.localizeWithParams import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.common.navigation.DydxRouter @@ -19,10 +21,13 @@ import exchange.dydx.trading.feature.vault.VaultInputState import exchange.dydx.trading.feature.vault.VaultInputType import exchange.dydx.trading.feature.vault.canDeposit import exchange.dydx.trading.feature.vault.canWithdraw +import exchange.dydx.trading.feature.vault.depositwithdraw.components.VaultSlippageCheckbox import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -36,21 +41,25 @@ class DydxVaultConfirmationViewModel @Inject constructor( private val parser: ParserProtocol, ) : ViewModel(), DydxViewModel { + private val isSubmitting: MutableStateFlow = MutableStateFlow(false) + val state: Flow = combine( abacusStateManager.state.vault, inputState.amount, inputState.type.filterNotNull(), inputState.result, - ) { vault, amount, type, result -> - createViewState(vault, amount, type, result) + isSubmitting, + ) { vault, amount, type, result, isSubmitting -> + createViewState(vault, amount, type, result, isSubmitting) } private fun createViewState( vault: Vault?, amount: Double?, type: VaultInputType, - result: VaultFormValidationResult? + result: VaultFormValidationResult?, + isSubmitting: Boolean, ): DydxVaultConfirmationView.ViewState { return DydxVaultConfirmationView.ViewState( localizer = localizer, @@ -79,20 +88,45 @@ class DydxVaultConfirmationViewModel @Inject constructor( router.navigateBack() inputState.stage.value = VaultInputStage.EDIT }, - ctaButton = createInputCtaButton(type, result), + ctaButton = createInputCtaButton(type, result, isSubmitting), + slippage = createSlippage(type, result), ) } - private fun createInputCtaButton( + private fun createSlippage( type: VaultInputType, result: VaultFormValidationResult? + ): VaultSlippageCheckbox.ViewState? { + if (type == VaultInputType.WITHDRAW && result?.summaryData?.needSlippageAck == true) { + val slippage = formatter.percent(result?.summaryData?.estimatedSlippage, digits = 2) ?: "" + val slippageText = localizer.localizeWithParams( + path = "APP.VAULTS.SLIPPAGE_ACK", + params = mapOf("AMOUNT" to slippage), + ) + return VaultSlippageCheckbox.ViewState( + localizer = localizer, + text = slippageText, + checked = inputState.slippageAcked.value, + onCheckedChange = { inputState.slippageAcked.value = it }, + ) + } else { + return null + } + } + + private fun createInputCtaButton( + type: VaultInputType, + result: VaultFormValidationResult?, + isSubmitting: Boolean, ): InputCtaButton.ViewState { when (type) { VaultInputType.DEPOSIT -> { val ctaButtonTitle = localizer.localize("APP.VAULTS.CONFIRM_DEPOSIT_CTA") return InputCtaButton.ViewState( localizer = localizer, - ctaButtonState = if (result?.canDeposit == true) { + ctaButtonState = if (isSubmitting) { + InputCtaButton.State.Disabled(localizer.localize("APP.TRADE.SUBMITTING")) + } else if (result?.canDeposit == true) { InputCtaButton.State.Enabled(ctaButtonTitle) } else { InputCtaButton.State.Disabled(ctaButtonTitle) @@ -106,7 +140,9 @@ class DydxVaultConfirmationViewModel @Inject constructor( val ctaButtonTitle = localizer.localize("APP.VAULTS.CONFIRM_WITHDRAW_CTA") return InputCtaButton.ViewState( localizer = localizer, - ctaButtonState = if (result?.canWithdraw == true) { + ctaButtonState = if (isSubmitting) { + InputCtaButton.State.Disabled(localizer.localize("APP.TRADE.SUBMITTING")) + } else if (result?.canWithdraw == true) { InputCtaButton.State.Enabled(ctaButtonTitle) } else { InputCtaButton.State.Disabled(ctaButtonTitle) @@ -121,25 +157,39 @@ class DydxVaultConfirmationViewModel @Inject constructor( private fun submitDeposit(depositData: VaultDepositData?) { val depositData = depositData ?: return + isSubmitting.value = true cosmosClient.depositToMegavault( subaccountNumber = parser.asInt(depositData.subaccountFrom) ?: 0, amountUsdc = depositData.amount, completion = { response -> print(response) abacusStateManager.refreshVaultAccount() + inputState.reset() + routeToVault() }, ) } private fun submitWithdraw(withdrawData: VaultWithdrawData?) { val withdrawData = withdrawData ?: return + isSubmitting.value = true cosmosClient.withdrawFromMegavault( subaccountNumber = parser.asInt(withdrawData.subaccountTo) ?: 0, shares = withdrawData.shares.toLong(), minAmount = withdrawData.minAmount.toLong(), completion = { response -> print(response) + abacusStateManager.refreshVaultAccount() + inputState.reset() + routeToVault() }, ) } + + private fun routeToVault() { + viewModelScope.launch { + router.navigateBack() + router.navigateBack() + } + } } 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 index 2d39ed9d..010b67ba 100644 --- 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 @@ -11,12 +11,10 @@ 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.receipt.validation.DydxValidationView 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.canDeposit import exchange.dydx.trading.feature.vault.depositwithdraw.components.VaultAmountBox import exchange.dydx.trading.feature.vault.depositwithdraw.createViewModel import exchange.dydx.trading.feature.vault.displayedError @@ -80,13 +78,13 @@ class DydxVaultDepositViewModel @Inject constructor( inputState.amount.value = parser.asDouble(amount) }, ), - validation = result?.displayedError?.createViewModel(localizer), + validation = result?.displayedError?.createViewModel(localizer), ctaButton = InputCtaButton.ViewState( localizer = localizer, ctaButtonState = if (result?.hasBlockingError == true || inputState.amount.value == null) { InputCtaButton.State.Disabled(localizer.localize("APP.VAULTS.PREVIEW_DEPOSIT")) } else { - InputCtaButton.State.Enabled(localizer.localize("APP.VAULTS.PREVIEW_DEPOSIT")) + InputCtaButton.State.Enabled(localizer.localize("APP.VAULTS.PREVIEW_DEPOSIT")) }, ctaAction = { inputState.stage.value = VaultInputStage.CONFIRM diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/icons/PlatformIcons.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/icons/PlatformIcons.kt index 2808c94b..7950ce40 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/icons/PlatformIcons.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/icons/PlatformIcons.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.SnackbarDefaults.backgroundColor import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -113,13 +114,14 @@ fun PlatformSelectedIcon( size: Dp = 20.dp, padding: Dp = 4.dp, ) { + val shape = RoundedCornerShape(7.dp) Image( painter = painterResource(id = R.drawable.icon_check), contentDescription = "", modifier = Modifier - .background(backgroundColor.color, CircleShape) - .border(1.dp, borderColor.color, CircleShape) - .clip(CircleShape) + .background(backgroundColor.color, shape) + .border(1.dp, borderColor.color, shape) + .clip(shape) .size(size) .padding(padding), colorFilter = ColorFilter @@ -135,10 +137,11 @@ fun PlatformUnselectedIcon( size: Dp = 20.dp, padding: Dp = 4.dp, ) { + val shape = RoundedCornerShape(7.dp) Canvas( modifier = Modifier - .border(1.dp, borderColor.color, CircleShape) - .clip(CircleShape) + .border(1.dp, borderColor.color, shape) + .clip(shape) .size(size), ) { drawCircle(color = backgroundColor.color)