diff --git a/gradle.properties b/gradle.properties index b1a2500f..28e18b99 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,8 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 org.gradle.parallel=true org.gradle.caching=true +# compilation avoidance for non public code changes +kotlin.incremental.useClasspathSnapshot=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK diff --git a/scripts/build_abacus_local.sh b/scripts/build_abacus_local.sh new file mode 100755 index 00000000..ac38d3d0 --- /dev/null +++ b/scripts/build_abacus_local.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# This script is used to build the abacus project locally. +# +# Please run this after making local change to the abacus project. The change will be +# picked up by the v4-native-android project (via the local maven repository). Note that +# the script needs to be run everytime a change is made to the abacus project. +# +# The script will update both the v4-abacus and v4-native-android repositories, so please make sure +# you clean-up/revert the changes after you are done with the local testing + +ABACUS_DIR=~/v4-abacus +ANDROID_DIR=~/v4-native-android + +# Create a random version number +NEW_VERSION="local.$(date +%s)" + +# search for the first line that starts with "version" in build.gradle.kts +# get the value in the quotes +VERSION=$(grep "^version = " ${ABACUS_DIR}/build.gradle.kts | sed -n 's/version = "\(.*\)"/\1/p') + +sed -i '' "s/version = \"$VERSION\"/version = \"$NEW_VERSION\"/" ${ABACUS_DIR}/build.gradle.kts +echo "Version bumped to $NEW_VERSION" + +echo "Building Abacus ..." + +cd ${ABACUS_DIR} +./gradlew publishToMavenLocal + +cd ${ANDROID_DIR} + +# get the version from the file +targetFileName="v4/build.gradle" +OLD_VERSION=$(grep "^ abacusVersion = " $targetFileName | sed -n 's/ abacusVersion = ''\(.*\)''/\1/p') + +if [ -n "$NEW_VERSION" ] && [ -n "$OLD_VERSION" ]; then + echo "Bumping Abacus version from $OLD_VERSION to $NEW_VERSION" + sed -i '' "s/^ abacusVersion = $OLD_VERSION/ abacusVersion = '$NEW_VERSION'/" $targetFileName + echo "Version bumped to $NEW_VERSION" +else + echo "No version found" +fi \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index faed481d..2f9a08cd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -40,7 +40,7 @@ dependencyResolutionManagement { } } } - // mavenLocal() + mavenLocal() } } diff --git a/v4/app/build.gradle b/v4/app/build.gradle index d5f72c66..eacc7ab7 100644 --- a/v4/app/build.gradle +++ b/v4/app/build.gradle @@ -19,7 +19,7 @@ android { minSdkVersion parent.minSdkVersion targetSdkVersion parent.targetSdkVersion versionCode 9 - versionName "1.0.0" + versionName "1.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/v4/build.gradle b/v4/build.gradle index 96438662..0bd53f24 100644 --- a/v4/build.gradle +++ b/v4/build.gradle @@ -89,7 +89,7 @@ ext { compileSdkVersion = 34 // App dependencies - abacusVersion = '1.6.40' + abacusVersion = '1.6.46' carteraVersion = '0.1.13' kollectionsVersion = '2.0.16' diff --git a/v4/common/src/main/java/exchange/dydx/trading/common/formatter/DydxFormatter.kt b/v4/common/src/main/java/exchange/dydx/trading/common/formatter/DydxFormatter.kt index b3e1c79d..5f614242 100644 --- a/v4/common/src/main/java/exchange/dydx/trading/common/formatter/DydxFormatter.kt +++ b/v4/common/src/main/java/exchange/dydx/trading/common/formatter/DydxFormatter.kt @@ -1,5 +1,6 @@ package exchange.dydx.trading.common.formatter +import exchange.dydx.utilities.utils.rounded import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat @@ -7,15 +8,18 @@ import java.text.DecimalFormatSymbols import java.text.NumberFormat import java.text.SimpleDateFormat import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit import java.util.Date import java.util.Locale -import java.util.regex.Pattern import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.abs import kotlin.math.absoluteValue +import kotlin.math.round @Singleton class DydxFormatter @Inject constructor() { @@ -37,12 +41,12 @@ class DydxFormatter @Inject constructor() { DateTimeFormatter.ofPattern("MMM dd").withZone(ZoneOffset.UTC) } - private val timeFormatter: SimpleDateFormat by lazy { - SimpleDateFormat("HH:mm:ss", locale) + private val timeFormatter: DateTimeFormatter by lazy { + DateTimeFormatter.ofPattern("HH:mm:ss", locale) } - private val dateTimeFormatter: SimpleDateFormat by lazy { - SimpleDateFormat("yyyy-MM-dd HH:mm:ss", locale) + private val dateTimeFormatter: DateTimeFormatter by lazy { + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", locale) } fun dollarVolume(number: Double?, digits: Int = 2): String? { @@ -97,15 +101,14 @@ class DydxFormatter @Inject constructor() { null } } - fun localFormatted(number: Double?, digits: Int): String? { - return localFormatted(number?.toBigDecimal(), digits) - } - - fun localFormatted(number: BigDecimal?, digits: Int): String? { + fun localFormatted(number: Double?, digits: Int?): String? { if (number != null) { + val number = if (digits != null) rounded(number, digits) else number val decimalFormat = NumberFormat.getInstance(locale) as? DecimalFormat - decimalFormat?.minimumFractionDigits = maxOf(digits, 0) - decimalFormat?.maximumFractionDigits = maxOf(digits, 0) + if (digits != null) { + decimalFormat?.minimumFractionDigits = maxOf(digits, 0) + decimalFormat?.maximumFractionDigits = maxOf(digits, 0) + } val formatted = decimalFormat?.format(number) val parsed = decimalFormat?.parse(formatted) @@ -118,39 +121,16 @@ class DydxFormatter @Inject constructor() { return null } - fun localFormatted(number: BigDecimal?, size: String): String? { - val digits = digits(size) - return localFormatted(number, digits) + fun dollar(number: Double?, size: Double? = null): String? { + return dollar(number = number, digits = digits(size)) } - private fun digits(size: String): Int { - val pattern = Pattern.compile("\\.(\\d+)") - - val matcher = pattern.matcher(size) - if (matcher.find()) { - return matcher.group(1).length - } - return 2 // Default digits - } - - fun dollar(number: Double?, size: String? = null): String? { - return dollar(number = number?.toBigDecimal(), size = size) - } - - fun dollar(number: BigDecimal?, size: String? = null): String? { - return dollar(number, digits = digits(size ?: "0.01")) - } - - fun dollar(number: Double?, digits: Int): String? { - return dollar(number = number?.toBigDecimal(), digits = digits) - } - - fun dollar(number: BigDecimal?, digits: Int): String? { + fun dollar(number: Double?, digits: Int?): String? { if (number == null) return null - val formattedNumber = localFormatted(number.abs(), digits) + val formattedNumber = localFormatted(abs(number), digits) return formattedNumber?.let { - val rawDouble = raw(number.toDouble(), digits)?.toDouble() ?: 0.0 - if (rawDouble >= 0.0) { + val rounded = if (digits != null && digits >= 0) number.rounded(toPlaces = digits) else number + if (rounded >= 0) { "$$it" } else { "-$$it" @@ -159,10 +139,6 @@ class DydxFormatter @Inject constructor() { } fun percent(number: Double?, digits: Int, minDigits: Int? = null): String? { - return percent(number = number?.toBigDecimal(), digits = digits, minDigits = minDigits) - } - - fun percent(number: BigDecimal?, digits: Int, minDigits: Int? = null): String? { if (number != null) { percentFormatter.minimumFractionDigits = minDigits ?: digits percentFormatter.maximumFractionDigits = digits @@ -233,17 +209,19 @@ class DydxFormatter @Inject constructor() { } } - fun dateTime(time: Instant?): String? { + fun dateTime(time: Instant?, timeZone: ZoneId = ZoneId.systemDefault()): String? { return if (time != null) { - dateTimeFormatter.format(Date.from(time)) + val ldt: LocalDateTime = time.atZone(timeZone).toLocalDateTime() + dateTimeFormatter.format(ldt) } else { null } } - fun clock(time: Instant?): String? { + fun clock(time: Instant?, timeZone: ZoneId = ZoneId.systemDefault()): String? { return if (time != null) { - timeFormatter.format(Date.from(time)) + val ldt: LocalDateTime = time.atZone(timeZone).toLocalDateTime() + timeFormatter.format(ldt) } else { null } @@ -264,27 +242,50 @@ class DydxFormatter @Inject constructor() { } } + /* + xxxxx.yyyyy + + Will take the number and round it to the closest step size + e.g. if number is 1021 and step size is "100" then output is "1000" + */ + fun decimalLocaleAgnostic(number: Double?, size: Double? = null): String? { + return raw(number = number, size = size, locale = Locale.US) + } + /* xxxxx.yyyyy + */ - fun decimalLocaleAgnostic(number: Double?, digits: Int): String? { + fun decimalLocaleAgnostic(number: Double?, digits: Int?): String? { return raw(number = number, digits = digits, locale = Locale.US) } + /* + xxxxxx,yyyyy or xxxxx.yyyyy + + Will take the number and round it to the closest step size + e.g. if number is 1021 and step size is "100" then output is "1000" + */ + fun raw(number: Double?, size: Double? = null, locale: Locale? = null): String? { + val digits = digits(size) + return raw(number = number, digits = digits, locale = locale ?: this.locale) + } + /* xxxxxx,yyyyy or xxxxx.yyyyy */ - fun raw(number: Double?, digits: Int? = null, locale: Locale = Locale.getDefault()): String? { + fun raw(number: Double?, digits: Int?, locale: Locale = Locale.getDefault()): String? { return number?.let { value -> if (value.isFinite()) { if (digits != null) { + val rounded = rounded(value, digits) val rawFormatter = DecimalFormat.getInstance(locale).apply { minimumFractionDigits = maxOf(digits, 0) maximumFractionDigits = maxOf(digits, 0) roundingMode = RoundingMode.HALF_UP isGroupingUsed = false } - val formatted = rawFormatter.format(number) + val formatted = rawFormatter.format(rounded) val number = rawFormatter.parse(formatted) if (number.toDouble() == 0.0) { // handle -0.0 rawFormatter.format(0.0) @@ -292,11 +293,53 @@ class DydxFormatter @Inject constructor() { formatted } } else { - BigDecimal(number).toPlainString() + number.toString() } } else { "∞" } } } + + private fun rounded(number: Double, digits: Int): Double { + if (digits >= 0) { + return number + } else { + val reversed = digits * -1 + val divideBy = Math.pow(10.0, reversed.toDouble() - 1) + return round(number / divideBy).toInt() * divideBy + } + } + + /* + Returns the number of digits for a given size specified in the format of 10^(-x) + e.g. + 0.001 -> 3, + 0.1 -> 1, + 1 -> 0, + 10 -> -1 + 1000 -> -3 + */ + private fun digits(size: Double?): Int? { + if (size == null || size <= 0.0) return null + + var size = size + if (size >= 1) { + var count = 0 + while (size >= 1) { + count++ + size /= 10 + } + return count * -1 + } else if (size <= 0.1) { + var count = 0 + while (size <= 0.1) { + count++ + size *= 10 + } + return count + } else { + return null + } + } } 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 4c81fcac..dccaf755 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 @@ -52,6 +52,7 @@ object TradeRoutes { const val margin_type = "trade/margin_type" const val target_leverage = "trade/target_leverage" const val trigger = "trade/take_profit_stop_loss" + const val adjust_margin = "trade/adjust_margin" } object TransferRoutes { diff --git a/v4/common/src/test/java/exchange/dydx/common/formatter/DydxFormatterTests.kt b/v4/common/src/test/java/exchange/dydx/common/formatter/DydxFormatterTests.kt index 5e8bc4da..5b943a89 100644 --- a/v4/common/src/test/java/exchange/dydx/common/formatter/DydxFormatterTests.kt +++ b/v4/common/src/test/java/exchange/dydx/common/formatter/DydxFormatterTests.kt @@ -4,6 +4,7 @@ import exchange.dydx.trading.common.formatter.DydxFormatter import org.junit.Assert import org.junit.Test import java.time.Instant +import java.time.ZoneId import java.util.Date import java.util.Locale @@ -97,6 +98,7 @@ class DydxFormatterTests { val number: Double, val digits: Int = 2, val expected: String, + val locale: Locale = Locale.US, ) val testCases = listOf( @@ -108,14 +110,50 @@ class DydxFormatterTests { TestCase(number = 0.0, digits = 0, expected = "$0"), TestCase(number = 0.0, digits = 1, expected = "$0.0"), TestCase(number = 0.6, digits = 0, expected = "$1"), + TestCase(number = 1.0, digits = 2, expected = "$1,00", locale = Locale.FRANCE), + TestCase(number = 0.5, digits = 2, expected = "$0,50", locale = Locale.FRANCE), + TestCase(number = -0.25, digits = 2, expected = "-$0,25", locale = Locale.FRANCE), + TestCase(number = -0.0002, digits = 2, expected = "$0,00", locale = Locale.FRANCE), + TestCase(number = 0.0, digits = 2, expected = "$0,00", locale = Locale.FRANCE), + TestCase(number = 0.0, digits = 0, expected = "$0", locale = Locale.FRANCE), + TestCase(number = 0.0, digits = 1, expected = "$0,0", locale = Locale.FRANCE), + TestCase(number = 0.6, digits = 0, expected = "$1", locale = Locale.FRANCE), ) testCases.forEach { testCase -> + formatter.locale = testCase.locale val formatted = formatter.dollar(number = testCase.number, digits = testCase.digits) assert(formatted == testCase.expected) { "Test case: $testCase, formatted: $formatted" } } } + @Test + fun testDollar_DoubleSize() { + val formatter = DydxFormatter() + + data class TestCase( + val number: Double, + val size: Double?, + val expected: String, + ) + + val testCases = listOf( + TestCase(number = 0.6, size = 1.0, expected = "$1"), + TestCase(number = 1.0, size = 0.01, expected = "$1.00"), + TestCase(number = 0.5, size = 0.01, expected = "$0.50"), + TestCase(number = -0.25, size = 0.01, expected = "-$0.25"), + TestCase(number = -0.0002, size = 0.01, expected = "$0.00"), + TestCase(number = 0.0, size = 0.01, expected = "$0.00"), + TestCase(number = 0.0, size = 1.0, expected = "$0"), + TestCase(number = 0.0, size = 0.1, expected = "$0.0"), + ) + + testCases.forEach { testCase -> + val formatted = formatter.dollar(number = testCase.number, size = testCase.size) + assert(formatted == testCase.expected) { "Test case: $testCase, formatted: $formatted" } + } + } + @Test fun testPercent() { val formatter = DydxFormatter() @@ -175,14 +213,14 @@ class DydxFormatterTests { @Test fun testDateTime() { val formatter = DydxFormatter() - val time = formatter.dateTime(Instant.parse("2021-10-01T00:00:00Z")) + val time = formatter.dateTime(Instant.parse("2021-10-01T00:00:00Z"), ZoneId.of("America/Los_Angeles")) Assert.assertEquals("2021-09-30 17:00:00", time) } @Test fun testClock() { val formatter = DydxFormatter() - val time = formatter.clock(Instant.parse("2021-10-01T00:00:00Z")) + val time = formatter.clock(Instant.parse("2021-10-01T00:00:00Z"), ZoneId.of("America/Los_Angeles")) Assert.assertEquals("17:00:00", time) } @@ -238,6 +276,39 @@ class DydxFormatterTests { } } + @Test + fun testDecimalLocaleAgnostic_DoubleSize() { + val formatter = DydxFormatter() + data class TestCase( + val number: Double, + val size: Double?, + val expected: String, + ) + + val testCases = listOf( + TestCase(number = 1.0, size = 0.01, expected = "1.00"), + TestCase(number = -0.001, size = 0.0, expected = "-0.001"), // invalid size + TestCase(number = -0.001, size = 0.001, expected = "-0.001"), + TestCase(number = -0.001, size = 0.01, expected = "0.00"), + TestCase(number = 0.001, size = 0.01, expected = "0.00"), + TestCase(number = -0.005, size = 0.01, expected = "-0.01"), + TestCase(number = -0.0051, size = 0.01, expected = "-0.01"), + TestCase(number = 1.6, size = 1.0, expected = "2"), + TestCase(number = 1123345.123, size = 0.01, expected = "1123345.12"), + TestCase(number = 1123345.126, size = 0.01, expected = "1123345.13"), + TestCase(number = 1123349.123, size = 10.0, expected = "1123350"), + TestCase(number = 1123341.123, size = 10.0, expected = "1123340"), + TestCase(number = -1123341.123, size = 10.0, expected = "-1123340"), + TestCase(number = 1123341.123, size = 100000000.0, expected = "0"), + TestCase(number = 1123341.123, size = null, expected = "1123341.123"), + ) + + testCases.forEach { testCase -> + val formatted = formatter.decimalLocaleAgnostic(number = testCase.number, size = testCase.size) + assert(formatted == testCase.expected) { "Test case: $testCase, formatted: $formatted" } + } + } + @Test fun testRaw() { val formatter = DydxFormatter() @@ -256,7 +327,7 @@ class DydxFormatterTests { TestCase(number = 0.001, digits = 2, expected = "0.00"), TestCase(number = -0.005, digits = 2, expected = "-0.01"), TestCase(number = -0.0051, digits = 2, expected = "-0.01"), - TestCase(number = 1.0, digits = null, expected = "1"), + TestCase(number = 1.0, digits = null, expected = "1.0"), TestCase(number = 1123345.123, digits = 2, expected = "1123345.12"), TestCase(number = 1123345.123, digits = 2, expected = "1123345,12", locale = Locale.FRANCE), ) @@ -266,4 +337,38 @@ class DydxFormatterTests { assert(formatted == testCase.expected) { "Test case: $testCase, formatted: $formatted" } } } + + @Test + fun testRaw_DoubleSize() { + val formatter = DydxFormatter() + + data class TestCase( + val number: Double, + val size: Double?, + val expected: String, + var locale: Locale = Locale.getDefault() + ) + + val testCases = listOf( + TestCase(number = 1.0, size = 0.01, expected = "1.00"), + TestCase(number = -0.001, size = 0.0, expected = "-0.001"), // invalid size + TestCase(number = -0.001, size = 0.001, expected = "-0.001"), + TestCase(number = -0.001, size = 0.01, expected = "0.00"), + TestCase(number = 0.001, size = 0.01, expected = "0.00"), + TestCase(number = -0.005, size = 0.01, expected = "-0.01"), + TestCase(number = -0.0051, size = 0.01, expected = "-0.01"), + TestCase(number = 1.0, size = null, expected = "1.0"), + TestCase(number = 1123345.123, size = 0.01, expected = "1123345.12"), + TestCase(number = 1123345.123, size = 0.01, expected = "1123345,12", locale = Locale.FRANCE), + TestCase(number = 1123349.123, size = 10.0, expected = "1123350", locale = Locale.FRANCE), + TestCase(number = 1123344.123, size = 10.0, expected = "1123340", locale = Locale.FRANCE), + TestCase(number = 1.0, size = null, expected = "1.0", locale = Locale.FRANCE), + TestCase(number = 1123341.123, size = 100000000.0, expected = "0", locale = Locale.FRANCE), + ) + + testCases.forEach { testCase -> + val formatted = formatter.raw(number = testCase.number, size = testCase.size, locale = testCase.locale) + assert(formatted == testCase.expected) { "Test case: $testCase, formatted: $formatted" } + } + } } diff --git a/v4/core/src/main/java/exchange/dydx/trading/TradingActivity.kt b/v4/core/src/main/java/exchange/dydx/trading/TradingActivity.kt index 07011767..272635e6 100644 --- a/v4/core/src/main/java/exchange/dydx/trading/TradingActivity.kt +++ b/v4/core/src/main/java/exchange/dydx/trading/TradingActivity.kt @@ -140,7 +140,7 @@ class TradingActivity : FragmentActivity() { ) { DydxNavGraph( appRouter = viewModel.router, - modifier = Modifier, + modifier = it, ) } } diff --git a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/DydxMarketInfoView.kt b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/DydxMarketInfoView.kt index a309921e..7695ab16 100644 --- a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/DydxMarketInfoView.kt +++ b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/DydxMarketInfoView.kt @@ -1,5 +1,8 @@ package exchange.dydx.trading.feature.market.marketinfo +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -155,11 +158,23 @@ object DydxMarketInfoView : DydxComponent { ) } item(key = "stats") { - when (state.statsTabSelection) { - DydxMarketStatsTabView.Selection.Statistics -> { - DydxMarketStatsView.Content(Modifier) - } - DydxMarketStatsTabView.Selection.About -> { + AnimatedVisibility( + visible = + state.statsTabSelection == DydxMarketStatsTabView.Selection.Statistics, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + DydxMarketStatsView.Content(Modifier) + } + AnimatedVisibility( + visible = + state.statsTabSelection == DydxMarketStatsTabView.Selection.About, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + Column( + modifier = Modifier, + ) { DydxMarketResourcesView.Content(Modifier) Spacer(modifier = Modifier.height(ThemeShapes.VerticalPadding)) DydxMarketConfigsView.Content(Modifier) diff --git a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/depth/DydxMarketDepthViewModel.kt b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/depth/DydxMarketDepthViewModel.kt index 1cce3105..ba7d5b1d 100644 --- a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/depth/DydxMarketDepthViewModel.kt +++ b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/depth/DydxMarketDepthViewModel.kt @@ -81,13 +81,8 @@ class DydxMarketDepthViewModel @Inject constructor( null, true, ), - interaction = InteractionConfig( - true, - false, - true, - true, - 500.0f, - this, + interaction = InteractionConfig.default.copy( + selectionListener = this, ), xAxis = AxisConfig( false, diff --git a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/funding/DydxMarketFundingRateViewModel.kt b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/funding/DydxMarketFundingRateViewModel.kt index be845a3d..9944939c 100644 --- a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/funding/DydxMarketFundingRateViewModel.kt +++ b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/funding/DydxMarketFundingRateViewModel.kt @@ -52,13 +52,8 @@ class DydxMarketFundingRateViewModel @Inject constructor( null, true, ), - interaction = InteractionConfig( - true, - false, - true, - true, - 500.0f, - this, + interaction = InteractionConfig.default.copy( + selectionListener = this, ), xAxis = AxisConfig( false, diff --git a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionButtonsView.kt b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionButtonsView.kt index d7451db0..c1fb2694 100644 --- a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionButtonsView.kt +++ b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionButtonsView.kt @@ -1,22 +1,44 @@ package exchange.dydx.trading.feature.market.marketinfo.components.position +import androidx.compose.foundation.clickable 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.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.platformui.components.buttons.PlatformButton import exchange.dydx.platformui.components.buttons.PlatformButtonState +import exchange.dydx.platformui.components.dividers.PlatformDivider +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.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.designSystem.theme.themeFont import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold +import exchange.dydx.utilities.utils.toDp @Preview @Composable @@ -30,14 +52,35 @@ fun Preview_DydxMarketPositionButtonsView() { } object DydxMarketPositionButtonsView : DydxComponent { + + data class TriggerViewState( + val label: String? = null, + val triggerPrice: String? = null, + val limitPrice: String? = null, + val sizePercent: String? = null, + ) { + companion object { + val preview = TriggerViewState( + label = "TP", + triggerPrice = "$120.0", + limitPrice = "$110.0", + sizePercent = "10%", + ) + } + } + data class ViewState( val localizer: LocalizerProtocol, val addTriggerAction: (() -> Unit)? = null, val closeAction: (() -> Unit)? = null, + val takeProfitTrigger: TriggerViewState? = null, + val stopLossTrigger: TriggerViewState? = null, ) { companion object { val preview = ViewState( localizer = MockLocalizer(), + takeProfitTrigger = TriggerViewState.preview, + stopLossTrigger = TriggerViewState.preview, ) } } @@ -61,25 +104,166 @@ object DydxMarketPositionButtonsView : DydxComponent { .padding(horizontal = ThemeShapes.HorizontalPadding), horizontalArrangement = Arrangement.SpaceBetween, ) { - PlatformButton( - text = state.localizer.localize("APP.TRADE.ADD_TP_SL"), - state = PlatformButtonState.Secondary, - modifier = Modifier - .padding(vertical = ThemeShapes.VerticalPadding) - .weight(1f), - action = state.addTriggerAction ?: {}, - ) + var size by remember { mutableStateOf(IntSize.Zero) } + + if (state.takeProfitTrigger == null && state.stopLossTrigger == null) { + PlatformButton( + text = state.localizer.localize("APP.TRADE.ADD_TP_SL"), + state = PlatformButtonState.Secondary, + modifier = Modifier + .padding(vertical = ThemeShapes.VerticalPadding) + .weight(1f), + action = state.addTriggerAction ?: {}, + ) - Spacer(modifier = Modifier.width(ThemeShapes.HorizontalPadding)) + Spacer(modifier = Modifier.width(ThemeShapes.HorizontalPadding)) - PlatformButton( - text = state.localizer.localize("APP.TRADE.CLOSE_POSITION"), - state = PlatformButtonState.Destructive, + PlatformButton( + text = state.localizer.localize("APP.TRADE.CLOSE_POSITION"), + state = PlatformButtonState.Destructive, + modifier = Modifier + .padding(vertical = ThemeShapes.VerticalPadding) + .weight(1f), + action = state.closeAction ?: {}, + ) + } else { + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(ThemeShapes.VerticalPadding), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(ThemeShapes.HorizontalPadding), + ) { + state.takeProfitTrigger?.let { + TriggerViewContent( + modifier = Modifier + .weight(1f) + .onSizeChanged { size = it }, + state = it, + localizer = state.localizer, + action = state.addTriggerAction ?: {}, + ) + } ?: PlatformButton( + text = state.localizer.localize("APP.TRADE.ADD_TP"), + state = PlatformButtonState.Secondary, + modifier = Modifier + .padding(vertical = ThemeShapes.VerticalPadding) + .weight(1f) + .height(size.height.toDp), + action = state.addTriggerAction ?: {}, + ) + + state.stopLossTrigger?.let { + TriggerViewContent( + modifier = Modifier + .weight(1f) + .onSizeChanged { size = it }, + state = it, + localizer = state.localizer, + action = state.addTriggerAction ?: {}, + ) + } ?: PlatformButton( + text = state.localizer.localize("APP.TRADE.ADD_SL"), + state = PlatformButtonState.Secondary, + modifier = Modifier + .padding(vertical = ThemeShapes.VerticalPadding) + .weight(1f) + .height(size.height.toDp), + action = state.addTriggerAction ?: {}, + ) + } + + PlatformButton( + text = state.localizer.localize("APP.TRADE.CLOSE_POSITION"), + state = PlatformButtonState.Destructive, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = ThemeShapes.VerticalPadding), + action = state.closeAction ?: {}, + ) + } + } + } + } + + @Composable + private fun TriggerViewContent( + modifier: Modifier, + state: TriggerViewState, + localizer: LocalizerProtocol, + action: () -> Unit = {} + ) { + InputFieldScaffold( + modifier = modifier, + ) { + Column( modifier = Modifier - .padding(vertical = ThemeShapes.VerticalPadding) - .weight(1f), - action = state.closeAction ?: {}, - ) + .clickable { action() } + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(ThemeShapes.VerticalPadding), + ) { + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.label ?: "", + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.tiny) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + modifier = Modifier, + text = state.triggerPrice ?: "", + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.medium) + .themeColor(ThemeColor.SemanticColor.text_primary), + ) + } + + if (state.limitPrice != null || state.sizePercent != null) { + PlatformDivider() + + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = localizer.localize("APP.TRADE.LIMIT_ORDER_SHORT"), + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.tiny) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + maxLines = 1, + modifier = Modifier.weight(1f), + ) + + if (state.limitPrice != null) { + Text( + text = state.limitPrice, + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.mini) + .themeColor(ThemeColor.SemanticColor.text_secondary), + ) + } + + if (state.sizePercent != null) { + Text( + text = state.sizePercent, + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.mini) + .themeColor(ThemeColor.SemanticColor.text_secondary), + ) + } + } + } + } } } } diff --git a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionButtonsViewModel.kt b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionButtonsViewModel.kt index 3414c15c..0e0dee50 100644 --- a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionButtonsViewModel.kt +++ b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionButtonsViewModel.kt @@ -2,16 +2,23 @@ package exchange.dydx.trading.feature.market.marketinfo.components.position import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.SubaccountOrder +import exchange.dydx.abacus.output.SubaccountPosition import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.dydxstatemanager.MarketConfigsAndAsset +import exchange.dydx.dydxstatemanager.stopLossOrders +import exchange.dydx.dydxstatemanager.takeProfitOrders 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.TradeRoutes -import exchange.dydx.trading.feature.market.marketinfo.streams.MarketAndAsset import exchange.dydx.trading.feature.market.marketinfo.streams.MarketInfoStreaming import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapNotNull import javax.inject.Inject @HiltViewModel @@ -19,32 +26,84 @@ class DydxMarketPositionButtonsViewModel @Inject constructor( private val localizer: LocalizerProtocol, marketInfoStream: MarketInfoStreaming, private val router: DydxRouter, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, ) : ViewModel(), DydxViewModel { + private val marketIdFlow = marketInfoStream.marketAndAsset + .mapNotNull { it?.market?.id } + val state: Flow = - marketInfoStream.marketAndAsset.filterNotNull() - .map { - createViewState(it) - } + combine( + marketIdFlow, + marketIdFlow.flatMapLatest { abacusStateManager.state.selectedSubaccountPositionOfMarket(it) }, + marketIdFlow.flatMapLatest { abacusStateManager.state.takeProfitOrders(it) }, + marketIdFlow.flatMapLatest { abacusStateManager.state.stopLossOrders(it) }, + abacusStateManager.state.configsAndAssetMap, + ) { marketId, position, takeProfitOrders, stopLossOrders, configsAndAssetMap -> + createViewState(marketId, position, takeProfitOrders, stopLossOrders, configsAndAssetMap?.get(marketId)) + } .distinctUntilChanged() private fun createViewState( - marketAndAsset: MarketAndAsset, + marketId: String, + position: SubaccountPosition?, + takeProfitOrders: List?, + stopLossOrders: List?, + configsAndAsset: MarketConfigsAndAsset?, ): DydxMarketPositionButtonsView.ViewState { return DydxMarketPositionButtonsView.ViewState( localizer = localizer, addTriggerAction = { router.navigateTo( - route = TradeRoutes.trigger + "/${marketAndAsset.market.id}", + route = TradeRoutes.trigger + "/$marketId", presentation = DydxRouter.Presentation.Modal, ) }, closeAction = { router.navigateTo( - route = TradeRoutes.close_position + "/${marketAndAsset.market.id}", + route = TradeRoutes.close_position + "/$marketId", presentation = DydxRouter.Presentation.Modal, ) }, + takeProfitTrigger = takeProfitOrders?.firstOrNull()?.let { + createTriggerViewState( + label = "TP", + position = position, + order = it, + configsAndAsset = configsAndAsset, + ) + }, + stopLossTrigger = stopLossOrders?.firstOrNull()?.let { + createTriggerViewState( + label = "SL", + position = position, + order = it, + configsAndAsset = configsAndAsset, + ) + }, + ) + } + + private fun createTriggerViewState( + label: String, + position: SubaccountPosition?, + order: SubaccountOrder, + configsAndAsset: MarketConfigsAndAsset?, + ): DydxMarketPositionButtonsView.TriggerViewState { + val tickSize = configsAndAsset?.configs?.displayTickSizeDecimals ?: 0 + val size = order.size + val positionSize = position?.size?.current ?: 0.0 + val percentage = if (positionSize > 0.0) { + size / positionSize + } else { + 0.0 + } + return DydxMarketPositionButtonsView.TriggerViewState( + label = label, + triggerPrice = order.triggerPrice?.let { formatter.dollar(it, tickSize) }, + limitPrice = order.price.let { formatter.dollar(it, tickSize) }, + sizePercent = formatter.percent(percentage, 2), ) } } diff --git a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionView.kt b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionView.kt index ba83be14..b16392af 100644 --- a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionView.kt +++ b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionView.kt @@ -65,6 +65,7 @@ object DydxMarketPositionView : DydxComponent { shareAction = {}, closeAction = {}, sharedMarketPositionViewState = SharedMarketPositionViewState.preview, + enableTrigger = true, ) } } diff --git a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionViewModel.kt b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionViewModel.kt index 242812e2..d28a89a5 100644 --- a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionViewModel.kt +++ b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/position/DydxMarketPositionViewModel.kt @@ -56,6 +56,12 @@ class DydxMarketPositionViewModel @Inject constructor( asset = marketAndAsset.asset, formatter = formatter, localizer = localizer, + onAdjustMarginAction = { + router.navigateTo( + route = TradeRoutes.adjust_margin + "/${marketAndAsset.market.id}", + presentation = DydxRouter.Presentation.Modal, + ) + }, ), enableTrigger = featureFlags.isFeatureEnabled(DydxFeatureFlag.enable_sl_tp_trigger), ) diff --git a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/prices/DydxMarketPricesViewModel.kt b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/prices/DydxMarketPricesViewModel.kt index 3dd8c678..ad4f4384 100644 --- a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/prices/DydxMarketPricesViewModel.kt +++ b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketinfo/components/prices/DydxMarketPricesViewModel.kt @@ -296,13 +296,8 @@ class DydxMarketPricesViewModel @Inject constructor( null, true, ), - interaction = InteractionConfig( - true, - false, - true, - true, - 500.0f, - this, + interaction = InteractionConfig.default.copy( + selectionListener = this, ), xAxis = AxisConfig( true, diff --git a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketlist/components/DydxMarketSparklineView.kt b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketlist/components/DydxMarketSparklineView.kt index 004deaf3..e1f0967b 100644 --- a/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketlist/components/DydxMarketSparklineView.kt +++ b/v4/feature/market/src/main/java/exchange/dydx/trading/feature/market/marketlist/components/DydxMarketSparklineView.kt @@ -66,14 +66,7 @@ object DydxMarketSparklineView : DydxComponent { 0.0f, true, ), - interaction = InteractionConfig( - false, - false, - false, - false, - 500.0f, - - ), + interaction = InteractionConfig.noTouch, xAxis = AxisConfig(false, false), leftAxis = AxisConfig(false, false), rightAxis = null, @@ -99,12 +92,12 @@ object DydxMarketSparklineView : DydxComponent { } } regularView.update( - state.sharedMarketViewState?.sparkline ?: LineChartDataSet( + set = state.sharedMarketViewState?.sparkline ?: LineChartDataSet( listOf(), "Sparkline", ), - config, - state.sharedMarketViewState?.priceChangePercent24H?.sign?.let { + config = config, + lineColor = state.sharedMarketViewState?.priceChangePercent24H?.sign?.let { when (it) { PlatformUISign.Plus -> ThemeColor.SemanticColor.positiveColor.color.toArgb() PlatformUISign.Minus -> ThemeColor.SemanticColor.negativeColor.color.toArgb() diff --git a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/connect/DydxOnboardConnectView.kt b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/connect/DydxOnboardConnectView.kt index e2555992..87c03596 100644 --- a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/connect/DydxOnboardConnectView.kt +++ b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/connect/DydxOnboardConnectView.kt @@ -78,7 +78,7 @@ object DydxOnboardConnectView : DydxComponent { modifier = modifier, platformInfo = viewModel.platformInfo, ) { - Content(modifier, state) + Content(it, state) } } diff --git a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanView.kt b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanView.kt index 547fea25..b88cbcb0 100644 --- a/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanView.kt +++ b/v4/feature/onboarding/src/main/java/exchange/dydx/feature/onboarding/desktopscan/DydxDesktopScanView.kt @@ -52,7 +52,7 @@ object DydxDesktopScanView : DydxComponent { modifier = modifier, platformInfo = viewModel.platformInfo, ) { - Content(modifier, state) + Content(it, state) } PlatformDialogScaffold(dialog = viewModel.platformDialog) diff --git a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/overview/DydxPortfolioChartViewModel.kt b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/overview/DydxPortfolioChartViewModel.kt index 67bb8019..c0e7d502 100644 --- a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/overview/DydxPortfolioChartViewModel.kt +++ b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/overview/DydxPortfolioChartViewModel.kt @@ -53,13 +53,8 @@ class DydxPortfolioChartViewModel @Inject constructor( true, null, ), - interaction = InteractionConfig( - true, - false, - true, - true, - 500.0f, - this, + interaction = InteractionConfig.default.copy( + selectionListener = this, ), xAxis = AxisConfig(false, false, null), leftAxis = AxisConfig(false, false, null), diff --git a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionItemView.kt b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionItemView.kt index 64db05e8..cb9235f0 100644 --- a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionItemView.kt +++ b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionItemView.kt @@ -60,6 +60,7 @@ object DydxPortfolioPositionItemView { position: SharedMarketPositionViewState, isIsolatedMarketEnabled: Boolean, onTapAction: (SharedMarketPositionViewState) -> Unit = {}, + onModifyMarginAction: (SharedMarketPositionViewState) -> Unit = {}, ) { val shape = RoundedCornerShape(10.dp) Row( @@ -360,9 +361,7 @@ object DydxPortfolioPositionItemView { .width(32.dp) .height(32.dp), action = { - /* - TODO: Implement edit button action - */ + position.onAdjustMarginAction?.invoke() }, padding = 0.dp, shape = RoundedCornerShape(4.dp), diff --git a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionsView.kt b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionsView.kt index f7cb2233..4c929b79 100644 --- a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionsView.kt +++ b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionsView.kt @@ -47,6 +47,7 @@ object DydxPortfolioPositionsView : DydxComponent { val positions: List = listOf(), val isIsolatedMarketEnabled: Boolean, val onPositionTapAction: (SharedMarketPositionViewState) -> Unit = {}, + val onModifyMarginAction: (SharedMarketPositionViewState) -> Unit = {}, ) { companion object { val preview = ViewState( @@ -96,6 +97,7 @@ object DydxPortfolioPositionsView : DydxComponent { position = position, isIsolatedMarketEnabled = state.isIsolatedMarketEnabled, onTapAction = state.onPositionTapAction, + onModifyMarginAction = state.onModifyMarginAction, ) // Spacer(modifier = Modifier.height(10.dp)) diff --git a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionsViewModel.kt b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionsViewModel.kt index 9816bab9..93cc6441 100644 --- a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionsViewModel.kt +++ b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/components/positions/DydxPortfolioPositionsViewModel.kt @@ -13,6 +13,7 @@ import exchange.dydx.trading.common.featureflags.DydxFeatureFlags import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.MarketRoutes +import exchange.dydx.trading.common.navigation.TradeRoutes import exchange.dydx.trading.feature.shared.viewstate.SharedMarketPositionViewState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -58,6 +59,12 @@ class DydxPortfolioPositionsViewModel @Inject constructor( asset = assetMap?.get(position.assetId), formatter = formatter, localizer = localizer, + onAdjustMarginAction = { + router.navigateTo( + route = TradeRoutes.adjust_margin + "/${market.id}", + presentation = DydxRouter.Presentation.Modal, + ) + }, ) } ?: listOf(), isIsolatedMarketEnabled = isIsolatedMarketEnabled, @@ -68,6 +75,13 @@ class DydxPortfolioPositionsViewModel @Inject constructor( presentation = DydxRouter.Presentation.Push, ) }, + onModifyMarginAction = { position -> + val market = marketMap?.get(position.id) ?: return@ViewState + router.navigateTo( + route = TradeRoutes.adjust_margin + "/${market.id}", + presentation = DydxRouter.Presentation.Push, + ) + }, ) } } diff --git a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/orderdetails/DydxOrderDetailsView.kt b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/orderdetails/DydxOrderDetailsView.kt index d56494d4..67c4a87b 100644 --- a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/orderdetails/DydxOrderDetailsView.kt +++ b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/orderdetails/DydxOrderDetailsView.kt @@ -104,7 +104,7 @@ object DydxOrderDetailsView : DydxComponent { modifier = modifier, platformInfo = viewModel.platformInfo, ) { - Content(modifier, state) + Content(it, state) } } diff --git a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/orderdetails/DydxOrderDetailsViewModel.kt b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/orderdetails/DydxOrderDetailsViewModel.kt index fe1e0789..3e96ac54 100644 --- a/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/orderdetails/DydxOrderDetailsViewModel.kt +++ b/v4/feature/portfolio/src/main/java/exchange/dydx/trading/feature/portfolio/orderdetails/DydxOrderDetailsViewModel.kt @@ -52,11 +52,11 @@ class DydxOrderDetailsViewModel @Inject constructor( abacusStateManager.state.marketMap, abacusStateManager.state.assetMap, ) { fills, orders, marketMap, assetMap -> - if (fills == null || orders == null || marketMap == null || assetMap == null) { + if (marketMap == null || assetMap == null) { return@combine null } - val fill = fills.firstOrNull { it.id == orderOrFillId } - val order = orders.firstOrNull { it.id == orderOrFillId } + val fill = fills?.firstOrNull { it.id == orderOrFillId } + val order = orders?.firstOrNull { it.id == orderOrFillId } if (fill != null) { createFillViewState(fill, marketMap, assetMap) } else if (order != null) { diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/keyexport/DydxKeyExportView.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/keyexport/DydxKeyExportView.kt index dc3751ca..ea659666 100644 --- a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/keyexport/DydxKeyExportView.kt +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/keyexport/DydxKeyExportView.kt @@ -84,7 +84,7 @@ object DydxKeyExportView : DydxComponent { modifier = modifier, platformInfo = viewModel.platformInfo, ) { - Content(Modifier, state) + Content(it, state) } } diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/validation/DydxValidationView.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/validation/DydxValidationView.kt index 5edc80b2..a82f1898 100644 --- a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/validation/DydxValidationView.kt +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/validation/DydxValidationView.kt @@ -34,6 +34,7 @@ import exchange.dydx.platformui.designSystem.theme.themeColor import exchange.dydx.platformui.designSystem.theme.themeFont import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle +import exchange.dydx.trading.common.navigation.DydxAnimation import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer import exchange.dydx.utilities.utils.toDp @@ -66,6 +67,7 @@ object DydxValidationView : DydxComponent { data class ViewState( val localizer: LocalizerProtocol, val state: State = State.None, + val title: String? = null, val message: String? = null, val link: Link? = null, ) { @@ -73,6 +75,7 @@ object DydxValidationView : DydxComponent { val preview = ViewState( localizer = MockLocalizer(), state = State.Warning, + title = "This is a warning", message = "This is an error message", link = Link.preview, ) @@ -93,12 +96,13 @@ object DydxValidationView : DydxComponent { return } - when (state.state) { - State.Error, State.Warning -> { - ContentInBox(modifier, state) - } - State.None -> { - } + DydxAnimation.AnimateExpandInOut( + visible = when (state.state) { + State.Error, State.Warning -> true + State.None -> false + }, + ) { + ContentInBox(modifier, state) } } @@ -141,6 +145,15 @@ object DydxValidationView : DydxComponent { .padding(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { + if (viewState.title != null) { + Text( + modifier = Modifier, + text = viewState.title, + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_primary) + .themeFont(fontSize = ThemeFont.FontSize.small), + ) + } if (viewState.message != null) { Text( modifier = Modifier, diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/validation/DydxValidationViewModel.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/validation/DydxValidationViewModel.kt index b9cba2a7..71a56d40 100644 --- a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/validation/DydxValidationViewModel.kt +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/validation/DydxValidationViewModel.kt @@ -46,6 +46,12 @@ class DydxValidationViewModel @Inject constructor( transferError?.isNotEmpty() == true -> DydxValidationView.State.Error else -> DydxValidationView.State.None }, + title = when { + firstBlockingError != null -> firstBlockingError.resources.title?.localizedString(localizer) + firstWarning != null -> firstWarning.resources.title?.localizedString(localizer) + transferError?.isNotEmpty() == true -> null + else -> null + }, message = when { firstBlockingError != null -> firstBlockingError.resources.text?.localizedString(localizer) firstWarning != null -> firstWarning.resources.text?.localizedString(localizer) diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/scarfolds/InputFieldScarfold.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/scaffolds/InputFieldScaffold.kt similarity index 57% rename from v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/scarfolds/InputFieldScarfold.kt rename to v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/scaffolds/InputFieldScaffold.kt index a367ef79..9b079b9f 100644 --- a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/scarfolds/InputFieldScarfold.kt +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/scaffolds/InputFieldScaffold.kt @@ -1,4 +1,4 @@ -package exchange.dydx.trading.feature.shared.scarfolds +package exchange.dydx.trading.feature.shared.scaffolds import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -7,21 +7,27 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import exchange.dydx.platformui.components.inputs.PlatformInputAlertState import exchange.dydx.platformui.designSystem.theme.ThemeColor import exchange.dydx.platformui.designSystem.theme.color @Composable -fun InputFieldScarfold( +fun InputFieldScaffold( modifier: Modifier = Modifier, + alertState: PlatformInputAlertState = PlatformInputAlertState.None, content: @Composable () -> Unit, ) { + val shape = RoundedCornerShape(8.dp) Box( modifier = modifier - .background(color = ThemeColor.SemanticColor.layer_4.color, shape = RoundedCornerShape(8.dp)) + .background( + color = ThemeColor.SemanticColor.layer_4.color, + shape = shape, + ) .border( width = 1.dp, - color = ThemeColor.SemanticColor.layer_6.color, - shape = RoundedCornerShape(8.dp), + color = alertState.borderColor.color, + shape = shape, ), ) { content() diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/LabeledTextInput.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/LabeledTextInput.kt index 681f98c9..3567c991 100644 --- a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/LabeledTextInput.kt +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/LabeledTextInput.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.inputs.PlatformInputAlertState import exchange.dydx.platformui.components.inputs.PlatformTextInput import exchange.dydx.platformui.designSystem.theme.ThemeColor import exchange.dydx.platformui.designSystem.theme.ThemeFont @@ -40,6 +41,7 @@ object LabeledTextInput { val value: String? = null, val placeholder: String? = null, val onValueChanged: (String) -> Unit = {}, + val alertState: PlatformInputAlertState = PlatformInputAlertState.None, ) { companion object { val preview = ViewState( @@ -90,6 +92,7 @@ object LabeledTextInput { } }, value = state.value, + alertState = state.alertState, placeHolder = state.placeholder, onValueChange = state.onValueChanged, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/viewstate/SharedMarketPositionViewState.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/viewstate/SharedMarketPositionViewState.kt index e10e3efc..13e2b26d 100644 --- a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/viewstate/SharedMarketPositionViewState.kt +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/viewstate/SharedMarketPositionViewState.kt @@ -33,6 +33,7 @@ data class SharedMarketPositionViewState( val entryPrice: String? = null, val exitPrice: String? = null, val funding: SignedAmountView.ViewState? = null, + val onAdjustMarginAction: (() -> Unit)? = null, ) { companion object { val preview = SharedMarketPositionViewState( @@ -61,6 +62,7 @@ data class SharedMarketPositionViewState( asset: Asset?, formatter: DydxFormatter, localizer: LocalizerProtocol, + onAdjustMarginAction: (() -> Unit) ): SharedMarketPositionViewState? { val configs = market.configs ?: return null val positionSize = position.size?.current ?: 0.0 @@ -137,6 +139,7 @@ data class SharedMarketPositionViewState( sign = netFundingSign, coloringOption = SignedAmountView.ColoringOption.SignOnly, ), + onAdjustMarginAction = onAdjustMarginAction, ) } } diff --git a/v4/feature/trade/build.gradle b/v4/feature/trade/build.gradle index 5fb75469..6d416965 100644 --- a/v4/feature/trade/build.gradle +++ b/v4/feature/trade/build.gradle @@ -110,4 +110,6 @@ dependencies { androidTestImplementation "androidx.compose.ui:ui-test-junit4:$composeVersion" implementation("tz.co.asoft:kollections-interoperable:$kollectionsVersion") + + implementation("io.github.hoc081098:FlowExt:$flowExtVersion") } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/DydxTradeRouter.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/DydxTradeRouter.kt index 48216333..126f17cc 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/DydxTradeRouter.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/DydxTradeRouter.kt @@ -9,6 +9,7 @@ import exchange.dydx.trading.common.navigation.MarketRoutes import exchange.dydx.trading.common.navigation.TradeRoutes import exchange.dydx.trading.common.navigation.dydxComposable import exchange.dydx.trading.feature.trade.closeposition.DydxClosePositionInputView +import exchange.dydx.trading.feature.trade.margin.DydxAdjustMarginInputView import exchange.dydx.trading.feature.trade.tradeinput.DydxTradeInputMarginModeView import exchange.dydx.trading.feature.trade.tradeinput.DydxTradeInputTargetLeverageView import exchange.dydx.trading.feature.trade.tradestatus.DydxTradeStatusView @@ -22,8 +23,9 @@ fun NavGraphBuilder.tradeGraph( ) { dydxComposable( router = appRouter, - route = TradeRoutes.status, - deepLinks = appRouter.deeplinks(TradeRoutes.status), + route = TradeRoutes.status + "/{tradeType}", + arguments = listOf(navArgument("tradeType") { type = NavType.StringType }), + deepLinks = appRouter.deeplinksWithParam(TradeRoutes.status, "tradeType", true), ) { navBackStackEntry -> DydxTradeStatusView.Content(Modifier) } @@ -61,7 +63,7 @@ fun NavGraphBuilder.tradeGraph( dydxComposable( router = appRouter, route = TradeRoutes.margin_type, - deepLinks = appRouter.deeplinks(TradeRoutes.status), + deepLinks = appRouter.deeplinks(TradeRoutes.margin_type), ) { navBackStackEntry -> DydxTradeInputMarginModeView.Content(Modifier) } @@ -69,8 +71,23 @@ fun NavGraphBuilder.tradeGraph( dydxComposable( router = appRouter, route = TradeRoutes.target_leverage, - deepLinks = appRouter.deeplinks(TradeRoutes.status), + deepLinks = appRouter.deeplinks(TradeRoutes.target_leverage), ) { navBackStackEntry -> DydxTradeInputTargetLeverageView.Content(Modifier) } + + dydxComposable( + router = appRouter, + route = TradeRoutes.adjust_margin + "/{marketId}", + arguments = listOf(navArgument("marketId") { type = NavType.StringType }), + deepLinks = appRouter.deeplinksWithParam(TradeRoutes.adjust_margin, "marketId", true), + ) { navBackStackEntry -> + val id = navBackStackEntry.arguments?.getString("marketId") + if (id == null) { + Timber.w("No marketId passed") + appRouter.navigateTo(MarketRoutes.marketList) + return@dydxComposable + } + DydxAdjustMarginInputView.Content(Modifier) + } } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/ModelExt.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/ModelExt.kt new file mode 100644 index 00000000..e19e11ba --- /dev/null +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/ModelExt.kt @@ -0,0 +1,12 @@ +package exchange.dydx.trading.feature.trade + +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.ValidationError +import exchange.dydx.platformui.components.inputs.PlatformInputAlertState + +val ValidationError.alertState: PlatformInputAlertState + get() = when (type) { + ErrorType.error -> PlatformInputAlertState.Error + ErrorType.warning -> PlatformInputAlertState.Warning + else -> PlatformInputAlertState.None + } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/closeposition/components/DydxClosePositionInputCtaButtonViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/closeposition/components/DydxClosePositionInputCtaButtonViewModel.kt index ffc62345..6abb0837 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/closeposition/components/DydxClosePositionInputCtaButtonViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/closeposition/components/DydxClosePositionInputCtaButtonViewModel.kt @@ -60,7 +60,7 @@ class DydxClosePositionInputCtaButtonViewModel @Inject constructor( tradeStream.closePosition() router.navigateBack() router.navigateTo( - route = TradeRoutes.status, + route = TradeRoutes.status + "/closePosition", presentation = DydxRouter.Presentation.Modal, ) }, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/closeposition/components/DydxClosePositionInputSizeView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/closeposition/components/DydxClosePositionInputSizeView.kt index 6790fdfa..b0c514e0 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/closeposition/components/DydxClosePositionInputSizeView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/closeposition/components/DydxClosePositionInputSizeView.kt @@ -2,7 +2,6 @@ package exchange.dydx.trading.feature.trade.closeposition.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -13,7 +12,7 @@ import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledTextInput @Preview @@ -59,7 +58,7 @@ object DydxClosePositionInputSizeView : DydxComponent { return } - InputFieldScarfold(modifier) { + InputFieldScaffold(modifier) { Column { Row( verticalAlignment = Alignment.CenterVertically, @@ -68,7 +67,7 @@ object DydxClosePositionInputSizeView : DydxComponent { modifier = Modifier.weight(1f), state = LabeledTextInput.ViewState( localizer = state.localizer, - label = state?.localizer?.localize("APP.GENERAL.AMOUNT"), + label = state.localizer?.localize("APP.GENERAL.AMOUNT"), token = state.token, value = state.size, placeholder = state.placeholder ?: "", diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/di/TradeModule.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/di/TradeModule.kt index 28114f68..8d2bf1e0 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/di/TradeModule.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/di/TradeModule.kt @@ -7,8 +7,11 @@ import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent import dagger.hilt.android.scopes.ActivityRetainedScoped import exchange.dydx.trading.feature.trade.streams.MutableTradeStreaming +import exchange.dydx.trading.feature.trade.streams.MutableTriggerOrderStreaming import exchange.dydx.trading.feature.trade.streams.TradeStream import exchange.dydx.trading.feature.trade.streams.TradeStreaming +import exchange.dydx.trading.feature.trade.streams.TriggerOrderStream +import exchange.dydx.trading.feature.trade.streams.TriggerOrderStreaming import exchange.dydx.trading.feature.trade.tradeinput.DydxTradeInputView import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +39,16 @@ interface TradeModule { tradeStream: TradeStream, ): MutableTradeStreaming + @Binds + fun bindTriggerOrderStream( + mutableStream: MutableTriggerOrderStreaming, + ): TriggerOrderStreaming + + @Binds + fun bindMutableTriggerOrderStream( + triggerOrderStream: TriggerOrderStream, + ): MutableTriggerOrderStreaming + companion object { @Provides @ActivityRetainedScoped diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputView.kt new file mode 100644 index 00000000..b28b5f25 --- /dev/null +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputView.kt @@ -0,0 +1,523 @@ +package exchange.dydx.trading.feature.trade.margin + +import androidx.compose.animation.animateContentSize +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.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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.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 androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.PlatformInfoScaffold +import exchange.dydx.platformui.components.buttons.PlatformPillItem +import exchange.dydx.platformui.components.changes.PlatformAmountChange +import exchange.dydx.platformui.components.dividers.PlatformDivider +import exchange.dydx.platformui.components.inputs.PlatformTextInput +import exchange.dydx.platformui.components.tabgroups.PlatformTabGroup +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.trading.common.component.DydxComponent +import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface +import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold +import exchange.dydx.trading.feature.shared.views.HeaderViewCloseBotton +import exchange.dydx.trading.feature.shared.views.SizeTextView + +@Preview +@Composable +fun Preview_DydxAdjustMarginInputView() { + DydxThemedPreviewSurface { + DydxAdjustMarginInputView.Content(Modifier, DydxAdjustMarginInputView.ViewState.preview) + } +} + +object DydxAdjustMarginInputView : DydxComponent { + enum class MarginDirection { + Add, + Remove, + } + + data class PercentageOption( + val text: String, + val percentage: Double, + ) + + data class SubaccountReceipt( + val freeCollateral: List, + val marginUsage: List, + ) + + data class PositionReceipt( + val freeCollateral: List, + val leverage: List, + val liquidationPrice: List, + ) + + data class ViewState( + val localizer: LocalizerProtocol, + val formatter: DydxFormatter, + val direction: MarginDirection = MarginDirection.Add, + val percentage: Double?, + val percentageOptions: List, + val amountText: String?, + val subaccountReceipt: SubaccountReceipt, + val positionReceipt: PositionReceipt, + val error: String?, + val marginDirectionAction: ((direction: MarginDirection) -> Unit) = {}, + val percentageAction: (() -> Unit) = {}, + val editAction: ((String) -> Unit) = {}, + val action: (() -> Unit) = {}, + val closeAction: (() -> Unit) = {}, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + formatter = DydxFormatter(), + direction = MarginDirection.Add, + percentage = 0.5, + percentageOptions = listOf( + PercentageOption("10%", 0.1), + PercentageOption("20%", 0.2), + PercentageOption("30%", 0.3), + PercentageOption("50%", 0.5), + ), + amountText = "500", + subaccountReceipt = SubaccountReceipt( + freeCollateral = listOf("1000.00", "500.00"), + marginUsage = listOf("19.34", "38.45"), + ), + positionReceipt = PositionReceipt( + freeCollateral = listOf("1000.00", "1500.00"), + leverage = listOf("3.1", "2.4"), + liquidationPrice = listOf("1200.00", "1000.00"), + ), + error = null, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxAdjustMarginInputViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + PlatformInfoScaffold(modifier = modifier, platformInfo = viewModel.platformInfo) { + Content(modifier, state) + } + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + + Column( + modifier = modifier + .animateContentSize() + .fillMaxSize() + .themeColor(ThemeColor.SemanticColor.layer_4) + .padding(horizontal = 16.dp), + ) { + NavigationHeader( + modifier = Modifier, + state = state, + ) + PlatformDivider() + Spacer(modifier = Modifier.height(16.dp)) + MarginDirection( + modifier = Modifier, + state = state, + ) + Spacer(modifier = Modifier.height(16.dp)) + PercentageOptions( + modifier = Modifier, + state = state, + ) + Spacer(modifier = Modifier.height(16.dp)) + InputAndSubaccountReceipt( + modifier = Modifier, + state = state, + ) + Spacer(modifier = Modifier.weight(1f)) +// if (state.error == null) { +// LiquidationPrice( +// modifier = Modifier, +// state = state, +// ) +// } else { +// Error( +// modifier = Modifier, +// error = state.error, +// ) +// } +// Spacer(modifier = Modifier.height(8.dp)) +// PositionReceiptAndButton( +// modifier = Modifier, +// state = state, +// ) + } + } + + @Composable + fun NavigationHeader( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + Spacer(modifier = Modifier.width(16.dp)) + Text( + modifier = Modifier.padding(horizontal = 0.dp), + style = TextStyle.dydxDefault + .themeFont( + fontSize = ThemeFont.FontSize.large, + fontType = ThemeFont.FontType.plus, + ) + .themeColor(ThemeColor.SemanticColor.text_primary), + text = state.localizer.localize("APP.TRADE.ADJUST_ISOLATED_MARGIN"), + ) + Spacer(modifier = Modifier.weight(1f)) + HeaderViewCloseBotton( + closeAction = state.closeAction, + ) + } + } + + private fun marginDirectionText(direction: MarginDirection, localizer: LocalizerProtocol): String { + return when (direction) { + MarginDirection.Add -> localizer.localize("APP.TRADE.ADD_MARGIN") + MarginDirection.Remove -> localizer.localize("APP.TRADE.REMOVE_MARGIN") + } + } + + @Composable + fun MarginDirection( + modifier: Modifier, + state: ViewState, + ) { + val directions = listOf(MarginDirection.Add, MarginDirection.Remove) + + PlatformTabGroup( + modifier = modifier + .fillMaxWidth() + .height(42.dp), + scrollingEnabled = false, + items = directions.map { + { modifier -> + PlatformPillItem( + modifier = Modifier + .padding( + vertical = 4.dp, + horizontal = 8.dp, + ), + backgroundColor = ThemeColor.SemanticColor.layer_5, + ) { + Text( + text = marginDirectionText(it, state.localizer), + modifier = Modifier, + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.small), + + ) + } + } + }, + selectedItems = directions.map { + { modifier -> + PlatformPillItem( + modifier = Modifier + .padding( + vertical = 4.dp, + horizontal = 8.dp, + ), + backgroundColor = ThemeColor.SemanticColor.layer_2, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = marginDirectionText(it, state.localizer), + modifier = Modifier, + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_primary) + .themeFont(fontSize = ThemeFont.FontSize.small), + ) + } + } + } + }, + currentSelection = if (state.direction == MarginDirection.Add) 0 else 1, + onSelectionChanged = { it -> + state.marginDirectionAction.invoke(directions[it]) + }, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) + } + + @Composable + fun PercentageOptions( + modifier: Modifier, + state: ViewState, + ) { + PlatformTabGroup( + modifier = Modifier.fillMaxWidth(), + scrollingEnabled = false, + items = state.percentageOptions.map { + { modifier -> + PlatformPillItem( + modifier = Modifier + .padding( + vertical = 4.dp, + horizontal = 8.dp, + ), + backgroundColor = ThemeColor.SemanticColor.layer_5, + ) { + Text( + text = it.text, + modifier = Modifier, + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.small), + + ) + } + } + } ?: listOf(), + selectedItems = state.percentageOptions.map { + { modifier -> + PlatformPillItem( + modifier = Modifier + .padding( + vertical = 4.dp, + horizontal = 8.dp, + ), + backgroundColor = ThemeColor.SemanticColor.layer_2, + ) { + Text( + text = it.text, + modifier = Modifier, + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_primary) + .themeFont(fontSize = ThemeFont.FontSize.small), + + ) + } + } + } ?: listOf(), + equalWeight = false, + currentSelection = state.percentageOptions.indexOfFirst { + it.percentage == state.percentage + }, + onSelectionChanged = {}, + ) + } + + @Composable + fun InputAndSubaccountReceipt( + modifier: Modifier, + state: ViewState, + ) { + Column { + InputFieldScaffold(modifier.zIndex(1f)) { + AmountBox(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), + ) { + CrossFreeCollateralContent(modifier = Modifier, state) + CrossMarginContent(modifier = Modifier, state) + } + } + } + + @Composable + private fun AmountBox( + 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.localizer.localize("APP.GENERAL.AMOUNT"), + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.mini), + ) + + PlatformTextInput( + modifier = Modifier.fillMaxWidth(), + value = state.amountText ?: "", + textStyle = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_primary) + .themeFont(fontSize = ThemeFont.FontSize.medium), + placeHolder = if (state.amountText == null) { + state.formatter.raw(0.0, 2) + } else { + null + }, + onValueChange = { state.editAction.invoke(it) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + } + } + } + + @Composable + private fun CrossFreeCollateralContent( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = state.localizer.localize("APP.GENERAL.CROSS_FREE_COLLATERAL"), + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.small), + ) + Spacer(modifier = Modifier.weight(1f)) + + CrossFreeCollateralChange(modifier = Modifier, state = state) + } + } + + @Composable + private fun CrossFreeCollateralChange( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = modifier, + ) { + PlatformAmountChange( + before = { + SizeTextView.Content( + modifier = Modifier, + state = SizeTextView.ViewState( + localizer = state.localizer, + formatter = state.formatter, + size = state.subaccountReceipt.freeCollateral.firstOrNull()?.toDoubleOrNull(), + stepSize = 2, + ), + ) + }, + after = { + SizeTextView.Content( + modifier = Modifier, + state = SizeTextView.ViewState( + localizer = state.localizer, + formatter = state.formatter, + size = state.subaccountReceipt.freeCollateral.lastOrNull()?.toDoubleOrNull(), + stepSize = 2, + ), + ) + }, + ) + } + } + + @Composable + private fun CrossMarginContent( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = state.localizer.localize("APP.GENERAL.CROSS_MARGIN_USAGE"), + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.small), + ) + Spacer(modifier = Modifier.weight(1f)) + + CrossMarginUsageChange(modifier = Modifier, state = state) + } + } + + @Composable + private fun CrossMarginUsageChange( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = modifier, + ) { + PlatformAmountChange( + before = { + SizeTextView.Content( + modifier = Modifier, + state = SizeTextView.ViewState( + localizer = state.localizer, + formatter = state.formatter, + size = state.subaccountReceipt.marginUsage.firstOrNull()?.toDoubleOrNull(), + stepSize = 2, + ), + ) + }, + after = { + SizeTextView.Content( + modifier = Modifier, + state = SizeTextView.ViewState( + localizer = state.localizer, + formatter = state.formatter, + size = state.subaccountReceipt.marginUsage.lastOrNull()?.toDoubleOrNull(), + stepSize = 2, + ), + ) + }, + ) + } + } +} diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputViewModel.kt new file mode 100644 index 00000000..f657cda6 --- /dev/null +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputViewModel.kt @@ -0,0 +1,67 @@ +package exchange.dydx.trading.feature.trade.margin + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.input.TradeInput +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.platformui.components.PlatformInfo +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.common.navigation.DydxRouter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class DydxAdjustMarginInputViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val router: DydxRouter, + val platformInfo: PlatformInfo, +) : ViewModel(), DydxViewModel { + + val state: Flow = abacusStateManager.state.tradeInput + .map { + createViewState(it) + } + .distinctUntilChanged() + + private fun createViewState(tradeInput: TradeInput?): DydxAdjustMarginInputView.ViewState { + /* + Abacus not implemented for adjust margin yet. This is a placeholder. + */ + return DydxAdjustMarginInputView.ViewState( + localizer = localizer, + formatter = formatter, + direction = DydxAdjustMarginInputView.MarginDirection.Add, + percentage = 0.5, + percentageOptions = listOf( + DydxAdjustMarginInputView.PercentageOption("10%", 0.1), + DydxAdjustMarginInputView.PercentageOption("20%", 0.2), + DydxAdjustMarginInputView.PercentageOption("30%", 0.3), + DydxAdjustMarginInputView.PercentageOption("50%", 0.5), + ), + amountText = "500", + subaccountReceipt = DydxAdjustMarginInputView.SubaccountReceipt( + freeCollateral = listOf("1000.00", "500.00"), + marginUsage = listOf("19.34", "38.45"), + ), + positionReceipt = DydxAdjustMarginInputView.PositionReceipt( + freeCollateral = listOf("1000.00", "1500.00"), + leverage = listOf("3.1", "2.4"), + liquidationPrice = listOf("1200.00", "1000.00"), + ), + error = null, + marginDirectionAction = { }, + percentageAction = { }, + editAction = { }, + action = { }, + closeAction = { + router.navigateBack() + }, + ) + } +} diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/orderbook/components/DydxOrderbookSideViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/orderbook/components/DydxOrderbookSideViewModel.kt index 78aaffcb..0bd7d9f1 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/orderbook/components/DydxOrderbookSideViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/orderbook/components/DydxOrderbookSideViewModel.kt @@ -158,7 +158,7 @@ private fun createViewState( price = line.price, size = line.size, sizeText = formatter.raw(line.size, market?.configs?.displayStepSizeDecimals ?: 4) ?: "", - priceText = formatter.dollar(line.price, orderbook?.grouping?.tickSize.toString()) ?: "", + priceText = formatter.dollar(line.price, orderbook?.grouping?.tickSize) ?: "", depth = line.depth, taken = usage?.size, textColor = textColor, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/streams/TradeStream.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/streams/TradeStream.kt index 0cbed37d..87797d2c 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/streams/TradeStream.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/streams/TradeStream.kt @@ -4,15 +4,13 @@ import dagger.hilt.android.scopes.ActivityRetainedScoped import exchange.dydx.abacus.output.SubaccountOrder import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol -import exchange.dydx.trading.common.di.CoroutineScopes -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import javax.inject.Inject interface TradeStreaming { @@ -28,12 +26,12 @@ interface MutableTradeStreaming : TradeStreaming { @ActivityRetainedScoped class TradeStream @Inject constructor( val abacusStateManager: AbacusStateManagerProtocol, - @CoroutineScopes.App val appScope: CoroutineScope ) : MutableTradeStreaming { private var _submissionStatus: MutableStateFlow = MutableStateFlow(null) - override val submissionStatus: Flow = _submissionStatus + override val submissionStatus: StateFlow = + _submissionStatus.asStateFlow() override val lastOrder: Flow = combine( @@ -57,9 +55,8 @@ class TradeStream @Inject constructor( .distinctUntilChanged() override fun submitTrade() { - _submissionStatus.update { null } - appScope.launch { - val tradeInput = abacusStateManager.state.tradeInput.first() ?: return@launch + if (abacusStateManager.state.tradeInput != null) { + _submissionStatus.update { null } abacusStateManager.placeOrder { submissionStatus -> if (submissionStatus == AbacusStateManagerProtocol.SubmissionStatus.Success) { @@ -71,9 +68,8 @@ class TradeStream @Inject constructor( } override fun closePosition() { - _submissionStatus.update { null } - appScope.launch { - val closePositionInput = abacusStateManager.state.closePositionInput.first() ?: return@launch + if (abacusStateManager.state.closePositionInput != null) { + _submissionStatus.update { null } abacusStateManager.closePosition { submissionStatus -> _submissionStatus.update { submissionStatus } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/streams/TriggerOrderStream.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/streams/TriggerOrderStream.kt new file mode 100644 index 00000000..8f3f38f0 --- /dev/null +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/streams/TriggerOrderStream.kt @@ -0,0 +1,85 @@ +package exchange.dydx.trading.feature.trade.streams + +import dagger.hilt.android.scopes.ActivityRetainedScoped +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.dydxstatemanager.stopLossOrders +import exchange.dydx.dydxstatemanager.takeProfitOrders +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +enum class GainLossDisplayType { + Amount, + Percent; + + val value: String + get() = when (this) { + Amount -> "$" + Percent -> "%" + } + + companion object { + val list = listOf(Amount, Percent) + } +} + +interface TriggerOrderStreaming { + val submissionStatus: Flow + val takeProfitGainLossDisplayType: Flow + val stopLossGainLossDisplayType: Flow + val isNewTriggerOrder: Flow +} + +interface MutableTriggerOrderStreaming : TriggerOrderStreaming { + fun updatesubmissionStatus(status: AbacusStateManagerProtocol.SubmissionStatus?) + fun clearSubmissionStatus() + fun setTakeProfitGainLossDisplayType(displayType: GainLossDisplayType) + fun setStopLossGainLossDisplayType(displayType: GainLossDisplayType) +} + +@ActivityRetainedScoped +class TriggerOrderStream @Inject constructor( + val abacusStateManager: AbacusStateManagerProtocol, +) : MutableTriggerOrderStreaming { + + private val _submissionStatus: MutableStateFlow = MutableStateFlow(null) + private val _takeProfitGainLossDisplayType = MutableStateFlow(GainLossDisplayType.Amount) + private val _stopLossGainLossDisplayType = MutableStateFlow(GainLossDisplayType.Amount) + + override val submissionStatus = _submissionStatus + override val takeProfitGainLossDisplayType = _takeProfitGainLossDisplayType + override val stopLossGainLossDisplayType = _stopLossGainLossDisplayType + + private val marketIdFlow = abacusStateManager.state.triggerOrdersInput + .mapNotNull { it?.marketId } + + override val isNewTriggerOrder: Flow = + combine( + marketIdFlow.flatMapLatest { abacusStateManager.state.takeProfitOrders(it) }, + marketIdFlow.flatMapLatest { abacusStateManager.state.stopLossOrders(it) }, + ) { takeProfitOrders, stopLossOrders -> + takeProfitOrders.isNullOrEmpty() && stopLossOrders.isNullOrEmpty() + } + + override fun updatesubmissionStatus(status: AbacusStateManagerProtocol.SubmissionStatus?) { + _submissionStatus.update { status } + } + + override fun clearSubmissionStatus() { + _submissionStatus.update { null } + _takeProfitGainLossDisplayType.update { GainLossDisplayType.Amount } + _stopLossGainLossDisplayType.update { GainLossDisplayType.Amount } + } + + override fun setTakeProfitGainLossDisplayType(displayType: GainLossDisplayType) { + _takeProfitGainLossDisplayType.update { displayType } + } + + override fun setStopLossGainLossDisplayType(displayType: GainLossDisplayType) { + _stopLossGainLossDisplayType.update { displayType } + } +} diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputMarginModeView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputMarginModeView.kt index f5401ace..b6b5d6bf 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputMarginModeView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputMarginModeView.kt @@ -91,7 +91,7 @@ object DydxTradeInputMarginModeView : DydxComponent { val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value PlatformInfoScaffold(modifier = modifier, platformInfo = viewModel.platformInfo) { - Content(modifier, state) + Content(it, state) } } @@ -107,7 +107,7 @@ object DydxTradeInputMarginModeView : DydxComponent { .fillMaxSize() .themeColor(ThemeColor.SemanticColor.layer_4), ) { - MarginModeViewHeader( + NavigationHeader( modifier = Modifier, state = state, ) @@ -125,12 +125,12 @@ object DydxTradeInputMarginModeView : DydxComponent { } @Composable - fun MarginModeViewHeader( + fun NavigationHeader( modifier: Modifier, state: ViewState, ) { Row( - modifier = modifier + modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, @@ -172,7 +172,7 @@ object DydxTradeInputMarginModeView : DydxComponent { ) { val shape = RoundedCornerShape(10.dp) Row( - modifier = modifier + modifier = Modifier .padding( horizontal = ThemeShapes.HorizontalPadding, vertical = ThemeShapes.VerticalPadding, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputMarginModeViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputMarginModeViewModel.kt index 9c6ba7a6..38324f80 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputMarginModeViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputMarginModeViewModel.kt @@ -33,24 +33,23 @@ class DydxTradeInputMarginModeViewModel @Inject constructor( private fun createViewState(tradeInput: TradeInput?): DydxTradeInputMarginModeView.ViewState { return DydxTradeInputMarginModeView.ViewState( - localizer.localize("APP.GENERAL.MARGIN_MODE"), - tradeInput?.marketId ?: "", - DydxTradeInputMarginModeView.MarginTypeSelection( + title = localizer.localize("APP.GENERAL.MARGIN_MODE"), + asset = tradeInput?.marketId ?: "", + crossMargin = DydxTradeInputMarginModeView.MarginTypeSelection( localizer.localize("APP.GENERAL.CROSS_MARGIN"), localizer.localize("APP.GENERAL.CROSS_MARGIN_DESCRIPTION"), tradeInput?.marginMode == MarginMode.cross, - { - }, - ), - DydxTradeInputMarginModeView.MarginTypeSelection( + ) { + }, + isolatedMargin = DydxTradeInputMarginModeView.MarginTypeSelection( localizer.localize("APP.GENERAL.ISOLATED_MARGIN"), localizer.localize("APP.GENERAL.ISOLATED_MARGIN_DESCRIPTION"), tradeInput?.marginMode == MarginMode.isolated, - { - }, - ), - null, - { + ) { + }, + errorText = null, + closeAction = { + router.navigateBack() }, ) } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputTargetLeverageView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputTargetLeverageView.kt index 5ed2630e..e6fdd29b 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputTargetLeverageView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputTargetLeverageView.kt @@ -4,23 +4,39 @@ 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.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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 androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.platformui.components.PlatformInfoScaffold +import exchange.dydx.platformui.components.buttons.PlatformButton +import exchange.dydx.platformui.components.buttons.PlatformButtonState +import exchange.dydx.platformui.components.buttons.PlatformPillItem import exchange.dydx.platformui.components.dividers.PlatformDivider +import exchange.dydx.platformui.components.tabgroups.PlatformTabGroup 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.dydxDefault import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.designSystem.theme.themeFont import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface +import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.HeaderViewCloseBotton +import exchange.dydx.trading.feature.shared.views.LabeledTextInput data class LeverageTextAndValue(val text: String, val value: Double) @@ -37,16 +53,14 @@ fun Preview_DydxTradeInputTargetLeverageView() { object DydxTradeInputTargetLeverageView : DydxComponent { data class ViewState( - val title: String?, - val text: String?, + val localizer: LocalizerProtocol, val leverageText: String?, val leverageOptions: List?, val closeAction: (() -> Unit)? = null, ) { companion object { val preview = ViewState( - title = "title", - text = "text", + localizer = MockLocalizer(), leverageText = "1.0", leverageOptions = listOf(), ) @@ -59,7 +73,7 @@ object DydxTradeInputTargetLeverageView : DydxComponent { val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value PlatformInfoScaffold(modifier = modifier, platformInfo = viewModel.platformInfo) { - Content(modifier, state) + Content(it, state) } } @@ -73,21 +87,198 @@ object DydxTradeInputTargetLeverageView : DydxComponent { modifier = modifier .animateContentSize() .fillMaxSize() - .themeColor(ThemeColor.SemanticColor.layer_3), + .themeColor(ThemeColor.SemanticColor.layer_4), ) { - Row( - modifier - .fillMaxWidth() - .padding(vertical = ThemeShapes.VerticalPadding), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - ) { - HeaderViewCloseBotton( - closeAction = state.closeAction, + NavigationHeader( + modifier = Modifier, + state = state, + ) + PlatformDivider() + Description( + modifier = Modifier, + state = state, + ) + LeverageEditField( + modifier = Modifier, + state = state, + ) + LeverageOptions( + modifier = Modifier, + state = state, + ) + Spacer(modifier = Modifier.weight(1f)) + ActionButton( + modifier = Modifier, + state = state, + ) + } + } + + @Composable + fun NavigationHeader( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + Spacer(modifier = Modifier.width(16.dp)) + Text( + modifier = Modifier.padding(horizontal = 0.dp), + style = TextStyle.dydxDefault + .themeFont( + fontSize = ThemeFont.FontSize.large, + fontType = ThemeFont.FontType.plus, + ) + .themeColor(ThemeColor.SemanticColor.text_primary), + text = state.localizer.localize("APP.TRADE.ADJUST_TARGET_LEVERAGE"), + ) + Spacer(modifier = Modifier.weight(1f)) + HeaderViewCloseBotton( + closeAction = state.closeAction, + ) + } + } + + @Composable + fun Description( + modifier: Modifier, + state: ViewState, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + Spacer(modifier = Modifier.width(16.dp)) + Text( + modifier = Modifier.padding(horizontal = 0.dp), + text = state.localizer.localize("APP.TRADE.ADJUST_TARGET_LEVERAGE_DESCRIPTION"), + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small), + ) + } + } + + @Composable + fun LeverageEditField( + modifier: Modifier, + state: ViewState? + ) { + Row( + modifier = Modifier + .padding( + horizontal = ThemeShapes.HorizontalPadding, + vertical = 8.dp, + ) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + InputFieldScaffold(modifier) { + LabeledTextInput.Content( + modifier = Modifier, + state = LabeledTextInput.ViewState( + localizer = MockLocalizer(), + label = state?.localizer?.localize("APP.TRADE.TARGET_LEVERAGE"), + value = state?.leverageText ?: "", + onValueChanged = {}, + ), ) } + } + } - PlatformDivider() + @Composable + fun LeverageOptions( + modifier: Modifier, + state: ViewState? + ) { + Row( + modifier = Modifier + .padding( + horizontal = ThemeShapes.HorizontalPadding, + vertical = 8.dp, + ) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + PlatformTabGroup( + modifier = Modifier.fillMaxWidth(), + scrollingEnabled = false, + items = state?.leverageOptions?.map { + { modifier -> + PlatformPillItem( + modifier = Modifier + .padding( + vertical = 4.dp, + horizontal = 8.dp, + ), + backgroundColor = ThemeColor.SemanticColor.layer_5, + ) { + Text( + text = it.text, + modifier = Modifier, + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.small), + + ) + } + } + } ?: listOf(), + selectedItems = state?.leverageOptions?.map { + { modifier -> + PlatformPillItem( + modifier = Modifier + .padding( + vertical = 4.dp, + horizontal = 8.dp, + ), + backgroundColor = ThemeColor.SemanticColor.layer_2, + ) { + Text( + text = it.text, + modifier = Modifier, + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_primary) + .themeFont(fontSize = ThemeFont.FontSize.small), + + ) + } + } + } ?: listOf(), + equalWeight = false, + currentSelection = state?.leverageOptions?.indexOfFirst { + it.text == state.leverageText + }, + onSelectionChanged = {}, + ) + } + } + + @Composable + fun ActionButton( + modifier: Modifier, + state: ViewState? + ) { + PlatformButton( + modifier = Modifier + .padding( + horizontal = ThemeShapes.HorizontalPadding, + vertical = ThemeShapes.VerticalPadding, + ) + .fillMaxWidth(), + text = state?.localizer?.localize("APP.TRADE.CONFIRM_LEVERAGE"), + state = PlatformButtonState.Primary, + ) { } } } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputTargetLeverageViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputTargetLeverageViewModel.kt index 1d21a0cd..babc2cbc 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputTargetLeverageViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/DydxTradeInputTargetLeverageViewModel.kt @@ -35,11 +35,11 @@ class DydxTradeInputTargetLeverageViewModel @Inject constructor( val maxLeverage = tradeInput?.options?.maxLeverage ?: 5.0 val leverages = leverageOptions(maxLeverage) return DydxTradeInputTargetLeverageView.ViewState( - localizer.localize("APP.TRADE.ADJUST_TARGET_LEVERAGE"), - localizer.localize("APP.TRADE.ADJUST_TARGET_LEVERAGE_DESCRIPTION"), - formatter.localFormatted(targetLeverage, 1), - leverages, - { + localizer = localizer, + leverageText = formatter.localFormatted(targetLeverage, 1), + leverageOptions = leverages, + closeAction = { + router.navigateBack() }, ) } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/DydxTradeInputCtaButtonViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/DydxTradeInputCtaButtonViewModel.kt index 0b01f5e9..65d91dff 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/DydxTradeInputCtaButtonViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/DydxTradeInputCtaButtonViewModel.kt @@ -77,7 +77,7 @@ class DydxTradeInputCtaButtonViewModel @Inject constructor( ctaAction = { tradeStream.submitTrade() router.navigateTo( - route = TradeRoutes.status, + route = TradeRoutes.status + "/trade", presentation = DydxRouter.Presentation.Modal, ) }, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/execution/DydxTradeInputExecutionView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/execution/DydxTradeInputExecutionView.kt index 2e2d09ae..8742f707 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/execution/DydxTradeInputExecutionView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/execution/DydxTradeInputExecutionView.kt @@ -9,7 +9,7 @@ import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledSelectionInput @Preview @@ -47,7 +47,7 @@ object DydxTradeInputExecutionView : DydxComponent { return } - InputFieldScarfold(modifier) { + InputFieldScaffold(modifier) { LabeledSelectionInput.Content( modifier = Modifier, state = state.labeledSelectionInput, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/goodtil/DydxTradeInputGoodTilView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/goodtil/DydxTradeInputGoodTilView.kt index 7661540f..ac882d8f 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/goodtil/DydxTradeInputGoodTilView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/goodtil/DydxTradeInputGoodTilView.kt @@ -27,7 +27,7 @@ import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledSelectionInput import exchange.dydx.trading.feature.shared.views.LabeledTextInput @@ -72,7 +72,7 @@ object DydxTradeInputGoodTilView : DydxComponent { mutableStateOf(false) } - InputFieldScarfold(modifier) { + InputFieldScaffold(modifier) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/leverage/DydxTradeInputLeverageView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/leverage/DydxTradeInputLeverageView.kt index f4e784f6..f019531d 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/leverage/DydxTradeInputLeverageView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/leverage/DydxTradeInputLeverageView.kt @@ -1,6 +1,5 @@ package exchange.dydx.trading.feature.trade.tradeinput.components.inputfields.leverage -import android.util.Half.toFloat import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -39,9 +38,10 @@ import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.GradientSlider import exchange.dydx.trading.feature.shared.views.SideTextView +import exchange.dydx.utilities.utils.rounded import kotlin.math.abs @Preview @@ -66,7 +66,7 @@ object DydxTradeInputLeverageView : DydxComponent { val maxLeverage: Double?, val side: OrderSide = OrderSide.Buy, val sideToggleAction: (OrderSide) -> Unit = {}, - val leverageUpdateAction: (Double) -> Unit = {}, + val leverageUpdateAction: (String) -> Unit = {}, ) { companion object { val preview = ViewState( @@ -94,7 +94,7 @@ object DydxTradeInputLeverageView : DydxComponent { return } - InputFieldScarfold(modifier) { + InputFieldScaffold(modifier) { Column( modifier = modifier .padding(ThemeShapes.InputPaddingValues) @@ -147,11 +147,7 @@ object DydxTradeInputLeverageView : DydxComponent { modifier = modifier, value = leverageValue, onValueChange = { - try { - val value = it.toDouble() - state.leverageUpdateAction(value) - } catch (e: Exception) { - } + state.leverageUpdateAction(it) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), ) @@ -213,8 +209,7 @@ object DydxTradeInputLeverageView : DydxComponent { return } - val positionRatio = state.formatter.raw((positionLeverage / maxLeverage).toDouble(), 1) - ?.toFloat() ?: 0f + val positionRatio = (positionLeverage / maxLeverage).rounded(toPlaces = 1) val sliderViewState = GradientSlider.ViewState( localizer = state.localizer, @@ -224,7 +219,9 @@ object DydxTradeInputLeverageView : DydxComponent { OrderSide.Buy -> positionLeverage..maxLeverage }, onValueChange = { - state.leverageUpdateAction(it.toDouble()) + state.formatter.raw(it.toDouble())?.let { stringValue -> + state.leverageUpdateAction(stringValue) + } focusManager.clearFocus() }, leftRatio = when (state.side) { @@ -237,10 +234,9 @@ object DydxTradeInputLeverageView : DydxComponent { }, ) - val absPositionLeverage = state.formatter.raw(abs(positionLeverage).toDouble(), 1) - ?.toFloat() ?: 0f - val absMaxLeverage = state.formatter.raw(abs(maxLeverage).toDouble(), 1) - ?.toFloat() ?: 0f + val absPositionLeverage = abs(positionLeverage).rounded(1) + val absMaxLeverage = abs(maxLeverage).rounded(1) + Column( modifier = Modifier, verticalArrangement = Arrangement.spacedBy(0.dp), diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/limitprice/DydxTradeInputLimitPriceView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/limitprice/DydxTradeInputLimitPriceView.kt index 7014e722..7c898bc7 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/limitprice/DydxTradeInputLimitPriceView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/limitprice/DydxTradeInputLimitPriceView.kt @@ -9,7 +9,7 @@ import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledTextInput @Preview @@ -50,7 +50,7 @@ object DydxTradeInputLimitPriceView : DydxComponent { return } - InputFieldScarfold(modifier) { + InputFieldScaffold(modifier) { LabeledTextInput.Content( modifier = Modifier, state = state.labeledTextInput, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/size/DydxTradeInputSizeView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/size/DydxTradeInputSizeView.kt index 4d8e08e7..156b9caa 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/size/DydxTradeInputSizeView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/size/DydxTradeInputSizeView.kt @@ -32,7 +32,7 @@ import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer import exchange.dydx.trading.feature.shared.R -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledTextInput import exchange.dydx.trading.feature.shared.views.TokenTextView @@ -83,7 +83,7 @@ object DydxTradeInputSizeView : DydxComponent { val showingUsdc = remember { mutableStateOf(state.showingUsdc) } val focusManager = LocalFocusManager.current - InputFieldScarfold(modifier) { + InputFieldScaffold(modifier) { Column { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/timeinforce/DydxTradeInputTimeInForceView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/timeinforce/DydxTradeInputTimeInForceView.kt index f6928045..94a2ee55 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/timeinforce/DydxTradeInputTimeInForceView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/timeinforce/DydxTradeInputTimeInForceView.kt @@ -9,7 +9,7 @@ import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledSelectionInput @Preview @@ -50,7 +50,7 @@ object DydxTradeInputTimeInForceView : DydxComponent { return } - InputFieldScarfold(modifier) { + InputFieldScaffold(modifier) { LabeledSelectionInput.Content( modifier = Modifier, state = state.labeledSelectionInput, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/triggerprice/DydxTradeInputTriggerPriceView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/triggerprice/DydxTradeInputTriggerPriceView.kt index b5bb4958..63ba0466 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/triggerprice/DydxTradeInputTriggerPriceView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradeinput/components/inputfields/triggerprice/DydxTradeInputTriggerPriceView.kt @@ -9,7 +9,7 @@ import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledTextInput @Preview @@ -50,7 +50,7 @@ object DydxTradeInputTriggerPriceView : DydxComponent { return } - InputFieldScarfold(modifier) { + InputFieldScaffold(modifier) { LabeledTextInput.Content( modifier = Modifier, state = state.labeledTextInput, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradestatus/components/DydxTradeStatusCtaButtonView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradestatus/components/DydxTradeStatusCtaButtonView.kt index fd14619f..0f64f0fe 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradestatus/components/DydxTradeStatusCtaButtonView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradestatus/components/DydxTradeStatusCtaButtonView.kt @@ -24,9 +24,12 @@ fun Preview_DydxTradeStatusCtaButtonView() { } object DydxTradeStatusCtaButtonView : DydxComponent { + data class ViewState( val localizer: LocalizerProtocol, - val returnAction: () -> Unit = {}, + val ctaButtonTitle: String = "Try again", + val ctaButtonState: PlatformButtonState = PlatformButtonState.Secondary, + val ctaButtonAction: () -> Unit = {}, ) { companion object { val preview = ViewState( @@ -51,10 +54,10 @@ object DydxTradeStatusCtaButtonView : DydxComponent { PlatformButton( modifier = modifier, - text = state.localizer.localize("APP.TRADE.RETURN_TO_MARKET"), - state = PlatformButtonState.Secondary, + text = state.ctaButtonTitle, + state = state.ctaButtonState, ) { - state.returnAction() + state.ctaButtonAction() } } } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradestatus/components/DydxTradeStatusCtaButtonViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradestatus/components/DydxTradeStatusCtaButtonViewModel.kt index 45844607..4433e41c 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradestatus/components/DydxTradeStatusCtaButtonViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/tradestatus/components/DydxTradeStatusCtaButtonViewModel.kt @@ -1,12 +1,14 @@ package exchange.dydx.trading.feature.trade.tradestatus.components +import androidx.lifecycle.SavedStateHandle 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.platformui.components.buttons.PlatformButtonState import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.navigation.DydxRouter +import exchange.dydx.trading.feature.trade.streams.MutableTradeStreaming import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -15,23 +17,67 @@ import javax.inject.Inject @HiltViewModel class DydxTradeStatusCtaButtonViewModel @Inject constructor( private val localizer: LocalizerProtocol, - private val abacusStateManager: AbacusStateManagerProtocol, private val router: DydxRouter, + private val tradeStream: MutableTradeStreaming, + private val savedStateHandle: SavedStateHandle, ) : ViewModel(), DydxViewModel { + private enum class TradeType { + Trade, + ClosePosition; + + companion object { + fun fromString(value: String?): TradeType { + return when (value) { + "trade" -> Trade + "closePosition" -> ClosePosition + else -> throw IllegalArgumentException("Invalid trade type: $value") + } + } + } + } + + private val tradeType = TradeType.fromString(savedStateHandle["tradeType"]) + val state: Flow = - abacusStateManager.state.marketSummary + tradeStream.submissionStatus .map { createViewState(it) } .distinctUntilChanged() - private fun createViewState(marketSummary: PerpetualMarketSummary?): DydxTradeStatusCtaButtonView.ViewState { - return DydxTradeStatusCtaButtonView.ViewState( - localizer = localizer, - returnAction = { - router.navigateBack() - }, - ) + private fun createViewState( + submissionStatus: AbacusStateManagerProtocol.SubmissionStatus?, + ): DydxTradeStatusCtaButtonView.ViewState { + return when (submissionStatus) { + is AbacusStateManagerProtocol.SubmissionStatus.Success -> + DydxTradeStatusCtaButtonView.ViewState( + localizer = localizer, + ctaButtonTitle = localizer.localize("APP.TRADE.RETURN_TO_MARKET"), + ctaButtonState = PlatformButtonState.Secondary, + ctaButtonAction = { + router.navigateBack() + }, + ) + is AbacusStateManagerProtocol.SubmissionStatus.Failed -> + DydxTradeStatusCtaButtonView.ViewState( + localizer = localizer, + ctaButtonTitle = localizer.localize("APP.ONBOARDING.TRY_AGAIN"), + ctaButtonState = PlatformButtonState.Primary, + ctaButtonAction = { + when (tradeType) { + TradeType.Trade -> tradeStream.submitTrade() + TradeType.ClosePosition -> tradeStream.closePosition() + } + }, + ) + else -> + DydxTradeStatusCtaButtonView.ViewState( + localizer = localizer, + ctaButtonTitle = localizer.localize("APP.TRADE.SUBMITTING_ORDER"), + ctaButtonState = PlatformButtonState.Disabled, + ctaButtonAction = {}, + ) + } } } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/DydxTriggerOrderInputView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/DydxTriggerOrderInputView.kt index e23fee0a..28991931 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/DydxTriggerOrderInputView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/DydxTriggerOrderInputView.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.gestures.detectTapGestures 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.padding @@ -20,7 +19,10 @@ 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 androidx.lifecycle.Lifecycle import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.OnLifecycleEvent +import exchange.dydx.platformui.components.PlatformInfoScaffold import exchange.dydx.platformui.components.dividers.PlatformDivider import exchange.dydx.platformui.designSystem.theme.ThemeColor import exchange.dydx.platformui.designSystem.theme.ThemeFont @@ -30,8 +32,10 @@ import exchange.dydx.platformui.designSystem.theme.themeColor import exchange.dydx.platformui.designSystem.theme.themeFont import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle +import exchange.dydx.trading.common.navigation.DydxAnimation import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.receipt.validation.DydxValidationView import exchange.dydx.trading.feature.shared.views.HeaderView import exchange.dydx.trading.feature.shared.views.HeaderViewCloseBotton import exchange.dydx.trading.feature.trade.trigger.components.DydxTriggerOrderCtaButtonView @@ -39,6 +43,7 @@ import exchange.dydx.trading.feature.trade.trigger.components.DydxTriggerOrderRe import exchange.dydx.trading.feature.trade.trigger.components.inputfields.DydxTriggerOrderInputType import exchange.dydx.trading.feature.trade.trigger.components.inputfields.DydxTriggerOrderPriceInputType import exchange.dydx.trading.feature.trade.trigger.components.inputfields.gainloss.DydxTriggerOrderGainLossView +import exchange.dydx.trading.feature.trade.trigger.components.inputfields.headersection.DydxTriggerSectionHeaderView import exchange.dydx.trading.feature.trade.trigger.components.inputfields.limitprice.DydxTriggerOrderLimitPriceSectionView import exchange.dydx.trading.feature.trade.trigger.components.inputfields.price.DydxTriggerOrderPriceView import exchange.dydx.trading.feature.trade.trigger.components.inputfields.size.DydxTriggerOrderSizeView @@ -52,9 +57,20 @@ fun Preview_DydxTriggerOrderInputView() { } object DydxTriggerOrderInputView : DydxComponent { + + enum class ValidationErrorSection { + TakeProfit, + StopLoss, + Size, + LimitPrice, + None, + } + data class ViewState( val localizer: LocalizerProtocol, val closeAction: (() -> Unit)? = null, + val backHandler: (() -> Unit)? = null, + val validationErrorSection: ValidationErrorSection = ValidationErrorSection.None, ) { companion object { val preview = ViewState( @@ -68,7 +84,18 @@ object DydxTriggerOrderInputView : DydxComponent { val viewModel: DydxTriggerOrderInputViewModel = hiltViewModel() val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value - Content(modifier, state) + OnLifecycleEvent { _, event -> + if (event == Lifecycle.Event.ON_STOP) { + state?.backHandler?.invoke() + } + } + + PlatformInfoScaffold( + modifier = modifier, + platformInfo = viewModel.platformInfo, + ) { + Content(it, state) + } } @Composable @@ -96,6 +123,7 @@ object DydxTriggerOrderInputView : DydxComponent { Column( modifier = Modifier .fillMaxWidth() + .weight(1f) .verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(12.dp), ) { @@ -113,7 +141,7 @@ object DydxTriggerOrderInputView : DydxComponent { state = state, ) - AdvacedDividerView( + AdvancedDividerView( modifier = Modifier, state = state, ) @@ -124,14 +152,28 @@ object DydxTriggerOrderInputView : DydxComponent { .padding(horizontal = ThemeShapes.HorizontalPadding), ) + DydxAnimation.AnimateExpandInOut( + visible = state.validationErrorSection == ValidationErrorSection.Size, + ) { + DydxValidationView.Content( + modifier = Modifier.padding(horizontal = ThemeShapes.HorizontalPadding), + ) + } + DydxTriggerOrderLimitPriceSectionView.Content( modifier = Modifier .fillMaxWidth() .padding(horizontal = ThemeShapes.HorizontalPadding), ) - } - Spacer(modifier = Modifier.weight(1f)) + DydxAnimation.AnimateExpandInOut( + visible = state.validationErrorSection == ValidationErrorSection.LimitPrice, + ) { + DydxValidationView.Content( + modifier = Modifier.padding(horizontal = ThemeShapes.HorizontalPadding), + ) + } + } DydxTriggerOrderCtaButtonView.Content( modifier = Modifier @@ -185,15 +227,28 @@ object DydxTriggerOrderInputView : DydxComponent { .fillMaxWidth() .padding(horizontal = ThemeShapes.HorizontalPadding), ) { - Text( - text = state.localizer.localize("TRADE.BRACKET_ORDER_TP.TITLE"), - style = TextStyle.dydxDefault - .themeFont(fontSize = ThemeFont.FontSize.base) - .themeColor(ThemeColor.SemanticColor.text_secondary), - ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.localizer.localize("TRADE.BRACKET_ORDER_TP.TITLE"), + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.base) + .themeColor(ThemeColor.SemanticColor.text_secondary), + ) + + DydxTriggerSectionHeaderView.Content( + modifier = Modifier.weight(1f), + inputType = DydxTriggerOrderInputType.TakeProfit, + ) + } Row( - modifier = modifier + modifier = Modifier .fillMaxWidth() .padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -209,6 +264,14 @@ object DydxTriggerOrderInputView : DydxComponent { ) } } + + DydxAnimation.AnimateExpandInOut( + visible = state.validationErrorSection == ValidationErrorSection.TakeProfit, + ) { + DydxValidationView.Content( + modifier = Modifier.padding(horizontal = ThemeShapes.HorizontalPadding), + ) + } } @Composable @@ -218,15 +281,28 @@ object DydxTriggerOrderInputView : DydxComponent { .fillMaxWidth() .padding(horizontal = ThemeShapes.HorizontalPadding), ) { - Text( - text = state.localizer.localize("TRADE.BRACKET_ORDER_SL.TITLE"), - style = TextStyle.dydxDefault - .themeFont(fontSize = ThemeFont.FontSize.base) - .themeColor(ThemeColor.SemanticColor.text_secondary), - ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.localizer.localize("TRADE.BRACKET_ORDER_SL.TITLE"), + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.base) + .themeColor(ThemeColor.SemanticColor.text_secondary), + ) + + DydxTriggerSectionHeaderView.Content( + modifier = Modifier.weight(1f), + inputType = DydxTriggerOrderInputType.StopLoss, + ) + } Row( - modifier = modifier + modifier = Modifier .fillMaxWidth() .padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -242,13 +318,22 @@ object DydxTriggerOrderInputView : DydxComponent { ) } } + + DydxAnimation.AnimateExpandInOut( + visible = state.validationErrorSection == ValidationErrorSection.StopLoss, + ) { + DydxValidationView.Content( + modifier = Modifier.padding(horizontal = ThemeShapes.HorizontalPadding), + ) + } } @Composable - private fun AdvacedDividerView(modifier: Modifier, state: ViewState) { + private fun AdvancedDividerView(modifier: Modifier, state: ViewState) { Row( modifier = modifier .fillMaxWidth() + .padding(vertical = ThemeShapes.VerticalPadding) .padding(horizontal = ThemeShapes.HorizontalPadding), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/DydxTriggerOrderInputViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/DydxTriggerOrderInputViewModel.kt index b304d825..294b7f47 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/DydxTriggerOrderInputViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/DydxTriggerOrderInputViewModel.kt @@ -2,15 +2,32 @@ package exchange.dydx.trading.feature.trade.trigger import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.SubaccountOrder +import exchange.dydx.abacus.output.SubaccountPosition +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.TriggerOrdersInput +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.state.model.TriggerOrdersInputField import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.dydxstatemanager.stopLossOrders +import exchange.dydx.dydxstatemanager.takeProfitOrders +import exchange.dydx.platformui.components.PlatformInfo 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.feature.trade.streams.MutableTriggerOrderStreaming +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import javax.inject.Inject @HiltViewModel @@ -20,10 +37,20 @@ class DydxTriggerOrderInputViewModel @Inject constructor( private val router: DydxRouter, private val formatter: DydxFormatter, savedStateHandle: SavedStateHandle, + val platformInfo: PlatformInfo, + private val triggerOrderStream: MutableTriggerOrderStreaming, + @CoroutineScopes.ViewModel private val viewModelScope: CoroutineScope, ) : ViewModel(), DydxViewModel { private val marketId: String? + val state: Flow = + abacusStateManager.state.validationErrors + .map { validationErrors -> + createViewState(validationErrors) + } + .distinctUntilChanged() + init { marketId = savedStateHandle["marketId"] @@ -31,18 +58,205 @@ class DydxTriggerOrderInputViewModel @Inject constructor( router.navigateBack() } else { abacusStateManager.setMarket(marketId = marketId) - abacusStateManager.triggerOrders(input = marketId, type = TriggerOrdersInputField.marketId) + abacusStateManager.triggerOrders( + input = marketId, + type = TriggerOrdersInputField.marketId, + ) + + combine( + abacusStateManager.state.selectedSubaccountPositionOfMarket(marketId), + abacusStateManager.state.takeProfitOrders(marketId), + abacusStateManager.state.stopLossOrders(marketId), + abacusStateManager.state.triggerOrdersInput, + ) { position, takeProfitOrders, stopLossOrders, triggerOrdersInput -> + updateAbacusTriggerOrder( + position, + takeProfitOrders, + stopLossOrders, + triggerOrdersInput, + ) + } + .launchIn(viewModelScope) } - } - val state: Flow = flowOf(createViewState()) + subscribeToStatus() + } - private fun createViewState(): DydxTriggerOrderInputView.ViewState { + private fun createViewState( + errors: List? + ): DydxTriggerOrderInputView.ViewState { + val firstError = errors?.firstOrNull { it.type == ErrorType.error } + val firstWarning = errors?.firstOrNull { it.type == ErrorType.warning } + val fieldString = firstError?.fields?.firstOrNull() ?: firstWarning?.fields?.firstOrNull() + val field: TriggerOrdersInputField? = fieldString?.let { + TriggerOrdersInputField.invoke(it) + } return DydxTriggerOrderInputView.ViewState( localizer = localizer, closeAction = { router.navigateBack() }, + backHandler = { + abacusStateManager.resetTriggerOrders() + triggerOrderStream.clearSubmissionStatus() + }, + validationErrorSection = field?.let { + when (it) { + TriggerOrdersInputField.size -> + DydxTriggerOrderInputView.ValidationErrorSection.Size + + TriggerOrdersInputField.takeProfitPrice, + TriggerOrdersInputField.takeProfitUsdcDiff, + TriggerOrdersInputField.takeProfitPercentDiff -> + DydxTriggerOrderInputView.ValidationErrorSection.TakeProfit + + TriggerOrdersInputField.stopLossPrice, + TriggerOrdersInputField.stopLossUsdcDiff, + TriggerOrdersInputField.stopLossPercentDiff -> + DydxTriggerOrderInputView.ValidationErrorSection.StopLoss + + TriggerOrdersInputField.takeProfitLimitPrice, + TriggerOrdersInputField.stopLossLimitPrice -> + DydxTriggerOrderInputView.ValidationErrorSection.LimitPrice + + else -> + DydxTriggerOrderInputView.ValidationErrorSection.None + } + } ?: DydxTriggerOrderInputView.ValidationErrorSection.None, ) } + + private fun updateAbacusTriggerOrder( + position: SubaccountPosition?, + takeProfitOrders: List?, + stopLossOrders: List?, + triggerOrdersInput: TriggerOrdersInput?, + ) { + var takeProfitOrderSize = 0.0 + if (takeProfitOrders?.size == 1) { + takeProfitOrders.first()?.let { order -> + takeProfitOrderSize = order.size + if (triggerOrdersInput?.takeProfitOrder?.orderId == null) { + abacusStateManager.triggerOrders( + order.id, + TriggerOrdersInputField.takeProfitOrderId, + ) + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(order.size), + TriggerOrdersInputField.takeProfitOrderSize, + ) + abacusStateManager.triggerOrders( + order.type.rawValue, + TriggerOrdersInputField.takeProfitOrderType, + ) + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(order.triggerPrice), + TriggerOrdersInputField.takeProfitPrice, + ) + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(order.price), + TriggerOrdersInputField.takeProfitLimitPrice, + ) + } + } + } else { + if (triggerOrdersInput?.takeProfitOrder?.type == null) { + abacusStateManager.triggerOrders( + OrderType.takeProfitMarket.rawValue, + TriggerOrdersInputField.takeProfitOrderType, + ) + } + } + + var stopLossOrderSize = 0.0 + if (stopLossOrders?.size == 1) { + stopLossOrders.first()?.let { order -> + stopLossOrderSize = order.size + if (triggerOrdersInput?.stopLossOrder?.orderId == null) { + abacusStateManager.triggerOrders( + order.id, + TriggerOrdersInputField.stopLossOrderId, + ) + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(order.size), + TriggerOrdersInputField.stopLossOrderSize, + ) + abacusStateManager.triggerOrders( + order.type.rawValue, + TriggerOrdersInputField.stopLossOrderType, + ) + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(order.triggerPrice), + TriggerOrdersInputField.stopLossPrice, + ) + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(order.price), + TriggerOrdersInputField.stopLossLimitPrice, + ) + } + } + } else { + if (triggerOrdersInput?.stopLossOrder?.type == null) { + abacusStateManager.triggerOrders( + OrderType.stopMarket.rawValue, + TriggerOrdersInputField.stopLossOrderType, + ) + } + } + + if (triggerOrdersInput?.size == null) { + if (takeProfitOrderSize == 0.0 && stopLossOrderSize == 0.0) { + // defaulting to position size + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(position?.size?.current), + TriggerOrdersInputField.size, + ) + } else if (takeProfitOrderSize > 0.0 && stopLossOrderSize > 0.0 && takeProfitOrderSize != stopLossOrderSize) { + // different order size + abacusStateManager.triggerOrders( + null, + TriggerOrdersInputField.size, + ) + } else if (takeProfitOrderSize > 0.0) { + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(takeProfitOrderSize), + TriggerOrdersInputField.size, + ) + } else if (stopLossOrderSize > 0.0) { + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(stopLossOrderSize), + TriggerOrdersInputField.size, + ) + } + } + } + + private fun subscribeToStatus() { + triggerOrderStream.submissionStatus + .filterNotNull() + .map { status -> + when (status) { + is AbacusStateManagerProtocol.SubmissionStatus.Success -> + platformInfo.show( + title = localizer.localize("trade.trigger.order.submission.success.title"), + message = localizer.localize("trade.trigger.order.submission.success.message"), + buttonTitle = localizer.localize("APP.GENERAL.BACK"), + buttonAction = { + router.navigateBack() + }, + ) + + is AbacusStateManagerProtocol.SubmissionStatus.Failed -> + platformInfo.show( + title = localizer.localize("trade.trigger.order.submission.failed.title"), + message = localizer.localize("trade.trigger.order.submission.failed.message"), + type = PlatformInfo.InfoType.Error, + ) + + else -> null + } + } + .distinctUntilChanged() + .launchIn(viewModelScope) + } } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderCtaButtonView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderCtaButtonView.kt index 9170234f..b79136e4 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderCtaButtonView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderCtaButtonView.kt @@ -2,6 +2,7 @@ package exchange.dydx.trading.feature.trade.trigger.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol @@ -56,6 +57,8 @@ object DydxTriggerOrderCtaButtonView : DydxComponent { return } + val focusManager = LocalFocusManager.current + PlatformButton( modifier = modifier, text = when (state.ctaButtonState) { @@ -74,6 +77,7 @@ object DydxTriggerOrderCtaButtonView : DydxComponent { is State.Thinking -> PlatformButtonState.Disabled }, ) { + focusManager.clearFocus() state.ctaAction() } } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderCtaButtonViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderCtaButtonViewModel.kt index 4deff984..33dcc216 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderCtaButtonViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderCtaButtonViewModel.kt @@ -2,29 +2,68 @@ package exchange.dydx.trading.feature.trade.trigger.components import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.TriggerOrdersInput +import exchange.dydx.abacus.output.input.ValidationError 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.trade.streams.MutableTriggerOrderStreaming import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject @HiltViewModel class DydxTriggerOrderCtaButtonViewModel @Inject constructor( private val localizer: LocalizerProtocol, private val abacusStateManager: AbacusStateManagerProtocol, - private val formatter: DydxFormatter, + private val triggerOrderStream: MutableTriggerOrderStreaming, + ) : ViewModel(), DydxViewModel { - val state: Flow = flowOf(createViewState()) + val state: Flow = + combine( + triggerOrderStream.isNewTriggerOrder, + abacusStateManager.state.triggerOrdersInput, + abacusStateManager.state.validationErrors, + ) { isNewTriggerOrder, triggerOrdersInput, errors -> + createViewState(isNewTriggerOrder, triggerOrdersInput, errors) + } + .distinctUntilChanged() - private fun createViewState(): DydxTriggerOrderCtaButtonView.ViewState { + private fun createViewState( + isNewTriggerOrder: Boolean, + triggerOrdersInput: TriggerOrdersInput?, + errors: List, + ): DydxTriggerOrderCtaButtonView.ViewState { + val firstBlockingError = + errors.firstOrNull { it.type == ErrorType.required || it.type == ErrorType.error } + val buttonTitle = firstBlockingError?.resources?.action?.localized + ?: if (isNewTriggerOrder) { + localizer.localize("APP.TRADE.ADD_TRIGGERS") + } else { + localizer.localize("APP.TRADE.EDIT_TRIGGERS") + } return DydxTriggerOrderCtaButtonView.ViewState( localizer = localizer, - ctaButtonState = DydxTriggerOrderCtaButtonView.State.Disabled(), + ctaButtonState = if ( + ( + triggerOrdersInput?.takeProfitOrder?.price?.triggerPrice != null || triggerOrdersInput?.takeProfitOrder?.orderId != null || + triggerOrdersInput?.stopLossOrder?.price?.triggerPrice != null || triggerOrdersInput?.stopLossOrder?.orderId != null + ) && + triggerOrdersInput?.size ?: 0.0 > 0.0 && + firstBlockingError == null + ) { + DydxTriggerOrderCtaButtonView.State.Enabled(buttonTitle) + } else { + DydxTriggerOrderCtaButtonView.State.Disabled(buttonTitle) + }, ctaAction = { - // TODO: Implement + triggerOrderStream.updatesubmissionStatus(null) + abacusStateManager.commitTriggerOrders { status -> + triggerOrderStream.updatesubmissionStatus(status) + } }, ) } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderReceiptViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderReceiptViewModel.kt index 1e985f63..65582b6b 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderReceiptViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/DydxTriggerOrderReceiptViewModel.kt @@ -2,12 +2,19 @@ package exchange.dydx.trading.feature.trade.trigger.components import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.PerpetualMarket +import exchange.dydx.abacus.output.SubaccountPosition import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.dydxstatemanager.MarketConfigsAndAsset import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.formatter.DydxFormatter import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapNotNull import javax.inject.Inject @HiltViewModel @@ -17,11 +24,44 @@ class DydxTriggerOrderReceiptViewModel @Inject constructor( private val formatter: DydxFormatter, ) : ViewModel(), DydxViewModel { - val state: Flow = flowOf(createViewState()) + private val marketIdFlow = abacusStateManager.state.triggerOrdersInput + .mapNotNull { it?.marketId } - private fun createViewState(): DydxTriggerOrderReceiptView.ViewState { + val state: Flow = + combine( + marketIdFlow + .flatMapLatest { + abacusStateManager.state.market(marketId = it) + } + .filterNotNull() + .distinctUntilChanged(), + abacusStateManager.state.configsAndAssetMap, + marketIdFlow + .flatMapLatest { marketId -> + abacusStateManager.state.selectedSubaccountPositionOfMarket(marketId) + } + .filterNotNull() + .distinctUntilChanged(), + ) { market, configsAndAssetMap, position -> + createViewState(market, configsAndAssetMap?.get(market.id), position) + } + .distinctUntilChanged() + + private fun createViewState( + market: PerpetualMarket, + configsAndAsset: MarketConfigsAndAsset?, + position: SubaccountPosition, + ): DydxTriggerOrderReceiptView.ViewState { return DydxTriggerOrderReceiptView.ViewState( localizer = localizer, + entryPrice = formatter.dollar( + position.entryPrice?.current, + configsAndAsset?.configs?.tickSizeDecimals ?: 0, + ), + oraclePrice = formatter.dollar( + market.oraclePrice, + configsAndAsset?.configs?.tickSizeDecimals ?: 0, + ), ) } } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/gainloss/DydxTriggerOrderGainLossView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/gainloss/DydxTriggerOrderGainLossView.kt index fde06b0c..d95ab8a6 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/gainloss/DydxTriggerOrderGainLossView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/gainloss/DydxTriggerOrderGainLossView.kt @@ -27,7 +27,7 @@ import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledSelectionInput import exchange.dydx.trading.feature.shared.views.LabeledTextInput import exchange.dydx.trading.feature.trade.trigger.components.inputfields.DydxTriggerOrderInputType @@ -46,7 +46,7 @@ fun Preview_DydxTriggerOrderGainLossView() { object DydxTriggerOrderGainLossView : DydxComponent { data class ViewState( val localizer: LocalizerProtocol, - val labeledTextInput: LabeledTextInput.ViewState? = null, + val labeledTextInput: LabeledTextInput.ViewState, val labeledSelectionInput: LabeledSelectionInput.ViewState? = null, ) { companion object { @@ -89,7 +89,10 @@ object DydxTriggerOrderGainLossView : DydxComponent { mutableStateOf(false) } - InputFieldScarfold(modifier) { + InputFieldScaffold( + modifier = modifier, + alertState = state.labeledTextInput.alertState, + ) { Row( verticalAlignment = Alignment.CenterVertically, ) { diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/gainloss/DydxTriggerOrderGainLossViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/gainloss/DydxTriggerOrderGainLossViewModel.kt index bd9820d8..b7618a03 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/gainloss/DydxTriggerOrderGainLossViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/gainloss/DydxTriggerOrderGainLossViewModel.kt @@ -2,15 +2,22 @@ package exchange.dydx.trading.feature.trade.trigger.components.inputfields.gainl import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.TriggerOrdersInput +import exchange.dydx.abacus.output.input.TriggerPrice +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol -import exchange.dydx.abacus.state.model.TradeInputField +import exchange.dydx.abacus.state.model.TriggerOrdersInputField import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.dydxstatemanager.MarketConfigsAndAsset +import exchange.dydx.platformui.components.inputs.PlatformInputAlertState import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.feature.shared.views.LabeledSelectionInput import exchange.dydx.trading.feature.shared.views.LabeledTextInput +import exchange.dydx.trading.feature.trade.alertState +import exchange.dydx.trading.feature.trade.streams.GainLossDisplayType +import exchange.dydx.trading.feature.trade.streams.MutableTriggerOrderStreaming import exchange.dydx.trading.feature.trade.trigger.components.inputfields.DydxTriggerOrderInputType import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -22,11 +29,13 @@ class DydxTriggerOrderGainLossTakeProfitViewModel @Inject constructor( private val localizer: LocalizerProtocol, private val abacusStateManager: AbacusStateManagerProtocol, private val formatter: DydxFormatter, + private val triggerOrderStream: MutableTriggerOrderStreaming, ) : DydxTriggerOrderGainLossViewModel( localizer, abacusStateManager, formatter, DydxTriggerOrderInputType.TakeProfit, + triggerOrderStream, ) @HiltViewModel @@ -34,11 +43,13 @@ class DydxTriggerOrderGainLossStopLossViewModel @Inject constructor( private val localizer: LocalizerProtocol, private val abacusStateManager: AbacusStateManagerProtocol, private val formatter: DydxFormatter, + private val triggerOrderStream: MutableTriggerOrderStreaming, ) : DydxTriggerOrderGainLossViewModel( localizer, abacusStateManager, formatter, DydxTriggerOrderInputType.StopLoss, + triggerOrderStream, ) open class DydxTriggerOrderGainLossViewModel( @@ -46,45 +57,115 @@ open class DydxTriggerOrderGainLossViewModel( private val abacusStateManager: AbacusStateManagerProtocol, private val formatter: DydxFormatter, val inputType: DydxTriggerOrderInputType, + private val triggerOrderStream: MutableTriggerOrderStreaming, ) : ViewModel(), DydxViewModel { val state: Flow = combine( + when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> triggerOrderStream.takeProfitGainLossDisplayType + DydxTriggerOrderInputType.StopLoss -> triggerOrderStream.stopLossGainLossDisplayType + }, abacusStateManager.state.triggerOrdersInput, abacusStateManager.state.configsAndAssetMap, - ) { triggerOrdersInput, configsAndAssetMap -> + abacusStateManager.state.validationErrors, + ) { displayType, triggerOrdersInput, configsAndAssetMap, validationErrors -> val marketId = triggerOrdersInput?.marketId ?: return@combine null - createViewState(triggerOrdersInput, configsAndAssetMap?.get(marketId)) + createViewState(displayType, triggerOrdersInput, configsAndAssetMap?.get(marketId), validationErrors) } .distinctUntilChanged() private fun createViewState( + displayType: GainLossDisplayType, triggerOrdersInput: TriggerOrdersInput?, configsAndAsset: MarketConfigsAndAsset?, + validationErrors: List?, ): DydxTriggerOrderGainLossView.ViewState { - val label = when (inputType) { - DydxTriggerOrderInputType.TakeProfit -> localizer.localize("APP.GENERAL.GAIN") - DydxTriggerOrderInputType.StopLoss -> localizer.localize("APP.GENERAL.LOSS") + val marketConfigs = configsAndAsset?.configs + val tickSize = marketConfigs?.displayTickSizeDecimals ?: 0 + val firstErrorOrWarning = validationErrors?.firstOrNull { it.type == ErrorType.error } + ?: validationErrors?.firstOrNull { it.type == ErrorType.warning } + + fun formatOrder(orderPrice: TriggerPrice) = + when (displayType) { + GainLossDisplayType.Amount -> formatter.raw(orderPrice.usdcDiff, tickSize) + GainLossDisplayType.Percent -> orderPrice.percentDiff?.let { + formatter.percent( + it / 100.0, + 2, + ) + } + } + + val inputField: TriggerOrdersInputField = when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> + when (displayType) { + GainLossDisplayType.Amount -> TriggerOrdersInputField.takeProfitUsdcDiff + GainLossDisplayType.Percent -> TriggerOrdersInputField.takeProfitPercentDiff + } + + DydxTriggerOrderInputType.StopLoss -> + when (displayType) { + GainLossDisplayType.Amount -> TriggerOrdersInputField.stopLossUsdcDiff + GainLossDisplayType.Percent -> TriggerOrdersInputField.stopLossPercentDiff + } } + return DydxTriggerOrderGainLossView.ViewState( localizer = localizer, labeledTextInput = LabeledTextInput.ViewState( localizer = localizer, - label = label, - value = null, + label = when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> localizer.localize("APP.GENERAL.GAIN") + DydxTriggerOrderInputType.StopLoss -> localizer.localize("APP.GENERAL.LOSS") + }, + value = when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> triggerOrdersInput?.takeProfitOrder?.price?.let { + formatOrder(it) + } + + DydxTriggerOrderInputType.StopLoss -> triggerOrdersInput?.stopLossOrder?.price?.let { + formatOrder(it) + } + }, + alertState = when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> + if (firstErrorOrWarning?.fields?.contains(TriggerOrdersInputField.takeProfitPercentDiff.rawValue) == true || + firstErrorOrWarning?.fields?.contains(TriggerOrdersInputField.takeProfitUsdcDiff.rawValue) == true + ) { + firstErrorOrWarning.alertState + } else { + PlatformInputAlertState.None + } + + DydxTriggerOrderInputType.StopLoss -> + if (firstErrorOrWarning?.fields?.contains(TriggerOrdersInputField.stopLossPercentDiff.rawValue) == true || + firstErrorOrWarning?.fields?.contains(TriggerOrdersInputField.stopLossUsdcDiff.rawValue) == true + ) { + firstErrorOrWarning.alertState + } else { + PlatformInputAlertState.None + } + }, + placeholder = formatter.raw(0.0, tickSize), onValueChanged = { value -> - abacusStateManager.trade(value, TradeInputField.goodTilDuration) + abacusStateManager.triggerOrders(value, inputField) }, ), labeledSelectionInput = LabeledSelectionInput.ViewState( localizer = localizer, - options = listOf("%", "$"), - selectedIndex = 0, + options = GainLossDisplayType.list.map { it.value }, + selectedIndex = GainLossDisplayType.list.indexOf(displayType), onSelectionChanged = { index -> -// val type = tradeInput?.options?.goodTilUnitOptions?.getOrNull(index)?.type -// if (type != null) { -// abacusStateManager.trade(type, TradeInputField.goodTilUnit) -// } + when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> triggerOrderStream.setTakeProfitGainLossDisplayType( + GainLossDisplayType.list[index], + ) + + DydxTriggerOrderInputType.StopLoss -> triggerOrderStream.setStopLossGainLossDisplayType( + GainLossDisplayType.list[index], + ) + } }, ), ) diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/headersection/DydxTriggerSectionHeaderView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/headersection/DydxTriggerSectionHeaderView.kt new file mode 100644 index 00000000..0060e03f --- /dev/null +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/headersection/DydxTriggerSectionHeaderView.kt @@ -0,0 +1,123 @@ +package exchange.dydx.trading.feature.trade.trigger.components.inputfields.headersection + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +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.platform.LocalFocusManager +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.PlatformVerticalDivider +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle +import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface +import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.shared.views.SignedAmountView +import exchange.dydx.trading.feature.trade.trigger.components.inputfields.DydxTriggerOrderInputType + +@Preview +@Composable +fun Preview_DydxTriggerSectionHeaderView() { + DydxThemedPreviewSurface { + DydxTriggerSectionHeaderView.Content( + Modifier, + DydxTriggerSectionHeaderView.ViewState.preview, + ) + } +} + +object DydxTriggerSectionHeaderView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val label: String, + val amount: SignedAmountView.ViewState? = null, + var clearAction: (() -> Unit)? = null, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + label = "1.0M", + amount = SignedAmountView.ViewState.preview, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + Content(modifier, DydxTriggerOrderInputType.TakeProfit) + } + + @Composable + fun Content(modifier: Modifier, inputType: DydxTriggerOrderInputType) { + when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> { + val viewModel: DydxTriggerSectionHeaderTakeProfitViewModel = hiltViewModel() + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + DydxTriggerOrderInputType.StopLoss -> { + val viewModel: DydxTriggerSectionHeaderStopLossViewModel = hiltViewModel() + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + } + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null || state.amount == null) { + return + } + + val focusManager = LocalFocusManager.current + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.End, + ) { + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier.height(24.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.label, + style = TextStyle.dydxDefault.themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + + SignedAmountView.Content( + modifier = Modifier + .padding(start = 8.dp), + state = state.amount, + ) + + PlatformVerticalDivider() + + Text( + modifier = Modifier + .clickable { + focusManager.clearFocus() + state.clearAction?.invoke() + }, + text = state.localizer.localize("APP.GENERAL.CLEAR"), + style = TextStyle.dydxDefault.themeColor(ThemeColor.SemanticColor.color_red), + ) + } + } + } +} diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/headersection/DydxTriggerSectionHeaderViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/headersection/DydxTriggerSectionHeaderViewModel.kt new file mode 100644 index 00000000..f23944e4 --- /dev/null +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/headersection/DydxTriggerSectionHeaderViewModel.kt @@ -0,0 +1,143 @@ +package exchange.dydx.trading.feature.trade.trigger.components.inputfields.headersection + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.input.TriggerOrdersInput +import exchange.dydx.abacus.output.input.TriggerPrice +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.state.model.TriggerOrdersInputField +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.dydxstatemanager.MarketConfigsAndAsset +import exchange.dydx.platformui.components.PlatformUISign +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.feature.shared.views.SignedAmountView +import exchange.dydx.trading.feature.trade.streams.GainLossDisplayType +import exchange.dydx.trading.feature.trade.streams.TriggerOrderStreaming +import exchange.dydx.trading.feature.trade.trigger.components.inputfields.DydxTriggerOrderInputType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import java.lang.Math.abs +import javax.inject.Inject + +@HiltViewModel +class DydxTriggerSectionHeaderTakeProfitViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val triggerOrderStream: TriggerOrderStreaming, +) : DydxTriggerSectionHeaderViewModel( + localizer, + abacusStateManager, + formatter, + DydxTriggerOrderInputType.TakeProfit, + triggerOrderStream, +) + +@HiltViewModel +class DydxTriggerSectionHeaderStopLossViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val triggerOrderStream: TriggerOrderStreaming, +) : DydxTriggerSectionHeaderViewModel( + localizer, + abacusStateManager, + formatter, + DydxTriggerOrderInputType.StopLoss, + triggerOrderStream, +) + +open class DydxTriggerSectionHeaderViewModel( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + val inputType: DydxTriggerOrderInputType, + private val triggerOrderStream: TriggerOrderStreaming, +) : ViewModel(), DydxViewModel { + + val state: Flow = + combine( + when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> triggerOrderStream.takeProfitGainLossDisplayType + DydxTriggerOrderInputType.StopLoss -> triggerOrderStream.stopLossGainLossDisplayType + }, + abacusStateManager.state.triggerOrdersInput, + abacusStateManager.state.configsAndAssetMap, + ) { displayType, triggerOrdersInput, configsAndAssetMap -> + val marketId = triggerOrdersInput?.marketId ?: return@combine null + createViewState(displayType, triggerOrdersInput, configsAndAssetMap?.get(marketId)) + } + .distinctUntilChanged() + + private fun createViewState( + displayType: GainLossDisplayType, + triggerOrdersInput: TriggerOrdersInput?, + configsAndAsset: MarketConfigsAndAsset?, + ): DydxTriggerSectionHeaderView.ViewState { + val marketConfigs = configsAndAsset?.configs + val tickSize = marketConfigs?.displayTickSizeDecimals ?: 0 + + fun formatOrder(orderPrice: TriggerPrice) = + when (displayType) { + // showing the reverse of the display type + GainLossDisplayType.Percent -> orderPrice.usdcDiff?.let { diff -> + SignedAmountView.ViewState( + text = formatter.dollar(kotlin.math.abs(diff), tickSize), + sign = if (diff > 0.0) PlatformUISign.Plus else PlatformUISign.Minus, + coloringOption = SignedAmountView.ColoringOption.SignOnly, + ) + } + GainLossDisplayType.Amount -> orderPrice.percentDiff?.let { diff -> + SignedAmountView.ViewState( + text = formatter.percent(kotlin.math.abs(diff / 100.0), 2), + sign = if (diff > 0.0) PlatformUISign.Plus else PlatformUISign.Minus, + coloringOption = SignedAmountView.ColoringOption.SignOnly, + ) + } + } + + return DydxTriggerSectionHeaderView.ViewState( + localizer = localizer, + label = when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> localizer.localize("APP.GENERAL.GAIN") + DydxTriggerOrderInputType.StopLoss -> localizer.localize("APP.GENERAL.LOSS") + }, + amount = when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> triggerOrdersInput?.takeProfitOrder?.price?.let { + formatOrder(it) + } + DydxTriggerOrderInputType.StopLoss -> triggerOrdersInput?.stopLossOrder?.price?.let { + formatOrder(it) + } + }, + clearAction = { + when (inputType) { + DydxTriggerOrderInputType.TakeProfit -> { + val fields = listOf( + TriggerOrdersInputField.takeProfitOrderType, + TriggerOrdersInputField.takeProfitUsdcDiff, + TriggerOrdersInputField.takeProfitPercentDiff, + TriggerOrdersInputField.takeProfitPrice, + TriggerOrdersInputField.takeProfitOrderSize, + TriggerOrdersInputField.takeProfitLimitPrice, + ) + fields.forEach { abacusStateManager.triggerOrders(null, it) } + } + DydxTriggerOrderInputType.StopLoss -> { + val fields = listOf( + TriggerOrdersInputField.stopLossOrderType, + TriggerOrdersInputField.stopLossUsdcDiff, + TriggerOrdersInputField.stopLossPercentDiff, + TriggerOrdersInputField.stopLossPrice, + TriggerOrdersInputField.stopLossOrderSize, + TriggerOrdersInputField.stopLossLimitPrice, + ) + fields.forEach { abacusStateManager.triggerOrders(null, it) } + } + } + }, + ) + } +} diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/limitprice/DydxTriggerOrderLimitPriceSectionViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/limitprice/DydxTriggerOrderLimitPriceSectionViewModel.kt index 66415bda..f1315694 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/limitprice/DydxTriggerOrderLimitPriceSectionViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/limitprice/DydxTriggerOrderLimitPriceSectionViewModel.kt @@ -2,16 +2,16 @@ package exchange.dydx.trading.feature.trade.trigger.components.inputfields.limit import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import exchange.dydx.abacus.output.input.TriggerOrdersInput +import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.state.model.TriggerOrdersInputField import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol -import exchange.dydx.dydxstatemanager.MarketConfigsAndAsset import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.formatter.DydxFormatter 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 @HiltViewModel @@ -21,28 +21,49 @@ class DydxTriggerOrderLimitPriceSectionViewModel @Inject constructor( private val formatter: DydxFormatter, ) : ViewModel(), DydxViewModel { - private val enabled = MutableStateFlow(false) + private val enabledFlow = MutableStateFlow(false) val state: Flow = - combine( - enabled, - abacusStateManager.state.triggerOrdersInput, - abacusStateManager.state.configsAndAssetMap, - ) { sizeEnabled, triggerOrdersInput, configsAndAssetMap -> - val marketId = triggerOrdersInput?.marketId ?: return@combine null - createViewState(sizeEnabled, triggerOrdersInput, configsAndAssetMap?.get(marketId)) - } + enabledFlow + .map { sizeEnabled -> createViewState(sizeEnabled) } .distinctUntilChanged() private fun createViewState( sizeEnabled: Boolean, - triggerOrdersInput: TriggerOrdersInput?, - configsAndAsset: MarketConfigsAndAsset?, ): DydxTriggerOrderLimitPriceSectionView.ViewState { return DydxTriggerOrderLimitPriceSectionView.ViewState( localizer = localizer, enabled = sizeEnabled, - onEnabledChanged = { enabled.value = it }, + onEnabledChanged = { enabled -> + enabledFlow.value = enabled + if (!enabled) { + abacusStateManager.triggerOrders( + null, + TriggerOrdersInputField.takeProfitLimitPrice, + ) + abacusStateManager.triggerOrders( + null, + TriggerOrdersInputField.stopLossLimitPrice, + ) + abacusStateManager.triggerOrders( + OrderType.takeProfitMarket.rawValue, + TriggerOrdersInputField.takeProfitOrderType, + ) + abacusStateManager.triggerOrders( + OrderType.stopMarket.rawValue, + TriggerOrdersInputField.stopLossOrderType, + ) + } else { + abacusStateManager.triggerOrders( + OrderType.takeProfitLimit.rawValue, + TriggerOrdersInputField.takeProfitOrderType, + ) + abacusStateManager.triggerOrders( + OrderType.stopLimit.rawValue, + TriggerOrdersInputField.stopLossOrderType, + ) + } + }, ) } } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/price/DydxTriggerOrderPriceView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/price/DydxTriggerOrderPriceView.kt index 30c1af8b..2c8bc0ea 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/price/DydxTriggerOrderPriceView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/price/DydxTriggerOrderPriceView.kt @@ -9,7 +9,7 @@ import exchange.dydx.trading.common.component.DydxComponent import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledTextInput import exchange.dydx.trading.feature.trade.trigger.components.inputfields.DydxTriggerOrderPriceInputType @@ -72,7 +72,10 @@ object DydxTriggerOrderPriceView : DydxComponent { return } - InputFieldScarfold(modifier) { + InputFieldScaffold( + modifier = modifier, + alertState = state.labeledTextInput.alertState, + ) { LabeledTextInput.Content( modifier = Modifier, state = state.labeledTextInput, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/price/DydxTriggerOrderPriceViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/price/DydxTriggerOrderPriceViewModel.kt index 2da80069..1f58c022 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/price/DydxTriggerOrderPriceViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/price/DydxTriggerOrderPriceViewModel.kt @@ -2,13 +2,18 @@ package exchange.dydx.trading.feature.trade.trigger.components.inputfields.price import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.TriggerOrdersInput +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.state.model.TriggerOrdersInputField import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.dydxstatemanager.MarketConfigsAndAsset +import exchange.dydx.platformui.components.inputs.PlatformInputAlertState import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.feature.shared.views.LabeledTextInput +import exchange.dydx.trading.feature.trade.alertState import exchange.dydx.trading.feature.trade.trigger.components.inputfields.DydxTriggerOrderPriceInputType import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -74,41 +79,95 @@ open class DydxTriggerOrderPriceViewModel( combine( abacusStateManager.state.triggerOrdersInput, abacusStateManager.state.configsAndAssetMap, - ) { triggerOrdersInput, configsAndAssetMap -> + abacusStateManager.state.validationErrors, + ) { triggerOrdersInput, configsAndAssetMap, validationErrors -> val marketId = triggerOrdersInput?.marketId ?: return@combine null - createViewState(triggerOrdersInput, configsAndAssetMap?.get(marketId)) + createViewState(triggerOrdersInput, configsAndAssetMap?.get(marketId), validationErrors) } .distinctUntilChanged() private fun createViewState( triggerOrdersInput: TriggerOrdersInput?, configsAndAsset: MarketConfigsAndAsset?, + validationErrors: List?, ): DydxTriggerOrderPriceView.ViewState { val marketConfigs = configsAndAsset?.configs - val value = when (inputType) { - DydxTriggerOrderPriceInputType.TakeProfit -> null - DydxTriggerOrderPriceInputType.StopLoss -> null - DydxTriggerOrderPriceInputType.TakeProfitLimit -> null - DydxTriggerOrderPriceInputType.StopLossLimit -> null - } - val label = when (inputType) { - DydxTriggerOrderPriceInputType.TakeProfit -> localizer.localize("APP.TRIGGERS_MODAL.TP_PRICE") - DydxTriggerOrderPriceInputType.StopLoss -> localizer.localize("APP.TRIGGERS_MODAL.SL_PRICE") - DydxTriggerOrderPriceInputType.TakeProfitLimit -> localizer.localize("APP.TRIGGERS_MODAL.TP_LIMIT") - DydxTriggerOrderPriceInputType.StopLossLimit -> localizer.localize("APP.TRIGGERS_MODAL.SL_LIMIT") - } - return DydxTriggerOrderPriceView.ViewState( + val tickSize = marketConfigs?.displayTickSizeDecimals ?: 0 + val firstErrorOrWarning = validationErrors?.firstOrNull { it.type == ErrorType.error } + ?: validationErrors?.firstOrNull { it.type == ErrorType.warning } + + val state = DydxTriggerOrderPriceView.ViewState( localizer = localizer, labeledTextInput = LabeledTextInput.ViewState( localizer = localizer, - label = label, + label = when (inputType) { + DydxTriggerOrderPriceInputType.TakeProfit -> localizer.localize("APP.TRIGGERS_MODAL.TP_PRICE") + DydxTriggerOrderPriceInputType.StopLoss -> localizer.localize("APP.TRIGGERS_MODAL.SL_PRICE") + DydxTriggerOrderPriceInputType.TakeProfitLimit -> localizer.localize("APP.TRIGGERS_MODAL.TP_LIMIT") + DydxTriggerOrderPriceInputType.StopLossLimit -> localizer.localize("APP.TRIGGERS_MODAL.SL_LIMIT") + }, token = "USD", - value = value, - placeholder = formatter.raw(0.0, marketConfigs?.displayTickSizeDecimals ?: 0), + value = when (inputType) { + DydxTriggerOrderPriceInputType.TakeProfit -> formatter.raw( + triggerOrdersInput?.takeProfitOrder?.price?.triggerPrice, + tickSize, + ) + + DydxTriggerOrderPriceInputType.StopLoss -> formatter.raw( + triggerOrdersInput?.stopLossOrder?.price?.triggerPrice, + tickSize, + ) + + DydxTriggerOrderPriceInputType.TakeProfitLimit -> formatter.raw( + triggerOrdersInput?.takeProfitOrder?.price?.limitPrice, + tickSize, + ) + + DydxTriggerOrderPriceInputType.StopLossLimit -> formatter.raw( + triggerOrdersInput?.stopLossOrder?.price?.limitPrice, + tickSize, + ) + }, + alertState = if (firstErrorOrWarning?.fields?.contains(inputType.abacusInputField.rawValue) == true) { + firstErrorOrWarning.alertState + } else { + PlatformInputAlertState.None + }, + placeholder = formatter.raw(0.0, tickSize), onValueChanged = { value -> - // abacusStateManager.trade(value, TradeInputField.limitPrice) + when (inputType) { + DydxTriggerOrderPriceInputType.TakeProfit -> abacusStateManager.triggerOrders( + value, + TriggerOrdersInputField.takeProfitPrice, + ) + + DydxTriggerOrderPriceInputType.StopLoss -> abacusStateManager.triggerOrders( + value, + TriggerOrdersInputField.stopLossPrice, + ) + + DydxTriggerOrderPriceInputType.TakeProfitLimit -> abacusStateManager.triggerOrders( + value, + TriggerOrdersInputField.takeProfitLimitPrice, + ) + + DydxTriggerOrderPriceInputType.StopLossLimit -> abacusStateManager.triggerOrders( + value, + TriggerOrdersInputField.stopLossLimitPrice, + ) + } }, ), ) + + return state } } + +val DydxTriggerOrderPriceInputType.abacusInputField: TriggerOrdersInputField + get() = when (this) { + DydxTriggerOrderPriceInputType.TakeProfit -> TriggerOrdersInputField.takeProfitPrice + DydxTriggerOrderPriceInputType.StopLoss -> TriggerOrdersInputField.stopLossPrice + DydxTriggerOrderPriceInputType.TakeProfitLimit -> TriggerOrdersInputField.takeProfitLimitPrice + DydxTriggerOrderPriceInputType.StopLossLimit -> TriggerOrdersInputField.stopLossLimitPrice + } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/size/DydxTriggerOrderSizeView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/size/DydxTriggerOrderSizeView.kt index 606d2864..ae6ccfbb 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/size/DydxTriggerOrderSizeView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/size/DydxTriggerOrderSizeView.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -32,7 +33,7 @@ import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.navigation.DydxAnimation import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.LabeledTextInput @Preview @@ -49,8 +50,9 @@ object DydxTriggerOrderSizeView : DydxComponent { val enabled: Boolean = true, val onEnabledChanged: (Boolean) -> Unit = {}, val labeledTextInput: LabeledTextInput.ViewState, - val percentage: Float = 40.0f, - val onPercentageChanged: (Float) -> Unit = {}, + val percentage: Double = 0.4, + val onPercentageChanged: (Double) -> Unit = {}, + val canEdit: Boolean = true, ) { companion object { val preview = ViewState( @@ -74,6 +76,8 @@ object DydxTriggerOrderSizeView : DydxComponent { return } + val focusManager = LocalFocusManager.current + Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(12.dp), @@ -85,7 +89,13 @@ object DydxTriggerOrderSizeView : DydxComponent { .themeFont(fontSize = ThemeFont.FontSize.base) .themeColor(ThemeColor.SemanticColor.text_secondary), value = state.enabled, - onValueChange = state.onEnabledChanged, + onValueChange = { + if (state.canEdit) { + focusManager.clearFocus() + state.onEnabledChanged(it) + } + }, + canEdit = state.canEdit, ) DydxAnimation.AnimateExpandInOut( @@ -104,9 +114,11 @@ object DydxTriggerOrderSizeView : DydxComponent { verticalAlignment = Alignment.CenterVertically, ) { CustomSlider( - value = state.percentage, - onValueChange = state.onPercentageChanged, - valueRange = 0.0f..100.0f, + value = state.percentage.toFloat(), + onValueChange = { + state.onPercentageChanged(it.toDouble()) + }, + valueRange = 0.0f..1.0f, modifier = modifier.weight(1f), showLabel = false, showIndicator = false, @@ -128,8 +140,9 @@ object DydxTriggerOrderSizeView : DydxComponent { }, ) - InputFieldScarfold( + InputFieldScaffold( modifier = Modifier.width(120.dp), + alertState = state.labeledTextInput.alertState, ) { LabeledTextInput.Content( modifier = Modifier, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/size/DydxTriggerOrderSizeViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/size/DydxTriggerOrderSizeViewModel.kt index b68c61f0..807f7445 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/size/DydxTriggerOrderSizeViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/trigger/components/inputfields/size/DydxTriggerOrderSizeViewModel.kt @@ -1,18 +1,27 @@ package exchange.dydx.trading.feature.trade.trigger.components.inputfields.size import androidx.lifecycle.ViewModel +import com.hoc081098.flowext.combine import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.SubaccountPosition +import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.TriggerOrdersInput +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.state.model.TriggerOrdersInputField import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.dydxstatemanager.MarketConfigsAndAsset +import exchange.dydx.platformui.components.inputs.PlatformInputAlertState import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.feature.shared.views.LabeledTextInput +import exchange.dydx.trading.feature.trade.alertState +import exchange.dydx.trading.feature.trade.streams.MutableTriggerOrderStreaming import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapNotNull import javax.inject.Inject @HiltViewModel @@ -20,41 +29,84 @@ class DydxTriggerOrderSizeViewModel @Inject constructor( private val localizer: LocalizerProtocol, private val abacusStateManager: AbacusStateManagerProtocol, private val formatter: DydxFormatter, + private val triggerOrderStream: MutableTriggerOrderStreaming, ) : ViewModel(), DydxViewModel { - private val enabled = MutableStateFlow(false) + private val enabledFlow = MutableStateFlow(false) val state: Flow = combine( - enabled, + enabledFlow, + abacusStateManager.state.triggerOrdersInput + .mapNotNull { it?.marketId } + .flatMapLatest { + abacusStateManager.state.selectedSubaccountPositionOfMarket(it) + }, + triggerOrderStream.isNewTriggerOrder, abacusStateManager.state.triggerOrdersInput, abacusStateManager.state.configsAndAssetMap, - ) { sizeEnabled, triggerOrdersInput, configsAndAssetMap -> + abacusStateManager.state.validationErrors, + ) { sizeEnabled, position, isNewTriggerOrder, triggerOrdersInput, configsAndAssetMap, validationErrors -> val marketId = triggerOrdersInput?.marketId ?: return@combine null - createViewState(sizeEnabled, triggerOrdersInput, configsAndAssetMap?.get(marketId)) + createViewState(sizeEnabled, position, isNewTriggerOrder, triggerOrdersInput, configsAndAssetMap?.get(marketId), validationErrors) } .distinctUntilChanged() private fun createViewState( sizeEnabled: Boolean, + position: SubaccountPosition?, + isNewTriggerOrder: Boolean, triggerOrdersInput: TriggerOrdersInput?, configsAndAsset: MarketConfigsAndAsset?, + validationErrors: List?, ): DydxTriggerOrderSizeView.ViewState { val marketConfigs = configsAndAsset?.configs + val stepSize = marketConfigs?.stepSize + val size = triggerOrdersInput?.size ?: 0.0 + val positionSize = position?.size?.current ?: 0.0 + val percentage = if (positionSize > 0.0) { + size / positionSize + } else { + 0.0 + } + val firstErrorOrWarning = validationErrors?.firstOrNull { it.type == ErrorType.error } + ?: validationErrors?.firstOrNull { it.type == ErrorType.warning } + return DydxTriggerOrderSizeView.ViewState( localizer = localizer, - enabled = sizeEnabled, - onEnabledChanged = { enabled.value = it }, + enabled = sizeEnabled && isNewTriggerOrder, + onEnabledChanged = { enabled -> + enabledFlow.value = enabled + if (!enabled) { + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(position?.size?.current, size = stepSize), + TriggerOrdersInputField.size, + ) + } + }, labeledTextInput = LabeledTextInput.ViewState( localizer = localizer, label = localizer.localize("APP.GENERAL.AMOUNT"), - token = configsAndAsset?.asset?.name, - value = null, - placeholder = formatter.raw(0.0, marketConfigs?.displayStepSizeDecimals ?: 0), + token = configsAndAsset?.asset?.id, + value = formatter.decimalLocaleAgnostic(size, size = stepSize), + alertState = if (firstErrorOrWarning?.fields?.contains(TriggerOrdersInputField.size.rawValue) == true) { + firstErrorOrWarning.alertState + } else { + PlatformInputAlertState.None + }, + placeholder = formatter.raw(0.0, size = stepSize), onValueChanged = { value -> - // abacusStateManager.trade(value, TradeInputField.limitPrice) + abacusStateManager.triggerOrders(value, TriggerOrdersInputField.size) }, ), + percentage = percentage, + onPercentageChanged = { percentage -> + abacusStateManager.triggerOrders( + formatter.decimalLocaleAgnostic(positionSize * percentage, size = stepSize), + TriggerOrdersInputField.size, + ) + }, + canEdit = isNewTriggerOrder, ) } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/DydxTransferView.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/DydxTransferView.kt index ce9f714b..ad224162 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/DydxTransferView.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/DydxTransferView.kt @@ -58,7 +58,7 @@ object DydxTransferView : DydxComponent { val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value PlatformInfoScaffold(modifier = modifier, platformInfo = viewModel.platformInfo) { - Content(modifier, state) + Content(it, state) } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/AddressInputBox.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/AddressInputBox.kt index 91f1bb84..c95faf58 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/AddressInputBox.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/AddressInputBox.kt @@ -23,7 +23,7 @@ import exchange.dydx.platformui.designSystem.theme.themeFont import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold @Preview @Composable @@ -57,7 +57,7 @@ object AddressInputBox { if (state == null) { return } - InputFieldScarfold(modifier) { + InputFieldScaffold(modifier) { Column( modifier = modifier .padding(horizontal = ThemeShapes.HorizontalPadding) diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/ChainsComboBox.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/ChainsComboBox.kt index 2c783de8..b066a13c 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/ChainsComboBox.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/ChainsComboBox.kt @@ -27,7 +27,7 @@ import exchange.dydx.platformui.designSystem.theme.themeFont import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer import exchange.dydx.trading.feature.shared.R -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold @Preview @Composable @@ -61,7 +61,7 @@ object ChainsComboBox { return } - InputFieldScarfold( + InputFieldScaffold( modifier = modifier.then( state.onTapAction?.let { Modifier.clickable(onClick = it) diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/TokensComboBox.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/TokensComboBox.kt index 25613bc2..602bb98b 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/TokensComboBox.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/TokensComboBox.kt @@ -27,7 +27,7 @@ import exchange.dydx.platformui.designSystem.theme.themeFont import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer import exchange.dydx.trading.feature.shared.R -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.TokenTextView @Preview @@ -64,7 +64,7 @@ object TokensComboBox { return } - InputFieldScarfold( + InputFieldScaffold( modifier = modifier.then( state.onTapAction?.let { Modifier.clickable(onClick = it) diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/TransferAmountBox.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/TransferAmountBox.kt index c45253ec..4ebac1f2 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/TransferAmountBox.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/components/TransferAmountBox.kt @@ -8,7 +8,6 @@ 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.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Text @@ -36,7 +35,7 @@ import exchange.dydx.platformui.designSystem.theme.themeFont import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer -import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold +import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold import exchange.dydx.trading.feature.shared.views.SizeTextView import exchange.dydx.trading.feature.shared.views.TokenTextView import java.lang.Double.max @@ -91,7 +90,7 @@ object TransferAmountBox { } Column { - InputFieldScarfold(modifier.zIndex(1f)) { + InputFieldScaffold(modifier.zIndex(1f)) { TopContent(modifier, state) } val shape = RoundedCornerShape(0.dp, 0.dp, 8.dp, 8.dp) diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/faucet/DydxTransferFaucetView.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/faucet/DydxTransferFaucetView.kt index aa65c0dd..360d3574 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/faucet/DydxTransferFaucetView.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/faucet/DydxTransferFaucetView.kt @@ -50,7 +50,7 @@ object DydxTransferFaucetView : DydxComponent { modifier = modifier, platformInfo = viewModel.platformInfo, ) { - Content(modifier, state) + Content(it, state) } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/status/DydxTransferStatusView.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/status/DydxTransferStatusView.kt index 5388c99e..747fbdbe 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/status/DydxTransferStatusView.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/status/DydxTransferStatusView.kt @@ -71,7 +71,7 @@ object DydxTransferStatusView : DydxComponent { modifier = modifier, platformInfo = viewModel.platformInfo, ) { - Content(modifier, state) + Content(it, state) } } diff --git a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusState.kt b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusState.kt index b7a0c487..05c1bbc6 100644 --- a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusState.kt +++ b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusState.kt @@ -17,6 +17,7 @@ import exchange.dydx.abacus.output.Notification import exchange.dydx.abacus.output.PerpetualMarket import exchange.dydx.abacus.output.PerpetualMarketSummary import exchange.dydx.abacus.output.PerpetualState +import exchange.dydx.abacus.output.PositionSide import exchange.dydx.abacus.output.Restriction import exchange.dydx.abacus.output.Subaccount import exchange.dydx.abacus.output.SubaccountFill @@ -48,6 +49,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -203,6 +205,20 @@ class AbacusState( .stateIn(stateManagerScope, SharingStarted.Lazily, null) } + fun selectedSubaccountPositionOfMarket(marketId: String): StateFlow { + return selectedSubaccountPositions + .map { positions -> + positions?.first { position -> + position?.id == marketId && + ( + position?.side?.current == PositionSide.SHORT || + position?.side?.current == PositionSide.LONG + ) + } + } + .stateIn(stateManagerScope, SharingStarted.Lazily, null) + } + val selectedSubaccountOrders: StateFlow?> by lazy { selectedSubaccount .map { subaccount -> @@ -211,6 +227,16 @@ class AbacusState( .stateIn(stateManagerScope, SharingStarted.Lazily, null) } + fun selectedSubaccountOrdersOfMarket(marketId: String): StateFlow?> { + return selectedSubaccountOrders + .map { orders -> + orders?.filter { order -> + order?.marketId == marketId + } + } + .stateIn(stateManagerScope, SharingStarted.Lazily, null) + } + val selectedSubaccountPNLs: StateFlow?> by lazy { perpetualState .map { diff --git a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusStateManager.kt b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusStateManager.kt index f4403037..527134fc 100644 --- a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusStateManager.kt +++ b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusStateManager.kt @@ -108,6 +108,7 @@ interface AbacusStateManagerProtocol { fun commitCCTPWithdraw(callback: (Boolean, ParsingError?, Any?) -> Unit) fun triggerOrders(input: String?, type: TriggerOrdersInputField?) + fun commitTriggerOrders(callback: (SubmissionStatus) -> Unit) // extensions fun resetTransferInputFields() { @@ -115,6 +116,15 @@ interface AbacusStateManagerProtocol { transfer(null, TransferInputField.usdcSize) transfer(null, TransferInputField.type) } + + fun resetTriggerOrders() { + val fields = TriggerOrdersInputField.values().filter { + it != TriggerOrdersInputField.marketId + } + for (field in fields) { + triggerOrders(null, field) + } + } } // Temporary location, should probably make a separate dagger-qualifiers module. @@ -162,6 +172,12 @@ class AbacusStateManager @Inject constructor( appConfigs.squidVersion = AppConfigs.SquidVersion.V2 appConfigsV2.onboardingConfigs.squidVersion = OnboardingConfigs.SquidVersion.V2 + // Disable Abacus logging since it's too verbose. Enable it if you need to debug Abacus. + if (BuildConfig.DEBUG) { + appConfigs.enableLogger = false + appConfigsV2.enableLogger = false + } + if (featureFlags.isFeatureEnabled(DydxFeatureFlag.enable_abacus_v2)) { AsyncAbacusStateManagerV2( deploymentUri = deploymentUri, @@ -385,6 +401,16 @@ class AbacusStateManager @Inject constructor( asyncStateManager.triggerOrders(input, type) } + override fun commitTriggerOrders(callback: (AbacusStateManagerProtocol.SubmissionStatus) -> Unit) { + asyncStateManager.commitTriggerOrders { successful: Boolean, error: ParsingError?, _ -> + if (successful) { + callback(AbacusStateManagerProtocol.SubmissionStatus.Success) + } else { + callback(AbacusStateManagerProtocol.SubmissionStatus.Failed(error)) + } + } + } + // MARK: StateNotificationProtocol override fun apiStateChanged(apiState: ApiState?) { diff --git a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusStateTriggerOrder.kt b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusStateTriggerOrder.kt new file mode 100644 index 00000000..a70d0d4f --- /dev/null +++ b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/AbacusStateTriggerOrder.kt @@ -0,0 +1,51 @@ +package exchange.dydx.dydxstatemanager + +import exchange.dydx.abacus.output.PositionSide +import exchange.dydx.abacus.output.SubaccountOrder +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderStatus +import exchange.dydx.abacus.output.input.OrderType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +fun AbacusState.triggerOrders(marketId: String): Flow?> = + selectedSubaccountOrdersOfMarket(marketId) + .map { orders -> + orders?.filter { order -> + order.status == OrderStatus.untriggered + } + } + .distinctUntilChanged() + +fun AbacusState.takeProfitOrders(marketId: String): Flow?> = + combine( + selectedSubaccountPositionOfMarket(marketId), + triggerOrders(marketId), + ) { position, orders -> + orders?.filter { order -> + position?.side?.current?.let { currentSide -> + (order.type == OrderType.takeProfitMarket || order.type == OrderType.takeProfitLimit) && + order.side.isOppositeOf(currentSide) + } ?: false + } + } + .distinctUntilChanged() + +fun AbacusState.stopLossOrders(marketId: String): Flow?> = + combine( + selectedSubaccountPositionOfMarket(marketId), + triggerOrders(marketId), + ) { position, orders -> + orders?.filter { order -> + position?.side?.current?.let { currentSide -> + (order.type == OrderType.stopMarket || order.type == OrderType.stopLimit) && + order.side.isOppositeOf(currentSide) + } ?: false + } + } + .distinctUntilChanged() + +private fun OrderSide.isOppositeOf(that: PositionSide): Boolean = + (this == OrderSide.buy && that == PositionSide.SHORT) || (this == OrderSide.sell && that == PositionSide.LONG) diff --git a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusLocalizerImp.kt b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusLocalizerImp.kt index f0d55186..3c888f08 100644 --- a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusLocalizerImp.kt +++ b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusLocalizerImp.kt @@ -37,7 +37,8 @@ class AbacusLocalizerImp @Inject constructor( } } - private val localizer = UIImplementationsExtensions.shared?.localizer as? DynamicLocalizer + private val localizer: DynamicLocalizer? + get() = UIImplementationsExtensions.shared?.localizer as? DynamicLocalizer override val languages: List get() { diff --git a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusThreadingImp.kt b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusThreadingImp.kt index 4a82d033..949cebdd 100644 --- a/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusThreadingImp.kt +++ b/v4/integration/dydxStateManager/src/main/java/exchange/dydx/dydxstatemanager/protocolImplementations/AbacusThreadingImp.kt @@ -22,12 +22,32 @@ class AbacusThreadingImp @Inject constructor( @CoroutineDispatchers.IO private val ioDispatcher: CoroutineDispatcher, @CoroutineDispatchers.Default private val defaultDispatcher: CoroutineDispatcher, ) : ThreadingProtocol { + + private val mainScope = appScope + + // Abacus runs lots of computations, but needs to be run without parallelism + private val abacusScope = appScope + defaultDispatcher.limitedParallelism(1) + private val networkScope = appScope + ioDispatcher + override fun async(type: ThreadingType, block: () -> Unit) { when (type) { - main -> appScope.launch { block() } - // Abacus runs lots of computations, but needs to be run without parallelism - abacus -> appScope.launch(defaultDispatcher.limitedParallelism(1)) { block() } - network -> appScope.launch(ioDispatcher) { block() } + main -> + mainScope + .launch { + block() + } + + abacus -> + abacusScope + .launch { + block() + } + + network -> + networkScope + .launch { + block() + } } } } diff --git a/v4/integration/web3/src/test/java/exchange/dydx/web3/ExampleUnitTest.kt b/v4/integration/web3/src/test/java/exchange/dydx/web3/ExampleUnitTest.kt index 8ba222f8..396389c0 100644 --- a/v4/integration/web3/src/test/java/exchange/dydx/web3/ExampleUnitTest.kt +++ b/v4/integration/web3/src/test/java/exchange/dydx/web3/ExampleUnitTest.kt @@ -18,11 +18,12 @@ class ExampleUnitTest { @Test fun eth_interactor() { val waiter = Waiter() - val interactor = EthereumInteractor("https://ethereum-goerli.publicnode.com") + val interactor = EthereumInteractor("https://ethereum-sepolia-rpc.publicnode.com") interactor.netVersion { error, networkVersion -> println("error: $error") println("networkVersion: $networkVersion") + waiter.resume() } - waiter.await(1000) + waiter.await(5000) } } diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/OnLifecycleEvent.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/OnLifecycleEvent.kt new file mode 100644 index 00000000..8af0aa3b --- /dev/null +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/OnLifecycleEvent.kt @@ -0,0 +1,27 @@ +package exchange.dydx.platformui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner + +@Composable +fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) { + val eventHandler = rememberUpdatedState(onEvent) + val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current) + + DisposableEffect(lifecycleOwner.value) { + val lifecycle = lifecycleOwner.value.lifecycle + val observer = LifecycleEventObserver { owner, event -> + eventHandler.value(owner, event) + } + + lifecycle.addObserver(observer) + onDispose { + lifecycle.removeObserver(observer) + } + } +} diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/PlatformInfo.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/PlatformInfo.kt index 9c1bd30b..372eb413 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/PlatformInfo.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/PlatformInfo.kt @@ -22,7 +22,7 @@ import kotlinx.serialization.json.JsonNull.content @Composable fun PlatformInfoScaffold( - modifier: Modifier, + modifier: Modifier = Modifier, platformInfo: PlatformInfo, content: @Composable (Modifier) -> Unit, ) { diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/buttons/PlatformButton.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/buttons/PlatformButton.kt index 75dc8aa3..a836a6a9 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/buttons/PlatformButton.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/buttons/PlatformButton.kt @@ -3,7 +3,7 @@ package exchange.dydx.platformui.components.buttons import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonDefaults import androidx.compose.material.OutlinedButton @@ -85,7 +85,7 @@ fun PlatformButton( } OutlinedButton( - modifier = modifier.height(52.dp), + modifier = modifier.defaultMinSize(minHeight = 52.dp), border = BorderStroke(1.dp, borderColor), shape = RoundedCornerShape(size = 8.dp), colors = ButtonDefaults @@ -102,6 +102,7 @@ fun PlatformButton( style = TextStyle.dydxDefault.themeFont(fontSize = fontSize) .themeColor(foreground = textColor), text = text, + maxLines = 1, ) } if (trailingContent != null) { diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/config/ChartConfig.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/config/ChartConfig.kt index 31f5f534..7879a800 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/config/ChartConfig.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/config/ChartConfig.kt @@ -34,7 +34,19 @@ data class InteractionConfig( val highlight: Boolean = true, val highlightDistance: Float = 500.0f, val selectionListener: OnChartValueSelectedListener? = null, -) +) { + val touchEnabled = pan || doubleTap || zoom || highlight + + companion object { + val default = InteractionConfig() + val noTouch = InteractionConfig( + pan = false, + doubleTap = false, + zoom = false, + highlight = false, + ) + } +} data class LineChartDrawingConfig( val lineWidth: Float = 1.0f, @@ -99,7 +111,7 @@ data class LineChartConfig( return LineChartConfig( lineDrawing = LineChartDrawingConfig(), drawing = DrawingConfig(), - interaction = InteractionConfig(), + interaction = InteractionConfig.default, xAxis = AxisConfig(), leftAxis = null, rightAxis = null, @@ -125,7 +137,7 @@ data class CombinedChartConfig( barDrawing = BarDrawingConfig(), lineDrawing = LineChartDrawingConfig(), drawing = DrawingConfig(), - interaction = InteractionConfig(), + interaction = InteractionConfig.default, xAxis = AxisConfig(), leftAxis = AxisConfig(), rightAxis = AxisConfig(), diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Bar.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Bar.kt index b9d3ea68..0a321db5 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Bar.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Bar.kt @@ -20,6 +20,7 @@ fun BarChart.config(config: IBarChartConfig) { } private fun BarChart.configInteraction(interaction: InteractionConfig) { + setTouchEnabled(interaction.touchEnabled) isDragEnabled = interaction.pan isDoubleTapToZoomEnabled = interaction.doubleTap isScaleXEnabled = interaction.zoom diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Candles.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Candles.kt index 3ff9ce47..f1bfddd8 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Candles.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Candles.kt @@ -22,6 +22,7 @@ fun CandleStickChartView.config(config: IBarChartConfig) { } private fun CandleStickChartView.configInteraction(interaction: InteractionConfig) { + setTouchEnabled(interaction.touchEnabled) isDragEnabled = interaction.pan isDoubleTapToZoomEnabled = interaction.doubleTap isScaleXEnabled = interaction.zoom diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Combined.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Combined.kt index f39fdfd7..537c6dc0 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Combined.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Combined.kt @@ -31,6 +31,7 @@ fun CombinedChart.config(config: ICombinedChartConfig) { } private fun CombinedChart.configInteraction(interaction: InteractionConfig) { + setTouchEnabled(interaction.touchEnabled) isDragEnabled = interaction.pan isDoubleTapToZoomEnabled = interaction.doubleTap isScaleXEnabled = interaction.zoom diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Line.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Line.kt index 5a751fb6..2b122f64 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Line.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/charts/view/Line.kt @@ -27,6 +27,7 @@ fun LineChartView.config(config: ILineChartConfig) { } private fun LineChart.configInteraction(interaction: InteractionConfig) { + setTouchEnabled(interaction.touchEnabled) isDragEnabled = interaction.pan isDoubleTapToZoomEnabled = interaction.doubleTap isScaleXEnabled = interaction.zoom diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/dividers/PlatformDivider.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/dividers/PlatformDivider.kt index 20e5bd2d..49cdcfbe 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/dividers/PlatformDivider.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/dividers/PlatformDivider.kt @@ -22,7 +22,7 @@ fun PlatformDivider(modifier: Modifier = Modifier) { @Composable fun PlatformVerticalDivider(modifier: Modifier = Modifier) { PlatformDivider( - modifier = Modifier + modifier = modifier .fillMaxHeight() // fill the max height .width(1.dp), ) diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformInputAlertState.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformInputAlertState.kt new file mode 100644 index 00000000..f185dccc --- /dev/null +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformInputAlertState.kt @@ -0,0 +1,23 @@ +package exchange.dydx.platformui.components.inputs + +import exchange.dydx.platformui.designSystem.theme.ThemeColor + +enum class PlatformInputAlertState { + None, + Error, + Warning; + + val borderColor: ThemeColor.SemanticColor + get() = when (this) { + None -> ThemeColor.SemanticColor.layer_6 + Error -> ThemeColor.SemanticColor.color_red + Warning -> ThemeColor.SemanticColor.color_yellow + } + + val textColor: ThemeColor.SemanticColor + get() = when (this) { + None -> ThemeColor.SemanticColor.text_primary + Error -> ThemeColor.SemanticColor.color_red + Warning -> ThemeColor.SemanticColor.color_yellow + } +} diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformSwitchInput.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformSwitchInput.kt index d2b37d3b..6632ebf7 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformSwitchInput.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformSwitchInput.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.text.TextStyle import exchange.dydx.platformui.designSystem.theme.ThemeColor import exchange.dydx.platformui.designSystem.theme.ThemeFont @@ -24,20 +25,23 @@ fun PlatformSwitchInput( .themeFont(fontSize = ThemeFont.FontSize.small), value: Boolean? = null, onValueChange: (Boolean) -> Unit = {}, + canEdit: Boolean = true, ) { + val alpha = if (canEdit) 1f else 0.4f Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, ) { if (label != null) { Text( - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).alpha(alpha), text = label, style = textStyle, ) } Switch( + modifier = Modifier.alpha(alpha), checked = value ?: false, onCheckedChange = onValueChange, colors = SwitchDefaults.colors( diff --git a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformTextInput.kt b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformTextInput.kt index bcdc5f4f..31b83b62 100644 --- a/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformTextInput.kt +++ b/v4/platformUI/src/main/java/exchange/dydx/platformui/components/inputs/PlatformTextInput.kt @@ -43,6 +43,7 @@ fun PlatformTextInput( fontSize = ThemeFont.FontSize.medium, fontType = ThemeFont.FontType.number, ), + alertState: PlatformInputAlertState = PlatformInputAlertState.None, placeHolder: String? = null, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, onValueChange: (String) -> Unit = {}, @@ -64,6 +65,7 @@ fun PlatformTextInput( currentValue.value = value } val displayValue = if (isFocused) currentValue.value ?: "" else value ?: "" + val textColor = if (isFocused) ThemeColor.SemanticColor.text_primary else alertState.textColor BasicTextField( modifier = Modifier.focusRequester(focusRequester = focusRequester), @@ -76,7 +78,7 @@ fun PlatformTextInput( keyboardOptions = keyboardOptions, interactionSource = interactionSource, textStyle = textStyle - .themeColor(ThemeColor.SemanticColor.text_primary), + .themeColor(textColor), cursorBrush = SolidColor(ThemeColor.SemanticColor.text_primary.color), decorationBox = { innerTextField -> Row(modifier = Modifier.fillMaxWidth()) { diff --git a/v4/utilities/src/main/java/exchange/dydx/utilities/utils/NumberUtils.kt b/v4/utilities/src/main/java/exchange/dydx/utilities/utils/NumberUtils.kt new file mode 100644 index 00000000..c50a6b63 --- /dev/null +++ b/v4/utilities/src/main/java/exchange/dydx/utilities/utils/NumberUtils.kt @@ -0,0 +1,11 @@ +package exchange.dydx.utilities.utils + +fun Double.rounded(toPlaces: Int): Double { + if (toPlaces < 0) throw IllegalArgumentException() + val factor = Math.pow(10.0, toPlaces.toDouble()) + return Math.round(this * factor) / factor +} + +fun Float.rounded(toPlaces: Int): Float { + return this.toDouble().rounded(toPlaces).toFloat() +} diff --git a/v4/utilities/src/test/java/exchange/dydx/utilities/NumberUtilsTests.kt b/v4/utilities/src/test/java/exchange/dydx/utilities/NumberUtilsTests.kt new file mode 100644 index 00000000..f5684875 --- /dev/null +++ b/v4/utilities/src/test/java/exchange/dydx/utilities/NumberUtilsTests.kt @@ -0,0 +1,26 @@ +package exchange.dydx.utilities + +import exchange.dydx.utilities.utils.rounded +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class NumberUtilsTests { + @Test + fun testRounded() { + assertEquals(1.0, 1.0.rounded(0)) + assertEquals(1.0, 1.1.rounded(0)) + assertEquals(2.0, 1.5.rounded(0)) + assertEquals(1.1, 1.1.rounded(1)) + assertEquals(1.12, 1.123.rounded(2)) + assertEquals(1.14, 1.138.rounded(2)) + assertEquals(1.123, 1.123.rounded(3)) + assertEquals(-1.12, (-1.123).rounded(2)) + assertEquals(-1.13, (-1.129).rounded(2)) + try { + 1.0.rounded(-1) + assert(false) + } catch (e: IllegalArgumentException) { + assert(true) + } + } +}