diff --git a/app/src/main/java/one/mixin/android/extension/UrlExtension.kt b/app/src/main/java/one/mixin/android/extension/UrlExtension.kt index 485a60573d..426bef7a48 100644 --- a/app/src/main/java/one/mixin/android/extension/UrlExtension.kt +++ b/app/src/main/java/one/mixin/android/extension/UrlExtension.kt @@ -385,7 +385,7 @@ internal data class PerpsTradeAction( ) internal data class PerpsOpenPositionAction( - val isLong: Boolean, + val isLong: Boolean?, val leverage: Int?, val margin: String?, ) @@ -416,16 +416,15 @@ internal fun String.toPerpsTradeAction(): PerpsTradeAction? { ?: return if (!hasLeaderPositionId) PerpsTradeAction(null, null) else null if (!marketId.isUUID()) return null - val isLong = if (query.queryParameter("action").equals("open", true)) { - when { + val isOpenAction = query.queryParameter("action").equals("open", true) + val openPosition = if (isOpenAction) { + val hasSide = query.hasQueryParameter("side") + val isLong = when { + !hasSide -> null query.queryParameter("side").equals("long", true) -> true query.queryParameter("side").equals("short", true) -> false - else -> null + else -> return null } - } else { - null - } - val openPosition = isLong?.let { val hasLeverage = query.hasQueryParameter("leverage") val leverage = query.queryParameter("leverage")?.toIntOrNull()?.takeIf { value -> value > 0 } if (hasLeverage && leverage == null) return null @@ -435,10 +434,12 @@ internal fun String.toPerpsTradeAction(): PerpsTradeAction? { if (hasMargin && margin == null) return null PerpsOpenPositionAction( - isLong = it, + isLong = isLong, leverage = leverage, margin = margin, ) + } else { + null } return PerpsTradeAction(marketId, leaderPositionId, openPosition) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt index 562069c28b..f425ac0440 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt @@ -125,7 +125,7 @@ private fun resolveCurrentToken( @Composable fun OpenPositionPage( market: PerpsMarket, - isLong: Boolean, + isLong: Boolean?, source: String, onBack: () -> Unit, onOrderCreated: () -> Unit = {}, @@ -148,6 +148,7 @@ fun OpenPositionPage( val acceptedPerpAssetIds = remember(acceptedPerpAssetIdsOrdered) { acceptedPerpAssetIdsOrdered.toSet() } var currentMarket by remember(marketId) { mutableStateOf(market) } + var selectedIsLong by rememberSaveable(marketId, isLong) { mutableStateOf(isLong ?: true) } var currentToken by remember { mutableStateOf(selectedToken) } var availableTokens by remember { mutableStateOf>(emptyList()) } var usdtAmount by rememberSaveable(marketId, initialMargin) { @@ -229,7 +230,7 @@ fun OpenPositionPage( onCurrentTokenChange(currentToken) } val maxLeverage = currentMarket.leverage.coerceAtLeast(1) - LaunchedEffect(usdtAmount, leverage, currentToken?.assetId, takeProfitPrice, stopLossPrice) { + LaunchedEffect(selectedIsLong, usdtAmount, leverage, currentToken?.assetId, takeProfitPrice, stopLossPrice) { errorInfo = null } LaunchedEffect(maxLeverage, marketId) { @@ -242,7 +243,7 @@ fun OpenPositionPage( } } - LaunchedEffect(usdtAmount, leverage, currentMarket.minAmount) { + LaunchedEffect(selectedIsLong, usdtAmount, leverage, currentMarket.minAmount) { val amount = usdtAmount.toBigDecimalOrNull() val minimumAmount = currentMarket.minAmount.toBigDecimalOrNull() ?: BigDecimal.ZERO if (!shouldRequestLiquidationPrice(amount, minimumAmount)) { @@ -264,7 +265,7 @@ fun OpenPositionPage( viewModel.estimateLiquidationPrice( marketId = currentMarket.marketId, amount = normalizedAmount, - side = if (isLong) "long" else "short", + side = if (selectedIsLong) "long" else "short", leverage = leverage.toInt(), ) } @@ -327,7 +328,7 @@ fun OpenPositionPage( stopLossPrice }, currentPrice = currentMarket.last, - isLong = isLong, + isLong = selectedIsLong, marketIconUrl = currentMarket.iconUrl, marketSymbol = currentMarket.tokenSymbol, marginAmount = usdtAmount, @@ -374,6 +375,13 @@ fun OpenPositionPage( .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp) ) { + if (isLong == null) { + PerpsSideSelector( + isLong = selectedIsLong, + onSideChange = { selectedIsLong = it }, + ) + Spacer(modifier = Modifier.height(16.dp)) + } Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically @@ -389,7 +397,7 @@ fun OpenPositionPage( Spacer(modifier = Modifier.width(12.dp)) Column { Text( - text = "${if (isLong) stringResource(R.string.Long) else stringResource(R.string.Short)} ${currentMarket.tokenSymbol}", + text = "${if (selectedIsLong) stringResource(R.string.Long) else stringResource(R.string.Short)} ${currentMarket.tokenSymbol}", fontSize = 16.sp, color = MixinAppTheme.colors.textPrimary ) @@ -530,7 +538,7 @@ fun OpenPositionPage( currentLeverage = leverage, maxLeverage = maxLeverage, amount = usdtAmount, - isLong = isLong + isLong = selectedIsLong ).setOnLeverageSelected { newLeverage -> leverage = newLeverage context.defaultSharedPreferences.putInt(getLeveragePrefKey(marketId), newLeverage.toInt()) @@ -564,7 +572,7 @@ fun OpenPositionPage( currentLeverage = leverage, maxLeverage = maxLeverage, amount = usdtAmount, - isLong = isLong + isLong = selectedIsLong ).setOnLeverageSelected { newLeverage -> leverage = newLeverage context.defaultSharedPreferences.putInt(getLeveragePrefKey(marketId), newLeverage.toInt()) @@ -623,7 +631,7 @@ fun OpenPositionPage( currentLeverage = leverage, maxLeverage = maxLeverage, amount = usdtAmount, - isLong = isLong + isLong = selectedIsLong ).setOnLeverageSelected { newLeverage -> leverage = newLeverage context.defaultSharedPreferences.putInt(getLeveragePrefKey(marketId), newLeverage.toInt()) @@ -660,7 +668,7 @@ fun OpenPositionPage( val profitInfo = calculateProfitInfo( amount = usdtAmount, leverage = leverage, - isLong = isLong, + isLong = selectedIsLong, priceChangePercent = 1.0, ) @@ -798,7 +806,7 @@ fun OpenPositionPage( viewModel.openPerpsOrder( assetId = token.assetId, marketId = m.marketId, - side = if (isLong) "long" else "short", + side = if (selectedIsLong) "long" else "short", amount = normalizedAmount, leverage = leverage.toInt(), walletId = walletId, @@ -812,7 +820,7 @@ fun OpenPositionPage( PerpsConfirmBottomSheetDialogFragment.newInstance( marketSymbol = m.displaySymbol, marketIcon = m.iconUrl, - isLong = isLong, + isLong = selectedIsLong, amount = response.payAmount, leverage = leverage.toInt(), entryPrice = m.last, @@ -910,6 +918,42 @@ fun OpenPositionPage( } +@Composable +private fun PerpsSideSelector( + isLong: Boolean, + onSideChange: (Boolean) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(MixinAppTheme.colors.backgroundGrayLight) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + listOf(true to R.string.Long, false to R.string.Short).forEach { (sideIsLong, labelRes) -> + val selected = sideIsLong == isLong + Box( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(6.dp)) + .background( + if (selected) MixinAppTheme.colors.accent else Color.Transparent, + ) + .clickable { onSideChange(sideIsLong) } + .padding(vertical = 10.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(labelRes), + fontSize = 14.sp, + color = if (selected) Color.White else MixinAppTheme.colors.textPrimary, + ) + } + } + } +} + internal fun resolveInitialPerpsLeverage( requestedLeverage: Int?, savedLeverage: Int, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsActivity.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsActivity.kt index 5bb1b03078..7b6334da77 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsActivity.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsActivity.kt @@ -110,7 +110,7 @@ class PerpsActivity : BaseActivity() { marketSymbol: String, marketDisplaySymbol: String, marketTokenSymbol: String = "", - isLong: Boolean, + isLong: Boolean?, source: String, returnToDetail: Boolean = false, leaderPositionId: String? = null, @@ -126,7 +126,7 @@ class PerpsActivity : BaseActivity() { putExtra(EXTRA_MARKET_DISPLAY_SYMBOL, marketDisplaySymbol) putExtra(EXTRA_MARKET_TOKEN_SYMBOL, marketTokenSymbol) putExtra(EXTRA_MODE, MODE_OPEN_POSITION) - putExtra(EXTRA_IS_LONG, isLong) + isLong?.let { putExtra(EXTRA_IS_LONG, it) } putExtra(EXTRA_SOURCE, source) putExtra(EXTRA_RETURN_TO_DETAIL, returnToDetail) leaderPositionId?.let { putExtra(EXTRA_LEADER_POSITION_ID, it) } @@ -162,7 +162,9 @@ class PerpsActivity : BaseActivity() { val displaySymbolExtra = currentIntent.getStringExtra(EXTRA_MARKET_DISPLAY_SYMBOL).orEmpty() val tokenSymbolExtra = currentIntent.getStringExtra(EXTRA_MARKET_TOKEN_SYMBOL).orEmpty() val mode = currentIntent.getStringExtra(EXTRA_MODE) ?: MODE_DETAIL - val isLong = currentIntent.getBooleanExtra(EXTRA_IS_LONG, true) + val isLong = currentIntent + .takeIf { it.hasExtra(EXTRA_IS_LONG) } + ?.getBooleanExtra(EXTRA_IS_LONG, true) val source = currentIntent.getStringExtra(EXTRA_SOURCE) ?: AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL val returnToDetail = currentIntent.getBooleanExtra(EXTRA_RETURN_TO_DETAIL, false) leaderPositionId = currentIntent.getStringExtra(EXTRA_LEADER_POSITION_ID) @@ -196,13 +198,15 @@ class PerpsActivity : BaseActivity() { marketDisplaySymbol = market.displaySymbol, marketTokenSymbol = market.tokenSymbol, source = source, + reuseCurrentActivity = false, leaderPositionId = leaderPositionId, ) + finish() } return@launch } AnalyticsTracker.trackPerpsOpenStart( - direction = if (isLong) AnalyticsTracker.PerpsDirection.LONG else AnalyticsTracker.PerpsDirection.SHORT, + direction = if (isLong == false) AnalyticsTracker.PerpsDirection.SHORT else AnalyticsTracker.PerpsDirection.LONG, source = source, ) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeBannerActionTarget.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeBannerActionTarget.kt index c72e613084..fb787c4662 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeBannerActionTarget.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeBannerActionTarget.kt @@ -13,7 +13,7 @@ internal sealed interface WalletHomeBannerActionTarget { ) : WalletHomeBannerActionTarget data class PerpsOpen( val marketId: String, - val isLong: Boolean, + val isLong: Boolean?, val leverage: Int?, val margin: String?, val leaderPositionId: String?, diff --git a/app/src/test/java/one/mixin/android/ui/conversation/PerpsTradeActionTest.kt b/app/src/test/java/one/mixin/android/ui/conversation/PerpsTradeActionTest.kt index 331343e621..6fefdf2506 100644 --- a/app/src/test/java/one/mixin/android/ui/conversation/PerpsTradeActionTest.kt +++ b/app/src/test/java/one/mixin/android/ui/conversation/PerpsTradeActionTest.kt @@ -105,13 +105,20 @@ class PerpsTradeActionTest { } @Test - fun directPerpsOpenRequiresOpenActionAndValidSide() { + fun directPerpsOpenRequiresOpenActionAndRejectsInvalidSide() { val base = "https://mixin.one/trade?type=perps&market=e015f42e-b0ff-38e7-87b1-7e8d46fea119&leader_position=45d4c134-5682-4b1a-baf5-7c73b1590cc1" assertNull("$base&side=long".toPerpsTradeAction()?.openPosition) - assertNull("$base&action=open".toPerpsTradeAction()?.openPosition) + assertEquals( + PerpsOpenPositionAction( + isLong = null, + leverage = null, + margin = null, + ), + "$base&action=open".toPerpsTradeAction()?.openPosition, + ) assertNull("$base&action=close&side=long".toPerpsTradeAction()?.openPosition) - assertNull("$base&action=open&side=invalid".toPerpsTradeAction()?.openPosition) + assertNull("$base&action=open&side=invalid".toPerpsTradeAction()) assertEquals( "45d4c134-5682-4b1a-baf5-7c73b1590cc1", "$base&action=open".toPerpsTradeAction()?.leaderPositionId, @@ -256,6 +263,20 @@ class PerpsTradeActionTest { ) } + @Test + fun classicBannerActionParsesIncompletePerpsOpenTarget() { + assertEquals( + WalletHomeBannerActionTarget.PerpsOpen( + marketId = "e015f42e-b0ff-38e7-87b1-7e8d46fea119", + isLong = null, + leverage = null, + margin = null, + leaderPositionId = "45d4c134-5682-4b1a-baf5-7c73b1590cc1", + ), + "https://mixin.one/trade?type=perps&market=e015f42e-b0ff-38e7-87b1-7e8d46fea119&action=open&leader_position=45d4c134-5682-4b1a-baf5-7c73b1590cc1".toClassicWalletHomeBannerActionTarget(), + ) + } + @Test fun classicBannerActionFallsBackToWebTarget() { val url = "https://mixin.one/users/41d16c28-0c3a-493d-a2b4-b57875371abf"