diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/LimitOrderContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/LimitOrderContent.kt index 6393ece5ed..1c6c518626 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/LimitOrderContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/LimitOrderContent.kt @@ -98,6 +98,18 @@ enum class FocusedField { NONE, IN_AMOUNT, OUT_AMOUNT, PRICE } private const val MAX_DISPLAY_ORDER_COUNT: Int = 10 +internal fun shouldShowLimitOrderOpenOrdersCard( + inputText: String, + outputText: String, + limitPriceText: String, + focusedField: FocusedField, + isKeyboardVisible: Boolean, +): Boolean = inputText.isBlank() && + outputText.isBlank() && + limitPriceText.isBlank() && + focusedField == FocusedField.NONE && + !isKeyboardVisible + private fun formatBalanceInput(balance: String?, isWeb3: Boolean): String { val amount = balance?.toBigDecimalOrNull() ?: return "" if (amount <= BigDecimal.ZERO) return "" @@ -161,7 +173,7 @@ fun LimitOrderContent( var isReverse by remember { mutableStateOf(false) } val walletId = if (inMixin) Session.getAccountId()!! else Web3Signer.currentWalletId - var focusedField by remember { mutableStateOf(FocusedField.PRICE) } + var focusedField by remember { mutableStateOf(FocusedField.NONE) } var fromToken by remember(from, to, isReverse) { mutableStateOf(if (isReverse) to else from) @@ -240,6 +252,18 @@ fun LimitOrderContent( } val availableFromBalance = availableFromBalanceValue.stripTrailingZeros().toPlainString() KeyboardAwareBox(modifier = Modifier.fillMaxHeight(), content = { availableHeight -> + LaunchedEffect(availableHeight, inputText, outputText, limitPriceText) { + if (availableHeight == null && inputText.isBlank() && outputText.isBlank() && limitPriceText.isBlank()) { + focusedField = FocusedField.NONE + } + } + val showOpenOrdersCard = shouldShowLimitOrderOpenOrdersCard( + inputText = inputText, + outputText = outputText, + limitPriceText = limitPriceText, + focusedField = focusedField, + isKeyboardVisible = availableHeight != null, + ) Column( modifier = if (availableHeight != null) { Modifier @@ -314,10 +338,13 @@ fun LimitOrderContent( InputArea(modifier = Modifier.onFocusChanged { if (it.isFocused) { focusedField = FocusedField.IN_AMOUNT + } else if (focusedField == FocusedField.IN_AMOUNT) { + focusedField = FocusedField.NONE } }, token = fromToken, text = inputText, title = stringResource(id = R.string.swap_send), readOnly = false, selectClick = { keyboardController?.hide() focusManager.clearFocus() + focusedField = FocusedField.NONE onSelectToken(isReverse, if (isReverse) SelectTokenType.To else SelectTokenType.From) }, onInputChanged = { inputText = it @@ -352,6 +379,8 @@ fun LimitOrderContent( modifier = Modifier.onFocusChanged { if (it.isFocused) { focusedField = FocusedField.OUT_AMOUNT + } else if (focusedField == FocusedField.OUT_AMOUNT) { + focusedField = FocusedField.NONE } }, token = toToken, @@ -361,6 +390,7 @@ fun LimitOrderContent( selectClick = { keyboardController?.hide() focusManager.clearFocus() + focusedField = FocusedField.NONE onSelectToken(isReverse, if (isReverse) SelectTokenType.From else SelectTokenType.To) }, onInputChanged = { @@ -398,7 +428,11 @@ fun LimitOrderContent( Column { PriceInputArea( modifier = Modifier.onFocusChanged { - if (it.isFocused) focusedField = FocusedField.PRICE + if (it.isFocused) { + focusedField = FocusedField.PRICE + } else if (focusedField == FocusedField.PRICE) { + focusedField = FocusedField.NONE + } }, fromToken = fromToken, toToken = toToken, @@ -425,7 +459,7 @@ fun LimitOrderContent( ) } - if (availableHeight != null || inputText.isNotBlank()) { + if (!showOpenOrdersCard) { Column(modifier = Modifier .wrapContentHeight() .padding(horizontal = 20.dp) @@ -447,11 +481,13 @@ fun LimitOrderContent( onClick = { keyboardController?.hide() focusManager.clearFocus() + focusedField = FocusedField.NONE if (isButtonEnabled && !isBusy && toToken != null) { isButtonEnabled = false isSubmitting = true keyboardController?.hide() focusManager.clearFocus() + focusedField = FocusedField.NONE scope.launch { runCatching { val fromTokenValue = requireNotNull(fromToken) @@ -652,6 +688,7 @@ fun LimitOrderContent( onDone = { keyboardController?.hide() focusManager.clearFocus() + focusedField = FocusedField.NONE }, onMarketPriceClick = { marketPriceClickTime = System.currentTimeMillis() diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/SwapContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/SwapContent.kt index e0eb2e806b..e983353ae6 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/SwapContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/SwapContent.kt @@ -137,9 +137,12 @@ fun SwapContent( var inputText by remember { mutableStateOf(limitTradeInputDecimalPlaces(initialAmount ?: "", fromMaxDecimalPlaces)) } - LaunchedEffect(lastOrderTime, fromMaxDecimalPlaces) { + LaunchedEffect(lastOrderTime) { inputText = limitTradeInputDecimalPlaces(initialAmount ?: "", fromMaxDecimalPlaces) } + LaunchedEffect(fromMaxDecimalPlaces) { + inputText = swapInputTextForMaxDecimalPlacesChange(inputText, fromMaxDecimalPlaces) + } val shouldRefreshQuote = remember { MutableStateFlow(inputText) } var isButtonEnabled by remember { mutableStateOf(true) } @@ -231,6 +234,11 @@ fun SwapContent( isSendFocused = isSendFocused, isKeyboardVisible = availableHeight != null, ) + LaunchedEffect(availableHeight, inputText) { + if (availableHeight == null && inputText.isBlank()) { + isSendFocused = false + } + } Column( modifier = Modifier .fillMaxWidth() @@ -427,6 +435,11 @@ internal fun shouldShowSwapRecommendedMarketCards( isKeyboardVisible: Boolean, ): Boolean = inMixin && hasRecommendedCards && inputText.isBlank() && !isSendFocused && !isKeyboardVisible +internal fun swapInputTextForMaxDecimalPlacesChange( + currentInput: String, + maxDecimalPlaces: Int?, +): String = limitTradeInputDecimalPlaces(currentInput, maxDecimalPlaces) + @Composable fun ReviewButton( inputText: String, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt index b61fa87c54..497a18dfd8 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt @@ -84,11 +84,9 @@ import one.mixin.android.tip.wc.internal.TipGas import one.mixin.android.tip.wc.internal.buildTipGas import one.mixin.android.ui.common.BaseFragment import one.mixin.android.ui.home.web3.Web3ViewModel -import one.mixin.android.ui.home.web3.trade.perps.AllPositionsFragment import one.mixin.android.ui.home.web3.trade.perps.PerpetualGuideBottomSheetDialogFragment -import one.mixin.android.ui.home.web3.trade.perps.PerpsActivity import one.mixin.android.ui.home.web3.trade.perps.PerpsMarketListBottomSheetDialogFragment -import one.mixin.android.ui.home.web3.trade.perps.PositionDetailFragment +import one.mixin.android.ui.home.web3.trade.perps.PerpsRouteNavigator import one.mixin.android.ui.wallet.AllOrdersFragment import one.mixin.android.ui.wallet.DepositFragment import one.mixin.android.ui.wallet.LimitTransferBottomSheetDialogFragment @@ -578,37 +576,39 @@ class TradeFragment : BaseFragment() { PerpsMarketListBottomSheetDialogFragment.newInstance(initialCategory, initialSort).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG) }, onShowAllOpenPositions = { - navTo(AllPositionsFragment.newOpenInstance(), AllPositionsFragment.TAG) + PerpsRouteNavigator.showPositionList( + fragmentManager = parentFragmentManager, + showOpenPositions = true, + ) }, onShowAllClosedPositions = { - navTo(AllPositionsFragment.newClosedInstance(), AllPositionsFragment.TAG) + PerpsRouteNavigator.showPositionList( + fragmentManager = parentFragmentManager, + showOpenPositions = false, + ) }, onOpenPositionClick = { position -> - navTo( - PositionDetailFragment.newInstance( - position, - AnalyticsTracker.PerpsSource.PERPS_HOME_LIST, - ), - PositionDetailFragment.TAG, + PerpsRouteNavigator.showPositionDetail( + fragmentManager = parentFragmentManager, + position = position, + source = AnalyticsTracker.PerpsSource.PERPS_HOME_LIST, ) }, onMarketItemClick = { market -> - PerpsActivity.showDetail( - requireContext(), - market.marketId, - market.displaySymbol, - market.displaySymbol, - market.tokenSymbol, - AnalyticsTracker.PerpsSource.MORE_EXPLORE, + PerpsRouteNavigator.showMarketDetail( + fragmentManager = parentFragmentManager, + marketId = market.marketId, + marketSymbol = market.displaySymbol, + displaySymbol = market.displaySymbol, + tokenSymbol = market.tokenSymbol, + source = AnalyticsTracker.PerpsSource.MORE_EXPLORE, ) }, onClosedPositionClick = { position -> - navTo( - PositionDetailFragment.newInstance( - position, - AnalyticsTracker.PerpsSource.PERPS_HOME_LIST, - ), - PositionDetailFragment.TAG, + PerpsRouteNavigator.showPositionDetail( + fragmentManager = parentFragmentManager, + order = position, + source = AnalyticsTracker.PerpsSource.PERPS_HOME_LIST, ) } ) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsFragment.kt index db61894e68..f0de2beb22 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsFragment.kt @@ -4,7 +4,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint @@ -13,6 +15,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import one.mixin.android.Constants import one.mixin.android.R +import one.mixin.android.api.response.perps.PerpsOrderItem import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.db.perps.PerpsMarketDao import one.mixin.android.extension.isNightMode @@ -28,24 +31,39 @@ class AllPositionsFragment : BaseFragment() { const val TAG = "AllPositionsFragment" private const val ARGS_POSITION_TYPE = "args_position_type" private const val ARGS_SOURCE = "args_source" + private const val ARGS_USE_TRADE_FLOW_NAVIGATOR = "args_use_trade_flow_navigator" private const val TYPE_OPEN = "type_open" private const val TYPE_CLOSED = "type_closed" fun newInstance( showOpenPositions: Boolean = false, source: String = AnalyticsTracker.PerpsSource.PERPS_ALL_POSITIONS, + useTradeFlowNavigator: Boolean = false, ) = AllPositionsFragment().apply { arguments = Bundle().apply { putString(ARGS_POSITION_TYPE, if (showOpenPositions) TYPE_OPEN else TYPE_CLOSED) putString(ARGS_SOURCE, source) + putBoolean(ARGS_USE_TRADE_FLOW_NAVIGATOR, useTradeFlowNavigator) } } - fun newOpenInstance(source: String = AnalyticsTracker.PerpsSource.PERPS_ALL_POSITIONS) = - newInstance(showOpenPositions = true, source = source) + fun newOpenInstance( + source: String = AnalyticsTracker.PerpsSource.PERPS_ALL_POSITIONS, + useTradeFlowNavigator: Boolean = false, + ) = newInstance( + showOpenPositions = true, + source = source, + useTradeFlowNavigator = useTradeFlowNavigator, + ) - fun newClosedInstance(source: String = AnalyticsTracker.PerpsSource.PERPS_ALL_POSITIONS) = - newInstance(showOpenPositions = false, source = source) + fun newClosedInstance( + source: String = AnalyticsTracker.PerpsSource.PERPS_ALL_POSITIONS, + useTradeFlowNavigator: Boolean = false, + ) = newInstance( + showOpenPositions = false, + source = source, + useTradeFlowNavigator = useTradeFlowNavigator, + ) } @Inject @@ -53,6 +71,25 @@ class AllPositionsFragment : BaseFragment() { private val viewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (arguments?.getBoolean(ARGS_USE_TRADE_FLOW_NAVIGATOR, false) != true) return + requireActivity().onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (isTopFragment()) { + close() + } else { + isEnabled = false + requireActivity().onBackPressedDispatcher.onBackPressed() + isEnabled = true + } + } + } + ) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -63,6 +100,7 @@ class AllPositionsFragment : BaseFragment() { else -> AllPositionsType.CLOSED } val source = arguments?.getString(ARGS_SOURCE) ?: AnalyticsTracker.PerpsSource.PERPS_ALL_POSITIONS + val useTradeFlowNavigator = arguments?.getBoolean(ARGS_USE_TRADE_FLOW_NAVIGATOR, false) == true if (positionType == AllPositionsType.OPEN) { AnalyticsTracker.trackPerpsAllPositions(source) } @@ -99,34 +137,38 @@ class AllPositionsFragment : BaseFragment() { val market = withContext(Dispatchers.IO) { perpsMarketDao.getMarket(position.marketId) } - activity?.let { ctx -> - PerpsActivity.showDetail( - context = ctx, + if (useTradeFlowNavigator) { + PerpsRouteNavigator.showMarketDetail( + fragmentManager = parentFragmentManager, marketId = position.marketId, - marketSymbol = market?.displaySymbol ?: "", - marketDisplaySymbol = market?.displaySymbol ?: "", - marketTokenSymbol = market?.tokenSymbol ?: "", + marketSymbol = market?.displaySymbol ?: position.displaySymbol.orEmpty(), + displaySymbol = market?.displaySymbol ?: position.displaySymbol.orEmpty(), + tokenSymbol = market?.tokenSymbol ?: position.tokenSymbol.orEmpty(), source = AnalyticsTracker.PerpsSource.PERPS_ALL_POSITIONS, ) + } else { + activity?.let { ctx -> + PerpsActivity.showDetail( + context = ctx, + marketId = position.marketId, + marketSymbol = market?.displaySymbol ?: position.displaySymbol.orEmpty(), + marketDisplaySymbol = market?.displaySymbol ?: position.displaySymbol.orEmpty(), + marketTokenSymbol = market?.tokenSymbol ?: position.tokenSymbol.orEmpty(), + source = AnalyticsTracker.PerpsSource.PERPS_ALL_POSITIONS, + ) + } } } }, onClosedPositionClick = { position -> - activity?.supportFragmentManager?.let { fm -> - fm.beginTransaction() - .setCustomAnimations( - R.anim.slide_in_right, - 0, - 0, - R.anim.slide_out_right, - ) - .add( - android.R.id.content, - PositionDetailFragment.newInstance(position, AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_LIST), - PositionDetailFragment.TAG, - ) - .addToBackStack(null) - .commit() + if (useTradeFlowNavigator) { + PerpsRouteNavigator.showPositionDetail( + fragmentManager = parentFragmentManager, + order = position, + source = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_LIST, + ) + } else { + showPositionDetail(parentFragmentManager, position) } }, ) @@ -134,4 +176,24 @@ class AllPositionsFragment : BaseFragment() { } } } + + private fun showPositionDetail(fragmentManager: FragmentManager, position: PerpsOrderItem) { + fragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right) + .add( + android.R.id.content, + PositionDetailFragment.newInstance(position, AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_LIST), + PositionDetailFragment.TAG, + ) + .addToBackStack(null) + .commit() + } + + private fun close() { + PerpsRouteNavigator.closeTopRoute(parentFragmentManager, this) + } + + private fun isTopFragment(): Boolean { + return parentFragmentManager.fragments.lastOrNull { it.isVisible } == this + } } 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 3c7f358c42..2a757d09fe 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 @@ -50,7 +50,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.fragment.app.FragmentActivity import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -61,6 +60,7 @@ import one.mixin.android.api.response.perps.PerpsMarket import one.mixin.android.compose.CoilImage import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.extension.defaultSharedPreferences +import one.mixin.android.extension.findFragmentActivityOrNull import one.mixin.android.extension.numberFormat8 import one.mixin.android.extension.openUrl import one.mixin.android.extension.priceFormat @@ -281,13 +281,13 @@ fun OpenPositionPage( ?: "" fun showPerpsGuide(tab: Int) { - val activity = context as? FragmentActivity ?: return + val activity = context.findFragmentActivityOrNull() ?: return PerpetualGuideBottomSheetDialogFragment.newInstance(tab) .show(activity.supportFragmentManager, PerpetualGuideBottomSheetDialogFragment.TAG) } fun showTpSlBottomSheet(mode: PerpsTpSlBottomSheetDialogFragment.Mode) { - val activity = context as? FragmentActivity ?: return + val activity = context.findFragmentActivityOrNull() ?: return PerpsTpSlBottomSheetDialogFragment.newInstance( mode = mode, price = if (mode == PerpsTpSlBottomSheetDialogFragment.Mode.TAKE_PROFIT) { @@ -447,7 +447,7 @@ fun OpenPositionPage( color = MixinAppTheme.colors.accent, ), modifier = Modifier.clickable { - val activity = context as? FragmentActivity ?: return@clickable + val activity = context.findFragmentActivityOrNull() ?: return@clickable val token = currentToken if (token == null) { onTokenSelect() @@ -491,7 +491,7 @@ fun OpenPositionPage( } Spacer(modifier = Modifier.height(2.dp)) val showLeverageInput = { - val activity = context as? FragmentActivity + val activity = context.findFragmentActivityOrNull() if (activity != null) { LeverageBottomSheetDialogFragment.newInstance( currentLeverage = leverage, @@ -526,7 +526,7 @@ fun OpenPositionPage( Text( modifier = Modifier.clickable { - val activity = context as? FragmentActivity ?: return@clickable + val activity = context.findFragmentActivityOrNull() ?: return@clickable LeverageBottomSheetDialogFragment.newInstance( currentLeverage = leverage, maxLeverage = maxLeverage, @@ -585,7 +585,7 @@ fun OpenPositionPage( .clickable { if (lev == -1) { AnalyticsTracker.trackPerpsLeverageSelect(PERPS_LEVERAGE_CUSTOM_TAB) - val activity = context as? FragmentActivity ?: return@clickable + val activity = context.findFragmentActivityOrNull() ?: return@clickable LeverageBottomSheetDialogFragment.newInstance( currentLeverage = leverage, maxLeverage = maxLeverage, @@ -745,7 +745,7 @@ fun OpenPositionPage( val walletId = Session.getAccountId() ?: "" // Privacy Wallet if (walletId.isEmpty()) { isProcessing = false; return@MixinButton } - val activity = context as? FragmentActivity ?: run { isProcessing = false; return@MixinButton } + val activity = context.findFragmentActivityOrNull() ?: run { isProcessing = false; return@MixinButton } val price = m.last.toBigDecimalOrNull() ?: BigDecimal.ZERO if (price == BigDecimal.ZERO) { isProcessing = false; return@MixinButton } 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 1607f1e8f1..9d9897dc98 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 @@ -21,6 +21,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import one.mixin.android.Constants import one.mixin.android.R +import one.mixin.android.api.response.perps.PerpsMarket +import one.mixin.android.api.response.perps.PerpsOrderItem import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.db.perps.PerpsMarketDao @@ -190,6 +192,9 @@ class PerpsActivity : BaseActivity() { initialMarket = market, onBack = { finish() }, onSharePosition = ::showSharePosition, + onViewAllClosedPositions = ::showAllClosedPositions, + onPositionClick = ::showPositionDetail, + onOpenPosition = ::showOpenPosition, source = source, ) } @@ -239,4 +244,39 @@ class PerpsActivity : BaseActivity() { PerpsPositionShareBottomFragment.newInstance(position) .show(supportFragmentManager, PerpsPositionShareBottomFragment.TAG) } + + private fun showAllClosedPositions() { + supportFragmentManager.beginTransaction() + .add( + android.R.id.content, + AllPositionsFragment.newClosedInstance(AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL), + AllPositionsFragment.TAG, + ) + .addToBackStack(null) + .commit() + } + + private fun showPositionDetail(position: PerpsOrderItem) { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right) + .add( + android.R.id.content, + PositionDetailFragment.newInstance(position, AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL), + PositionDetailFragment.TAG, + ) + .addToBackStack(null) + .commit() + } + + private fun showOpenPosition(market: PerpsMarket, isLong: Boolean) { + PerpsActivity.showOpenPosition( + context = this, + marketId = market.marketId, + marketSymbol = market.displaySymbol, + marketDisplaySymbol = market.displaySymbol, + marketTokenSymbol = market.tokenSymbol, + isLong = isLong, + source = AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL, + ) + } } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailFragment.kt new file mode 100644 index 0000000000..d0fc66cea1 --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailFragment.kt @@ -0,0 +1,130 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.compose.ui.platform.ComposeView +import dagger.hilt.android.AndroidEntryPoint +import one.mixin.android.api.response.perps.PerpsPositionItem +import one.mixin.android.compose.theme.MixinAppTheme +import one.mixin.android.extension.isNightMode +import one.mixin.android.ui.common.BaseFragment +import one.mixin.android.util.analytics.AnalyticsTracker + +@AndroidEntryPoint +class PerpsMarketDetailFragment : BaseFragment() { + companion object { + const val TAG = "PerpsMarketDetailFragment" + private const val ARGS_MARKET_ID = "args_market_id" + private const val ARGS_MARKET_SYMBOL = "args_market_symbol" + private const val ARGS_DISPLAY_SYMBOL = "args_display_symbol" + private const val ARGS_TOKEN_SYMBOL = "args_token_symbol" + private const val ARGS_SOURCE = "args_source" + + fun newInstance( + marketId: String, + marketSymbol: String, + displaySymbol: String, + tokenSymbol: String, + source: String = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + ): PerpsMarketDetailFragment { + return PerpsMarketDetailFragment().apply { + arguments = Bundle().apply { + putString(ARGS_MARKET_ID, marketId) + putString(ARGS_MARKET_SYMBOL, marketSymbol) + putString(ARGS_DISPLAY_SYMBOL, displaySymbol) + putString(ARGS_TOKEN_SYMBOL, tokenSymbol) + putString(ARGS_SOURCE, source) + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requireActivity().onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (isTopFragment()) { + close() + } else { + isEnabled = false + requireActivity().onBackPressedDispatcher.onBackPressed() + isEnabled = true + } + } + } + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + val marketId = requireArguments().getString(ARGS_MARKET_ID).orEmpty() + val marketSymbol = requireArguments().getString(ARGS_MARKET_SYMBOL).orEmpty() + val displaySymbol = requireArguments().getString(ARGS_DISPLAY_SYMBOL).orEmpty() + val tokenSymbol = requireArguments().getString(ARGS_TOKEN_SYMBOL).orEmpty() + val source = requireArguments().getString(ARGS_SOURCE) ?: AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL + + return ComposeView(inflater.context).apply { + setContent { + MixinAppTheme( + darkTheme = context.isNightMode(), + ) { + PerpsMarketDetailPage( + marketId = marketId, + marketSymbol = marketSymbol, + displaySymbol = displaySymbol, + tokenSymbol = tokenSymbol, + onBack = { close() }, + onSharePosition = ::sharePosition, + onViewAllClosedPositions = { + PerpsRouteNavigator.showPositionList( + fragmentManager = parentFragmentManager, + showOpenPositions = false, + source = AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL, + ) + }, + onPositionClick = { position -> + PerpsRouteNavigator.showPositionDetail( + fragmentManager = parentFragmentManager, + order = position, + source = AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL, + ) + }, + onOpenPosition = { market, isLong -> + PerpsRouteNavigator.showOpenPosition( + fragmentManager = parentFragmentManager, + marketId = market.marketId, + marketSymbol = market.displaySymbol, + displaySymbol = market.displaySymbol, + tokenSymbol = market.tokenSymbol, + isLong = isLong, + source = AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL, + ) + }, + source = source, + ) + } + } + } + } + + private fun close() { + PerpsRouteNavigator.closeTopRoute(parentFragmentManager, this) + } + + private fun isTopFragment(): Boolean { + return parentFragmentManager.fragments.lastOrNull { it.isVisible } == this + } + + private fun sharePosition(position: PerpsPositionItem) { + PerpsPositionShareBottomFragment.newInstance(position) + .show(parentFragmentManager, PerpsPositionShareBottomFragment.TAG) + } +} diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt index a236ab279e..c48f08dffd 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt @@ -47,7 +47,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.fragment.app.FragmentActivity import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner @@ -68,6 +67,7 @@ import one.mixin.android.api.response.perps.toPosition import one.mixin.android.compose.CoilImage import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.extension.defaultSharedPreferences +import one.mixin.android.extension.findFragmentActivityOrNull import one.mixin.android.extension.putInt import one.mixin.android.extension.numberFormatCompact import one.mixin.android.extension.openUrl @@ -95,6 +95,9 @@ fun PerpsMarketDetailPage( initialMarket: PerpsMarket? = null, onBack: () -> Unit, onSharePosition: (PerpsPositionItem) -> Unit, + onViewAllClosedPositions: () -> Unit, + onPositionClick: (PerpsOrderItem) -> Unit, + onOpenPosition: (PerpsMarket, Boolean) -> Unit, source: String, ) { val context = LocalContext.current @@ -278,7 +281,7 @@ fun PerpsMarketDetailPage( } fun showTpSlBottomSheetFromGuide(mode: PerpsTpSlBottomSheetDialogFragment.Mode) { - val activity = context as? FragmentActivity ?: return + val activity = context.findFragmentActivityOrNull() ?: return val existingPrice = when (mode) { PerpsTpSlBottomSheetDialogFragment.Mode.TAKE_PROFIT -> currentPosition.takeProfitPrice.orEmpty() PerpsTpSlBottomSheetDialogFragment.Mode.STOP_LOSS -> currentPosition.stopLossPrice.orEmpty() @@ -370,7 +373,7 @@ fun PerpsMarketDetailPage( HowPerpsWorksCard( onLearnClick = { AnalyticsTracker.trackPerpsGuide(AnalyticsTracker.PerpsSource.PERPS_DETAIL_CARD) - val activity = context as? FragmentActivity ?: return@HowPerpsWorksCard + val activity = context.findFragmentActivityOrNull() ?: return@HowPerpsWorksCard PerpetualGuideBottomSheetDialogFragment.newInstance( PerpetualGuideBottomSheetDialogFragment.TAB_OVERVIEW ).show(activity.supportFragmentManager, PerpetualGuideBottomSheetDialogFragment.TAG) @@ -381,7 +384,7 @@ fun PerpsMarketDetailPage( MarketInfoCard( market = market!!, onFundingRateTipClick = { - val activity = context as? FragmentActivity ?: return@MarketInfoCard + val activity = context.findFragmentActivityOrNull() ?: return@MarketInfoCard PerpetualGuideBottomSheetDialogFragment.newInstance( PerpetualGuideBottomSheetDialogFragment.TAB_FUNDING_RATE ).show(activity.supportFragmentManager, PerpetualGuideBottomSheetDialogFragment.TAG) @@ -393,36 +396,8 @@ fun PerpsMarketDetailPage( Spacer(modifier = Modifier.height(16.dp)) ClosedPositionsSection( positions = closedPositions, - onViewAll = { - val activity = context as? FragmentActivity ?: return@ClosedPositionsSection - activity.supportFragmentManager - .beginTransaction() - .add( - android.R.id.content, - AllPositionsFragment.newClosedInstance(AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL), - AllPositionsFragment.TAG - ) - .addToBackStack(null) - .commit() - }, - onPositionClick = { position -> - val activity = context as? FragmentActivity ?: return@ClosedPositionsSection - activity.supportFragmentManager - .beginTransaction() - .setCustomAnimations( - R.anim.slide_in_right, - 0, - 0, - R.anim.slide_out_right - ) - .add( - android.R.id.content, - PositionDetailFragment.newInstance(position, AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL), - PositionDetailFragment.TAG - ) - .addToBackStack(null) - .commit() - } + onViewAll = onViewAllClosedPositions, + onPositionClick = onPositionClick, ) } @@ -431,7 +406,7 @@ fun PerpsMarketDetailPage( HowPerpsWorksCard( onLearnClick = { AnalyticsTracker.trackPerpsGuide(AnalyticsTracker.PerpsSource.PERPS_DETAIL_CARD) - val activity = context as? FragmentActivity ?: return@HowPerpsWorksCard + val activity = context.findFragmentActivityOrNull() ?: return@HowPerpsWorksCard PerpetualGuideBottomSheetDialogFragment.newInstance( PerpetualGuideBottomSheetDialogFragment.TAB_OVERVIEW ).show(activity.supportFragmentManager, PerpetualGuideBottomSheetDialogFragment.TAG) @@ -485,7 +460,7 @@ fun PerpsMarketDetailPage( onClick = { if (isAddingProcessing) return@MixinButton isAddingProcessing = true - val activity = context as? FragmentActivity ?: run { isAddingProcessing = false; return@MixinButton } + val activity = context.findFragmentActivityOrNull() ?: run { isAddingProcessing = false; return@MixinButton } val positionForAdd = currentPosition PerpsAddBottomSheetDialogFragment.newInstance(positionForAdd) .setOnDestroy { @@ -555,7 +530,7 @@ fun PerpsMarketDetailPage( .height(48.dp), enabled = true, onClick = { - val activity = context as? FragmentActivity ?: return@MixinButton + val activity = context.findFragmentActivityOrNull() ?: return@MixinButton val position = currentPosition.toPosition() AnalyticsTracker.trackPerpsClosePositionStart() PerpsCloseBottomSheetDialogFragment.newInstance( @@ -601,15 +576,7 @@ fun PerpsMarketDetailPage( .weight(1f) .height(48.dp), onClick = { - PerpsActivity.showOpenPosition( - context = context, - marketId = marketId, - marketSymbol = marketSymbol, - marketDisplaySymbol = market?.displaySymbol ?: marketSymbol, - marketTokenSymbol = market?.tokenSymbol ?: "", - isLong = true, - source = AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL, - ) + market?.let { onOpenPosition(it, true) } }, backgroundColor = risingColor, contentColor = Color.White, @@ -626,15 +593,7 @@ fun PerpsMarketDetailPage( .weight(1f) .height(48.dp), onClick = { - PerpsActivity.showOpenPosition( - context = context, - marketId = marketId, - marketSymbol = marketSymbol, - marketDisplaySymbol = market?.displaySymbol ?: marketSymbol, - marketTokenSymbol = market?.tokenSymbol ?: "", - isLong = false, - source = AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL, - ) + market?.let { onOpenPosition(it, false) } }, backgroundColor = fallingColor, contentColor = Color.White, @@ -916,14 +875,14 @@ private fun OpenPositionCard( val compactTextStyle = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = false)) fun showTpSlGuide() { - val activity = context as? FragmentActivity ?: return + val activity = context.findFragmentActivityOrNull() ?: return PerpetualGuideBottomSheetDialogFragment.newInstance( PerpetualGuideBottomSheetDialogFragment.TAB_TP_SL ).show(activity.supportFragmentManager, PerpetualGuideBottomSheetDialogFragment.TAG) } fun showTpSlBottomSheet(mode: PerpsTpSlBottomSheetDialogFragment.Mode) { - val activity = context as? FragmentActivity ?: return + val activity = context.findFragmentActivityOrNull() ?: return val existingPrice = when (mode) { PerpsTpSlBottomSheetDialogFragment.Mode.TAKE_PROFIT -> position.takeProfitPrice.orEmpty() PerpsTpSlBottomSheetDialogFragment.Mode.STOP_LOSS -> position.stopLossPrice.orEmpty() @@ -1094,7 +1053,7 @@ private fun OpenPositionCard( modifier = Modifier .size(12.dp) .clickable { - val activity = context as? FragmentActivity ?: return@clickable + val activity = context.findFragmentActivityOrNull() ?: return@clickable PerpetualGuideBottomSheetDialogFragment.newInstance( PerpetualGuideBottomSheetDialogFragment.TAB_POSITION ).show(activity.supportFragmentManager, PerpetualGuideBottomSheetDialogFragment.TAG) @@ -1163,7 +1122,7 @@ private fun OpenPositionCard( modifier = Modifier .size(12.dp) .clickable { - val activity = context as? FragmentActivity ?: return@clickable + val activity = context.findFragmentActivityOrNull() ?: return@clickable PerpetualGuideBottomSheetDialogFragment.newInstance( PerpetualGuideBottomSheetDialogFragment.TAB_LIQUIDATION ).show(activity.supportFragmentManager, PerpetualGuideBottomSheetDialogFragment.TAG) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt index c3f6ad2b8d..ddd8f372a7 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt @@ -314,20 +314,21 @@ class PerpsMarketListBottomSheetDialogFragment : MixinBottomSheetDialogFragment( } if (hasOpenPosition || isLong == null) { - PerpsActivity.showDetail( - context = requireContext(), + PerpsRouteNavigator.showMarketDetail( + fragmentManager = parentFragmentManager, marketId = market.marketId, marketSymbol = market.displaySymbol, - marketDisplaySymbol = market.displaySymbol, - marketTokenSymbol = market.tokenSymbol + displaySymbol = market.displaySymbol, + tokenSymbol = market.tokenSymbol, + source = AnalyticsTracker.PerpsSource.MORE_EXPLORE, ) } else { - PerpsActivity.showOpenPosition( - context = requireContext(), + PerpsRouteNavigator.showOpenPosition( + fragmentManager = parentFragmentManager, marketId = market.marketId, marketSymbol = market.displaySymbol, - marketDisplaySymbol = market.displaySymbol, - marketTokenSymbol = market.tokenSymbol, + displaySymbol = market.displaySymbol, + tokenSymbol = market.tokenSymbol, isLong = requireNotNull(isLong), source = AnalyticsTracker.PerpsSource.MORE_EXPLORE, ) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsOpenPositionFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsOpenPositionFragment.kt new file mode 100644 index 0000000000..c36afddb47 --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsOpenPositionFragment.kt @@ -0,0 +1,174 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import one.mixin.android.R +import one.mixin.android.api.response.perps.PerpsMarket +import one.mixin.android.compose.theme.MixinAppTheme +import one.mixin.android.db.perps.PerpsMarketDao +import one.mixin.android.extension.isNightMode +import one.mixin.android.extension.toast +import one.mixin.android.ui.common.BaseFragment +import one.mixin.android.ui.wallet.TokenListBottomSheetDialogFragment +import one.mixin.android.ui.wallet.WalletActivity +import one.mixin.android.util.analytics.AnalyticsTracker +import one.mixin.android.vo.safe.TokenItem +import javax.inject.Inject + +@AndroidEntryPoint +class PerpsOpenPositionFragment : BaseFragment() { + companion object { + const val TAG = "PerpsOpenPositionFragment" + private const val ARGS_MARKET_ID = "args_market_id" + private const val ARGS_MARKET_SYMBOL = "args_market_symbol" + private const val ARGS_DISPLAY_SYMBOL = "args_display_symbol" + private const val ARGS_TOKEN_SYMBOL = "args_token_symbol" + private const val ARGS_IS_LONG = "args_is_long" + private const val ARGS_SOURCE = "args_source" + + fun newInstance( + marketId: String, + marketSymbol: String, + displaySymbol: String, + tokenSymbol: String, + isLong: Boolean, + source: String, + ): PerpsOpenPositionFragment { + return PerpsOpenPositionFragment().apply { + arguments = Bundle().apply { + putString(ARGS_MARKET_ID, marketId) + putString(ARGS_MARKET_SYMBOL, marketSymbol) + putString(ARGS_DISPLAY_SYMBOL, displaySymbol) + putString(ARGS_TOKEN_SYMBOL, tokenSymbol) + putBoolean(ARGS_IS_LONG, isLong) + putString(ARGS_SOURCE, source) + } + } + } + } + + @Inject + lateinit var perpsMarketDao: PerpsMarketDao + + private var selectedToken by mutableStateOf(null) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requireActivity().onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (isTopFragment()) { + close() + } else { + isEnabled = false + requireActivity().onBackPressedDispatcher.onBackPressed() + isEnabled = true + } + } + } + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + val marketId = requireArguments().getString(ARGS_MARKET_ID).orEmpty() + val marketSymbol = requireArguments().getString(ARGS_MARKET_SYMBOL).orEmpty() + val displaySymbol = requireArguments().getString(ARGS_DISPLAY_SYMBOL).orEmpty() + val tokenSymbol = requireArguments().getString(ARGS_TOKEN_SYMBOL).orEmpty() + val isLong = requireArguments().getBoolean(ARGS_IS_LONG, true) + val source = requireArguments().getString(ARGS_SOURCE) ?: AnalyticsTracker.PerpsSource.MORE_EXPLORE + + return ComposeView(inflater.context).apply { + setContent { + MixinAppTheme( + darkTheme = context.isNightMode(), + ) { + var market by remember(marketId) { mutableStateOf(null) } + LaunchedEffect(marketId) { + market = withContext(Dispatchers.IO) { + perpsMarketDao.getMarket(marketId) + } + if (market == null) { + toast(R.string.Alert_Not_Support) + close() + } else { + AnalyticsTracker.trackPerpsOpenPositionStart( + direction = if (isLong) AnalyticsTracker.PerpsDirection.LONG else AnalyticsTracker.PerpsDirection.SHORT, + source = source, + ) + } + } + market?.let { currentMarket -> + OpenPositionPage( + market = currentMarket, + isLong = isLong, + source = source, + onBack = { close() }, + onOpenSuccess = { openedMarketId -> + PerpsRouteNavigator.showMarketDetail( + fragmentManager = parentFragmentManager, + marketId = openedMarketId, + marketSymbol = currentMarket.displaySymbol.ifBlank { marketSymbol }, + displaySymbol = currentMarket.displaySymbol.ifBlank { displaySymbol }, + tokenSymbol = currentMarket.tokenSymbol.ifBlank { tokenSymbol }, + source = AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL, + ) + }, + selectedToken = selectedToken, + onTokenSelect = { showTokenSelection() }, + onCurrentTokenChange = { token -> + selectedToken = token + }, + ) + } + } + } + } + } + + private fun close() { + PerpsRouteNavigator.closeTopRoute(parentFragmentManager, this) + } + + private fun isTopFragment(): Boolean { + return parentFragmentManager.fragments.lastOrNull { it.isVisible } == this + } + + private fun showTokenSelection() { + TokenListBottomSheetDialogFragment.newInstance( + fromType = TokenListBottomSheetDialogFragment.TYPE_FROM_PERP, + currentAssetId = selectedToken?.assetId, + ).setOnAssetClick { token -> + selectedToken = token + }.setOnDepositClick { + showDepositAssetSelection() + }.show(parentFragmentManager, TokenListBottomSheetDialogFragment.TAG) + } + + private fun showDepositAssetSelection() { + val token = selectedToken + if (token == null) { + toast(R.string.Not_found) + return + } + activity?.let { + WalletActivity.showDeposit(it, token) + } + } +} diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsRouteNavigator.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsRouteNavigator.kt new file mode 100644 index 0000000000..437e34083c --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsRouteNavigator.kt @@ -0,0 +1,187 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import one.mixin.android.R +import one.mixin.android.api.response.perps.PerpsOrderItem +import one.mixin.android.api.response.perps.PerpsPositionItem +import one.mixin.android.util.analytics.AnalyticsTracker + +object PerpsRouteNavigator { + private val ROUTE_TAGS = setOf( + PerpsMarketDetailFragment.TAG, + AllPositionsFragment.TAG, + PositionDetailFragment.TAG, + PerpsOpenPositionFragment.TAG, + ) + + fun showTradeRoot(fragmentManager: FragmentManager) { + ROUTE_TAGS.forEach { tag -> + popRoute(fragmentManager, tag) + } + removeRoutes(fragmentManager) { it.isPerpsRoute() } + } + + fun showMarketDetail( + fragmentManager: FragmentManager, + marketId: String, + marketSymbol: String, + displaySymbol: String, + tokenSymbol: String, + source: String = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + ) { + popRoute(fragmentManager, PerpsMarketDetailFragment.TAG) + removeRoutes(fragmentManager) { it is PerpsMarketDetailFragment } + addRoute( + fragmentManager = fragmentManager, + fragment = PerpsMarketDetailFragment.newInstance( + marketId = marketId, + marketSymbol = marketSymbol, + displaySymbol = displaySymbol, + tokenSymbol = tokenSymbol, + source = source, + ), + tag = PerpsMarketDetailFragment.TAG, + ) + } + + fun showPositionList( + fragmentManager: FragmentManager, + showOpenPositions: Boolean, + source: String = AnalyticsTracker.PerpsSource.PERPS_ALL_POSITIONS, + ) { + popRoute(fragmentManager, AllPositionsFragment.TAG) + removeRoutes(fragmentManager) { it is AllPositionsFragment } + addRoute( + fragmentManager = fragmentManager, + fragment = AllPositionsFragment.newInstance( + showOpenPositions = showOpenPositions, + source = source, + useTradeFlowNavigator = true, + ), + tag = AllPositionsFragment.TAG, + ) + } + + fun showPositionDetail( + fragmentManager: FragmentManager, + position: PerpsPositionItem, + source: String = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + ) { + popRoute(fragmentManager, PositionDetailFragment.TAG) + removeRoutes(fragmentManager) { it is PositionDetailFragment } + addRoute( + fragmentManager = fragmentManager, + fragment = PositionDetailFragment.newInstance( + position = position, + source = source, + useTradeFlowNavigator = true, + ), + tag = PositionDetailFragment.TAG, + ) + } + + fun showPositionDetail( + fragmentManager: FragmentManager, + order: PerpsOrderItem, + source: String = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + ) { + popRoute(fragmentManager, PositionDetailFragment.TAG) + removeRoutes(fragmentManager) { it is PositionDetailFragment } + addRoute( + fragmentManager = fragmentManager, + fragment = PositionDetailFragment.newInstance( + order = order, + source = source, + useTradeFlowNavigator = true, + ), + tag = PositionDetailFragment.TAG, + ) + } + + fun showOpenPosition( + fragmentManager: FragmentManager, + marketId: String, + marketSymbol: String, + displaySymbol: String, + tokenSymbol: String, + isLong: Boolean, + source: String, + ) { + popRoute(fragmentManager, PerpsOpenPositionFragment.TAG) + removeRoutes(fragmentManager) { it is PerpsOpenPositionFragment } + addRoute( + fragmentManager = fragmentManager, + fragment = PerpsOpenPositionFragment.newInstance( + marketId = marketId, + marketSymbol = marketSymbol, + displaySymbol = displaySymbol, + tokenSymbol = tokenSymbol, + isLong = isLong, + source = source, + ), + tag = PerpsOpenPositionFragment.TAG, + ) + } + + fun closeTopRoute(fragmentManager: FragmentManager, fragment: Fragment) { + val topEntryName = fragmentManager.takeIf { it.backStackEntryCount > 0 } + ?.getBackStackEntryAt(fragmentManager.backStackEntryCount - 1) + ?.name + if (topEntryName in ROUTE_TAGS) { + fragmentManager.popBackStack() + return + } + + val topFragment = fragmentManager.fragments.lastOrNull { it.isVisible } + val target = if (topFragment?.isPerpsRoute() == true) topFragment else fragment + removeRoute(fragmentManager, target) + } + + private fun addRoute( + fragmentManager: FragmentManager, + fragment: Fragment, + tag: String, + ) { + fragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right) + .add(R.id.container, fragment, tag) + .addToBackStack(tag) + .commitAllowingStateLoss() + } + + private fun popRoute(fragmentManager: FragmentManager, tag: String) { + if (fragmentManager.isStateSaved) return + fragmentManager.popBackStackImmediate(tag, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + + private fun removeRoutes( + fragmentManager: FragmentManager, + predicate: (Fragment) -> Boolean, + ) { + val routeFragments = fragmentManager.fragments.filter(predicate) + if (routeFragments.isEmpty()) return + + val transaction = fragmentManager.beginTransaction() + routeFragments.forEach(transaction::remove) + if (fragmentManager.isStateSaved) { + transaction.commitAllowingStateLoss() + } else { + transaction.commitNowAllowingStateLoss() + } + } + + private fun removeRoute(fragmentManager: FragmentManager, fragment: Fragment) { + fragmentManager.beginTransaction() + .setCustomAnimations(0, R.anim.slide_out_right, R.anim.stay, 0) + .remove(fragment) + .commitAllowingStateLoss() + } + + private fun Fragment.isPerpsRoute(): Boolean { + return this is PerpsMarketDetailFragment || + this is AllPositionsFragment || + this is PositionDetailFragment || + this is PerpsOpenPositionFragment + } +} diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailFragment.kt index 6c808acf2e..1b77ab838a 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.platform.ComposeView @@ -37,22 +38,33 @@ class PositionDetailFragment : BaseFragment() { private const val ARGS_POSITION = "args_position" private const val ARGS_CLOSE_ORDER = "args_close_order" private const val ARGS_SOURCE = "args_source" + private const val ARGS_USE_TRADE_FLOW_NAVIGATOR = "args_use_trade_flow_navigator" private const val POSITION_REFRESH_INTERVAL_MS = 10_000L - fun newInstance(position: PerpsPositionItem, source: String = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL): PositionDetailFragment { + fun newInstance( + position: PerpsPositionItem, + source: String = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + useTradeFlowNavigator: Boolean = false, + ): PositionDetailFragment { return PositionDetailFragment().apply { arguments = Bundle().apply { putParcelable(ARGS_POSITION, position) putString(ARGS_SOURCE, source) + putBoolean(ARGS_USE_TRADE_FLOW_NAVIGATOR, useTradeFlowNavigator) } } } - fun newInstance(order: PerpsOrderItem, source: String = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL): PositionDetailFragment { + fun newInstance( + order: PerpsOrderItem, + source: String = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + useTradeFlowNavigator: Boolean = false, + ): PositionDetailFragment { return PositionDetailFragment().apply { arguments = Bundle().apply { putParcelable(ARGS_CLOSE_ORDER, order) putString(ARGS_SOURCE, source) + putBoolean(ARGS_USE_TRADE_FLOW_NAVIGATOR, useTradeFlowNavigator) } } } @@ -64,6 +76,25 @@ class PositionDetailFragment : BaseFragment() { requireContext().defaultSharedPreferences.getBoolean(Constants.Account.PREF_QUOTE_COLOR, false) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (arguments?.getBoolean(ARGS_USE_TRADE_FLOW_NAVIGATOR, false) != true) return + requireActivity().onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (isTopFragment()) { + close() + } else { + isEnabled = false + requireActivity().onBackPressedDispatcher.onBackPressed() + isEnabled = true + } + } + } + ) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -72,6 +103,7 @@ class PositionDetailFragment : BaseFragment() { val position = arguments?.getParcelableCompat(ARGS_POSITION, PerpsPositionItem::class.java) val closeOrder = arguments?.getParcelableCompat(ARGS_CLOSE_ORDER, PerpsOrderItem::class.java) val source = arguments?.getString(ARGS_SOURCE) ?: AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL + val useTradeFlowNavigator = arguments?.getBoolean(ARGS_USE_TRADE_FLOW_NAVIGATOR, false) == true AnalyticsTracker.trackPerpsActivityDetail(source) return ComposeView(inflater.context).apply { @@ -112,8 +144,11 @@ class PositionDetailFragment : BaseFragment() { pop = { activity?.onBackPressedDispatcher?.onBackPressed() }, + onViewMarket = { + openViewMarket(currentPosition, useTradeFlowNavigator) + }, onClose = { - showCloseDialog(currentPosition) + showCloseDialog(currentPosition, useTradeFlowNavigator) }, onShare = { sharePosition(currentPosition) @@ -141,8 +176,11 @@ class PositionDetailFragment : BaseFragment() { pop = { activity?.onBackPressedDispatcher?.onBackPressed() }, + onViewMarket = { + openViewMarket(closeOrder, useTradeFlowNavigator) + }, onTradeAgain = { - openTradeAgain(closeOrder) + openTradeAgain(closeOrder, useTradeFlowNavigator) }, onShare = { sharePosition(closeOrder, leverage ?: closeOrder.leverage) @@ -181,7 +219,7 @@ class PositionDetailFragment : BaseFragment() { activity?.onBackPressedDispatcher?.onBackPressed() }, onViewMarket = { - openViewMarket(closeOrder) + openViewMarket(closeOrder, useTradeFlowNavigator) }, onShare = { lifecycleScope.launch { @@ -211,46 +249,100 @@ class PositionDetailFragment : BaseFragment() { } } - private fun showCloseDialog(position: PerpsPositionItem) { + private fun showCloseDialog(position: PerpsPositionItem, useTradeFlowNavigator: Boolean) { val perpsPosition = position.toPosition() AnalyticsTracker.trackPerpsClosePositionStart() PerpsCloseBottomSheetDialogFragment.newInstance(perpsPosition) .setOnDone { - PerpsActivity.showDetail( - requireContext(), - position.marketId, - position.displaySymbol.orEmpty(), - position.displaySymbol.orEmpty(), - position.tokenSymbol.orEmpty(), - AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + showMarketDetail( + marketId = position.marketId, + marketSymbol = position.displaySymbol.orEmpty(), + displaySymbol = position.displaySymbol.orEmpty(), + tokenSymbol = position.tokenSymbol.orEmpty(), + source = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + useTradeFlowNavigator = useTradeFlowNavigator, ) - activity?.supportFragmentManager?.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } .showNow(parentFragmentManager, PerpsCloseBottomSheetDialogFragment.TAG) } - private fun openTradeAgain(order: PerpsOrderItem) { - PerpsActivity.showDetail( - context = requireContext(), + private fun openTradeAgain(order: PerpsOrderItem, useTradeFlowNavigator: Boolean) { + val marketSymbol = order.displaySymbol.orEmpty() + val tokenSymbol = order.tokenSymbol.orEmpty() + val isLong = order.side.equals("long", ignoreCase = true) + if (useTradeFlowNavigator) { + PerpsRouteNavigator.showOpenPosition( + fragmentManager = parentFragmentManager, + marketId = order.marketId, + marketSymbol = marketSymbol, + displaySymbol = marketSymbol, + tokenSymbol = tokenSymbol, + isLong = isLong, + source = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + ) + } else { + PerpsActivity.showOpenPosition( + context = requireContext(), + marketId = order.marketId, + marketSymbol = marketSymbol, + marketDisplaySymbol = marketSymbol, + marketTokenSymbol = tokenSymbol, + isLong = isLong, + source = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + ) + } + } + + private fun openViewMarket(order: PerpsOrderItem, useTradeFlowNavigator: Boolean) { + showMarketDetail( marketId = order.marketId, marketSymbol = order.displaySymbol.orEmpty(), - marketDisplaySymbol = order.displaySymbol.orEmpty(), - marketTokenSymbol = order.tokenSymbol.orEmpty(), + displaySymbol = order.displaySymbol.orEmpty(), + tokenSymbol = order.tokenSymbol.orEmpty(), source = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + useTradeFlowNavigator = useTradeFlowNavigator, ) - activity?.supportFragmentManager?.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } - private fun openViewMarket(order: PerpsOrderItem) { - PerpsActivity.showDetail( - context = requireContext(), - marketId = order.marketId, - marketSymbol = order.displaySymbol.orEmpty(), - marketDisplaySymbol = order.displaySymbol.orEmpty(), - marketTokenSymbol = order.tokenSymbol.orEmpty(), + private fun openViewMarket(position: PerpsPositionItem, useTradeFlowNavigator: Boolean) { + showMarketDetail( + marketId = position.marketId, + marketSymbol = position.displaySymbol.orEmpty(), + displaySymbol = position.displaySymbol.orEmpty(), + tokenSymbol = position.tokenSymbol.orEmpty(), source = AnalyticsTracker.PerpsSource.PERPS_ACTIVITY_DETAIL, + useTradeFlowNavigator = useTradeFlowNavigator, ) - activity?.supportFragmentManager?.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + + private fun showMarketDetail( + marketId: String, + marketSymbol: String, + displaySymbol: String, + tokenSymbol: String, + source: String, + useTradeFlowNavigator: Boolean, + ) { + if (useTradeFlowNavigator) { + PerpsRouteNavigator.showMarketDetail( + fragmentManager = parentFragmentManager, + marketId = marketId, + marketSymbol = marketSymbol, + displaySymbol = displaySymbol, + tokenSymbol = tokenSymbol, + source = source, + ) + } else { + PerpsActivity.showDetail( + requireContext(), + marketId, + marketSymbol, + displaySymbol, + tokenSymbol, + source, + ) + activity?.supportFragmentManager?.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } } private fun sharePosition(position: PerpsPositionItem) { @@ -262,4 +354,12 @@ class PositionDetailFragment : BaseFragment() { PerpsPositionShareBottomFragment.newInstance(order, leverage) .show(parentFragmentManager, PerpsPositionShareBottomFragment.TAG) } + + private fun close() { + PerpsRouteNavigator.closeTopRoute(parentFragmentManager, this) + } + + private fun isTopFragment(): Boolean { + return parentFragmentManager.fragments.lastOrNull { it.isVisible } == this + } } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt index 6e01cbb20a..5927dce740 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.fragment.app.FragmentActivity import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import one.mixin.android.R import one.mixin.android.api.response.perps.PerpsOrder @@ -49,6 +48,7 @@ import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.compose.CoilImage import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.extension.defaultSharedPreferences +import one.mixin.android.extension.findFragmentActivityOrNull import one.mixin.android.extension.toast import one.mixin.android.ui.home.web3.components.PageScaffold import one.mixin.android.ui.tip.wc.compose.ItemWalletContent @@ -65,6 +65,7 @@ fun PositionDetailPage( position: PerpsPositionItem, quoteColorReversed: Boolean = false, pop: () -> Unit, + onViewMarket: (() -> Unit)? = null, onClose: (() -> Unit)? = null, onShare: (() -> Unit)? = null, onSupport: (() -> Unit)? = null, @@ -148,7 +149,7 @@ fun PositionDetailPage( } fun showTpSlBottomSheetFromGuide(mode: PerpsTpSlBottomSheetDialogFragment.Mode) { - val activity = context as? FragmentActivity ?: return + val activity = context.findFragmentActivityOrNull() ?: return val existingPrice = when (mode) { PerpsTpSlBottomSheetDialogFragment.Mode.TAKE_PROFIT -> position.takeProfitPrice.orEmpty() PerpsTpSlBottomSheetDialogFragment.Mode.STOP_LOSS -> position.stopLossPrice.orEmpty() @@ -200,6 +201,13 @@ fun PositionDetailPage( return formatPerpsSignedRawUsdDecimal(value) } + val viewMarketClick = onViewMarket + val viewMarketModifier = if (viewMarketClick != null) { + Modifier.clickable { viewMarketClick() } + } else { + Modifier + } + PageScaffold( title = title, verticalScrollable = false, @@ -229,56 +237,60 @@ fun PositionDetailPage( ) ) { Spacer(modifier = Modifier.height(30.dp)) - - CoilImage( - model = position.iconUrl, - placeholder = R.drawable.ic_avatar_place_holder, + + Column( modifier = Modifier - .size(70.dp) - .clip(CircleShape) .align(Alignment.CenterHorizontally) - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Row( - modifier = Modifier.align(Alignment.CenterHorizontally), - verticalAlignment = Alignment.Bottom, + .then(viewMarketModifier), + horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = absQuantity.stripTrailingZeros().toPlainString(), - fontSize = 34.sp, - fontWeight = FontWeight.W500, - fontFamily = FontFamily(Font(R.font.mixin_font)), - color = MixinAppTheme.colors.textPrimary, + CoilImage( + model = position.iconUrl, + placeholder = R.drawable.ic_avatar_place_holder, + modifier = Modifier + .size(70.dp) + .clip(CircleShape) ) - val symbol = position.tokenSymbol?.takeIf { it.isNotBlank() } - if (symbol != null) { - Spacer(modifier = Modifier.width(4.dp)) + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + verticalAlignment = Alignment.Bottom, + ) { Text( - text = symbol, - fontSize = 16.sp, + text = absQuantity.stripTrailingZeros().toPlainString(), + fontSize = 34.sp, fontWeight = FontWeight.W500, + fontFamily = FontFamily(Font(R.font.mixin_font)), color = MixinAppTheme.colors.textPrimary, - modifier = Modifier.padding(bottom = 4.dp) ) + val symbol = position.tokenSymbol?.takeIf { it.isNotBlank() } + if (symbol != null) { + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = symbol, + fontSize = 16.sp, + fontWeight = FontWeight.W500, + color = MixinAppTheme.colors.textPrimary, + modifier = Modifier.padding(bottom = 4.dp) + ) + } } - } - - Spacer(modifier = Modifier.height(10.dp)) - Box( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(leverageBackgroundColor) - .padding(horizontal = 8.dp, vertical = 2.5.dp) - .align(Alignment.CenterHorizontally) - ) { - Text( - text = "$sideText ${position.leverage}x", - color = leverageTextColor, - fontSize = 14.sp - ) + Spacer(modifier = Modifier.height(10.dp)) + + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(leverageBackgroundColor) + .padding(horizontal = 8.dp, vertical = 2.5.dp) + ) { + Text( + text = "$sideText ${position.leverage}x", + color = leverageTextColor, + fontSize = 14.sp + ) + } } Spacer(modifier = Modifier.height(20.dp)) @@ -351,7 +363,8 @@ fun PositionDetailPage( PositionDetailItem( label = stringResource(R.string.Perpetual).uppercase(), value = position.displaySymbol ?: position.tokenSymbol ?: "Unknown", - icon = position.iconUrl + icon = position.iconUrl, + onClick = onViewMarket ) Spacer(modifier = Modifier.height(20.dp)) @@ -431,9 +444,17 @@ private fun PositionDetailItem( icon: String? = null, subtitle: String? = null, valueColor: Color = MixinAppTheme.colors.textPrimary, + onClick: (() -> Unit)? = null, ) { + val itemModifier = if (onClick != null) { + Modifier + .fillMaxWidth() + .clickable { onClick() } + } else { + Modifier.fillMaxWidth() + } Column( - modifier = Modifier.fillMaxWidth() + modifier = itemModifier ) { Text( text = label, @@ -486,6 +507,7 @@ fun PositionDetailPage( leverage: Int?, quoteColorReversed: Boolean = false, pop: () -> Unit, + onViewMarket: (() -> Unit)? = null, onTradeAgain: (() -> Unit)? = null, onShare: (() -> Unit)? = null, onSupport: (() -> Unit)? = null, @@ -547,6 +569,13 @@ fun PositionDetailPage( return formatPerpsSignedRawUsdDecimal(value) } + val viewMarketClick = onViewMarket + val viewMarketModifier = if (viewMarketClick != null) { + Modifier.clickable { viewMarketClick() } + } else { + Modifier + } + PageScaffold( title = title, verticalScrollable = false, @@ -577,54 +606,58 @@ fun PositionDetailPage( ) { Spacer(modifier = Modifier.height(30.dp)) - CoilImage( - model = closeOrder.iconUrl, - placeholder = R.drawable.ic_avatar_place_holder, + Column( modifier = Modifier - .size(70.dp) - .clip(CircleShape) .align(Alignment.CenterHorizontally) - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Row( - modifier = Modifier.align(Alignment.CenterHorizontally), - verticalAlignment = Alignment.Bottom, + .then(viewMarketModifier), + horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = absQuantity.stripTrailingZeros().toPlainString(), - fontSize = 34.sp, - fontWeight = FontWeight.W500, - fontFamily = FontFamily(Font(R.font.mixin_font)), - color = MixinAppTheme.colors.textPrimary, + CoilImage( + model = closeOrder.iconUrl, + placeholder = R.drawable.ic_avatar_place_holder, + modifier = Modifier + .size(70.dp) + .clip(CircleShape) ) - val symbol = closeOrder.tokenSymbol?.takeIf { it.isNotBlank() } - if (symbol != null) { - Spacer(modifier = Modifier.width(4.dp)) + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + verticalAlignment = Alignment.Bottom, + ) { Text( - text = symbol, - fontSize = 16.sp, + text = absQuantity.stripTrailingZeros().toPlainString(), + fontSize = 34.sp, fontWeight = FontWeight.W500, + fontFamily = FontFamily(Font(R.font.mixin_font)), color = MixinAppTheme.colors.textPrimary, - modifier = Modifier.padding(bottom = 4.dp) ) + val symbol = closeOrder.tokenSymbol?.takeIf { it.isNotBlank() } + if (symbol != null) { + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = symbol, + fontSize = 16.sp, + fontWeight = FontWeight.W500, + color = MixinAppTheme.colors.textPrimary, + modifier = Modifier.padding(bottom = 4.dp) + ) + } } - } - Spacer(modifier = Modifier.height(10.dp)) - Box( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(leverageBackgroundColor) - .padding(horizontal = 8.dp, vertical = 2.5.dp) - .align(Alignment.CenterHorizontally) - ) { - Text( - text = "$sideText ${effectiveLeverage}x", - color = leverageTextColor, - fontSize = 14.sp - ) + Spacer(modifier = Modifier.height(10.dp)) + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(leverageBackgroundColor) + .padding(horizontal = 8.dp, vertical = 2.5.dp) + ) { + Text( + text = "$sideText ${effectiveLeverage}x", + color = leverageTextColor, + fontSize = 14.sp + ) + } } Spacer(modifier = Modifier.height(20.dp)) @@ -689,7 +722,8 @@ fun PositionDetailPage( PositionDetailItem( label = stringResource(R.string.Perpetual).uppercase(), value = closeOrder.displaySymbol ?: closeOrder.tokenSymbol ?: "Unknown", - icon = closeOrder.iconUrl + icon = closeOrder.iconUrl, + onClick = onViewMarket ) Spacer(modifier = Modifier.height(20.dp)) @@ -794,6 +828,12 @@ fun OpenedOrderDetailPage( val quantity = openedOrder.quantity.toBigDecimalOrNull() ?: BigDecimal.ZERO val absQuantity = quantity.abs() val leverage = openedOrder.leverage + val viewMarketClick = onViewMarket + val viewMarketModifier = if (viewMarketClick != null) { + Modifier.clickable { viewMarketClick() } + } else { + Modifier + } PageScaffold( title = title, @@ -825,54 +865,58 @@ fun OpenedOrderDetailPage( ) { Spacer(modifier = Modifier.height(30.dp)) - CoilImage( - model = openedOrder.iconUrl, - placeholder = R.drawable.ic_avatar_place_holder, + Column( modifier = Modifier - .size(70.dp) - .clip(CircleShape) .align(Alignment.CenterHorizontally) - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Row( - modifier = Modifier.align(Alignment.CenterHorizontally), - verticalAlignment = Alignment.Bottom, + .then(viewMarketModifier), + horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = absQuantity.stripTrailingZeros().toPlainString(), - fontSize = 34.sp, - fontWeight = FontWeight.W500, - fontFamily = FontFamily(Font(R.font.mixin_font)), - color = MixinAppTheme.colors.textPrimary, + CoilImage( + model = openedOrder.iconUrl, + placeholder = R.drawable.ic_avatar_place_holder, + modifier = Modifier + .size(70.dp) + .clip(CircleShape) ) - val symbol = openedOrder.tokenSymbol?.takeIf { it.isNotBlank() } - if (symbol != null) { - Spacer(modifier = Modifier.width(4.dp)) + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + verticalAlignment = Alignment.Bottom, + ) { Text( - text = symbol, - fontSize = 16.sp, + text = absQuantity.stripTrailingZeros().toPlainString(), + fontSize = 34.sp, fontWeight = FontWeight.W500, + fontFamily = FontFamily(Font(R.font.mixin_font)), color = MixinAppTheme.colors.textPrimary, - modifier = Modifier.padding(bottom = 4.dp) ) + val symbol = openedOrder.tokenSymbol?.takeIf { it.isNotBlank() } + if (symbol != null) { + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = symbol, + fontSize = 16.sp, + fontWeight = FontWeight.W500, + color = MixinAppTheme.colors.textPrimary, + modifier = Modifier.padding(bottom = 4.dp) + ) + } } - } - Spacer(modifier = Modifier.height(10.dp)) - Box( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(leverageBackgroundColor) - .padding(horizontal = 8.dp, vertical = 2.5.dp) - .align(Alignment.CenterHorizontally) - ) { - Text( - text = "$sideText ${leverage}x", - color = leverageTextColor, - fontSize = 14.sp - ) + Spacer(modifier = Modifier.height(10.dp)) + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(leverageBackgroundColor) + .padding(horizontal = 8.dp, vertical = 2.5.dp) + ) { + Text( + text = "$sideText ${leverage}x", + color = leverageTextColor, + fontSize = 14.sp + ) + } } Spacer(modifier = Modifier.height(20.dp)) @@ -921,7 +965,8 @@ fun OpenedOrderDetailPage( PositionDetailItem( label = stringResource(R.string.Perpetual).uppercase(), value = openedOrder.displaySymbol ?: openedOrder.tokenSymbol ?: "Unknown", - icon = openedOrder.iconUrl + icon = openedOrder.iconUrl, + onClick = onViewMarket ) Spacer(modifier = Modifier.height(20.dp)) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 17517bd35d..b81886af8a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1157,4 +1157,43 @@ 輸入 Base58 或 64 字節 HEX 格式私鑰,例如 3fZb...9Qp 或 0x1a2b...cdef 輸入 WIF 或 32 字節 HEX 格式私鑰,例如 Kx45...9Ab 或 0x1a2b...cdef 輸入 HEX 格式私鑰,例如 0x1a2b...cdef + ✅ 賬號已升級 + ✅ 助記詞已匯出 + ✅ 手機號已修改 + ✅ PIN 已修改 + ❌ PIN 不正確 + ✅ PIN 已設定 + ✅ 恢復聯絡人已修改 + ✅ 已登入 + ✅ 已通過助記詞登入 + ✅ 已通過手機號登入 + ✅ 已通過恢復聯絡人登入 + ✅ 已退出登入 + ✅ 已退出登入 + 新增地址 + 新增恢復聯絡人 + 授權給機器人 + 修改手機號 + 修改 PIN + 修改恢復聯絡人 + 刪除賬號 + 刪除地址 + 刪除恢復聯絡人 + 匯出助記詞 + 鑄造藏品 + 分解藏品 + 撤銷多重簽名交易 + 設定 PIN + 登入桌面端 + 登入客戶端 + 簽名多重簽名交易 + 退出桌面端 + 退出客戶端 + 轉賬 + 轉移機器人的所有權 + 修改手機號 + 升級 Safe + 驗證 PIN + 檢視恢復聯絡人 + 提現 diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/trade/SwapRecommendedMarketCardsTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/trade/SwapRecommendedMarketCardsTest.kt index 9727911ac6..b13bf6633e 100644 --- a/app/src/test/java/one/mixin/android/ui/home/web3/trade/SwapRecommendedMarketCardsTest.kt +++ b/app/src/test/java/one/mixin/android/ui/home/web3/trade/SwapRecommendedMarketCardsTest.kt @@ -58,6 +58,65 @@ class SwapRecommendedMarketCardsTest { ) } + @Test + fun openOrdersShowOnlyWhenAdvancedInputsAreEmptyAndNotFocused() { + assertTrue( + shouldShowLimitOrderOpenOrdersCard( + inputText = "", + outputText = "", + limitPriceText = "", + focusedField = FocusedField.NONE, + isKeyboardVisible = false, + ) + ) + + assertFalse( + shouldShowLimitOrderOpenOrdersCard( + inputText = "", + outputText = "", + limitPriceText = "", + focusedField = FocusedField.PRICE, + isKeyboardVisible = false, + ) + ) + assertFalse( + shouldShowLimitOrderOpenOrdersCard( + inputText = "1", + outputText = "", + limitPriceText = "", + focusedField = FocusedField.NONE, + isKeyboardVisible = false, + ) + ) + assertFalse( + shouldShowLimitOrderOpenOrdersCard( + inputText = "", + outputText = "1", + limitPriceText = "", + focusedField = FocusedField.NONE, + isKeyboardVisible = false, + ) + ) + assertFalse( + shouldShowLimitOrderOpenOrdersCard( + inputText = "", + outputText = "", + limitPriceText = "1", + focusedField = FocusedField.NONE, + isKeyboardVisible = false, + ) + ) + assertFalse( + shouldShowLimitOrderOpenOrdersCard( + inputText = "", + outputText = "", + limitPriceText = "", + focusedField = FocusedField.NONE, + isKeyboardVisible = true, + ) + ) + } + @Test fun recommendedMarketPriceUsesCompactFixedDecimals() { assertEquals("$1.23", formatRecommendedMarketFiatPrice(BigDecimal("1.234"), "$")) diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/trade/TradeInputTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/trade/TradeInputTest.kt index 8a29dd5092..2e1389847c 100644 --- a/app/src/test/java/one/mixin/android/ui/home/web3/trade/TradeInputTest.kt +++ b/app/src/test/java/one/mixin/android/ui/home/web3/trade/TradeInputTest.kt @@ -48,4 +48,12 @@ class TradeInputTest { limitTradeInputDecimalPlaces("12.123456789", maxDecimalPlaces = null) ) } + + @Test + fun swapInputPrecisionChangePreservesCurrentInput() { + assertEquals( + "12.123456", + swapInputTextForMaxDecimalPlacesChange("12.123456789", maxDecimalPlaces = 6) + ) + } } diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsRouteNavigatorTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsRouteNavigatorTest.kt new file mode 100644 index 0000000000..49b66ac1da --- /dev/null +++ b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/PerpsRouteNavigatorTest.kt @@ -0,0 +1,45 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import android.os.Bundle +import android.widget.FrameLayout +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import one.mixin.android.R +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class PerpsRouteNavigatorTest { + @Test + fun routeTransactionAddsBackStackEntry() { + val activity = Robolectric.buildActivity(TestActivity::class.java).setup().get() + val fragmentManager = activity.supportFragmentManager + + val method = PerpsRouteNavigator::class.java.getDeclaredMethod( + "addRoute", + FragmentManager::class.java, + Fragment::class.java, + String::class.java, + ) + method.isAccessible = true + method.invoke(PerpsRouteNavigator, fragmentManager, Fragment(), "test_route") + fragmentManager.executePendingTransactions() + + assertEquals(1, fragmentManager.backStackEntryCount) + } + + class TestActivity : FragmentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView( + FrameLayout(this).apply { + id = R.id.container + } + ) + } + } +}