Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-18 17:51:32 +04:00
parent 9c1931f7d7
commit cfcc704666
21 changed files with 437 additions and 142 deletions

View file

@ -67,7 +67,8 @@ sealed interface TransactionCardType {
sealed interface SwapWarning {
data class PermissionNeeded(val tokenCurrency: String) : SwapWarning
data class GenericWarning(val message: String, val onClick: () -> Unit) : SwapWarning
data class InsufficientFunds(val tokenCurrency: String) : SwapWarning
data class GenericWarning(val message: String?, val onClick: () -> Unit) : SwapWarning
// data class RateExpired(val onClick: () -> Unit) : SwapWarning
// object HighPriceImpact : SwapWarning
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.models
data class SwapSuccessStateHolder(
val message: String,
val fromTokenAmount: String,
val toTokenAmount: String,
val onSecondaryButtonClick: () -> Unit,
)

View file

@ -4,11 +4,14 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.compose.animation.Crossfade
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.tangem.feature.swap.router.CustomTabsManager
import com.tangem.feature.swap.router.SwapRouter
import com.tangem.feature.swap.router.SwapScreen
import com.tangem.feature.swap.ui.SwapScreen
@ -25,35 +28,53 @@ class SwapFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
viewModel.setRouter(SwapRouter(fragmentManager = WeakReference(parentFragmentManager)))
viewModel.setRouter(
SwapRouter(
fragmentManager = WeakReference(parentFragmentManager),
customTabsManager = CustomTabsManager(WeakReference(activity?.application)),
),
)
activity?.onBackPressedDispatcher?.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
viewModel.uiState.onBackClicked()
}
},
)
return ComposeView(inflater.context).apply {
setContent {
Crossfade(targetState = viewModel.currentScreen) { screen ->
when (screen) {
SwapScreen.Main -> SwapScreen(stateHolder = viewModel.uiState)
SwapScreen.Success -> {
val successState = viewModel.uiState.successState
if (successState != null) {
SwapSuccessScreen(state = successState, viewModel.uiState.onBackClicked)
} else {
SwapScreen(stateHolder = viewModel.uiState)
}
}
SwapScreen.SelectToken -> {
val tokenState = viewModel.uiState.selectTokenState
if (tokenState != null) {
SwapSelectTokenScreen(
state = tokenState,
onBack = viewModel.uiState.onBackClicked,
)
} else {
SwapScreen(stateHolder = viewModel.uiState)
}
}
// screen
ScreenContent(viewModel = viewModel)
}
}
}
@Composable
private fun ScreenContent(viewModel: SwapViewModel) {
Crossfade(targetState = viewModel.currentScreen) { screen ->
when (screen) {
SwapScreen.Main -> SwapScreen(stateHolder = viewModel.uiState)
SwapScreen.Success -> {
val successState = viewModel.uiState.successState
if (successState != null) {
SwapSuccessScreen(state = successState, viewModel.uiState.onBackClicked)
} else {
SwapScreen(stateHolder = viewModel.uiState)
}
}
SwapScreen.SelectToken -> {
val tokenState = viewModel.uiState.selectTokenState
if (tokenState != null) {
SwapSelectTokenScreen(
state = tokenState,
onBack = viewModel.uiState.onBackClicked,
)
} else {
SwapScreen(stateHolder = viewModel.uiState)
}
}
}
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.feature.swap.router
import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import java.lang.ref.WeakReference
class CustomTabsManager(private val context: WeakReference<Context>) {
fun openUrl(url: String) {
val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.build(),
)
.build()
context.get()?.let { customTabsIntent.launchUrl(it, Uri.parse(url)) }
}
}

View file

@ -8,6 +8,7 @@ import java.lang.ref.WeakReference
internal class SwapRouter(
private val fragmentManager: WeakReference<FragmentManager>,
private val customTabsManager: CustomTabsManager,
) {
var currentScreen by mutableStateOf(SwapScreen.Main)
@ -24,6 +25,10 @@ internal class SwapRouter(
fragmentManager.get()?.popBackStack()
}
}
fun openUrl(url: String) {
customTabsManager.openUrl(url)
}
}
enum class SwapScreen {

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.MiddleEllipsisText
import com.tangem.core.ui.components.PrimaryButtonIconRight
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH10
@ -151,11 +152,11 @@ private fun InformationItem(subtitle: String, value: String) {
SpacerH16()
Text(
MiddleEllipsisText(
text = value,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
maxLines = 1,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetState
@ -8,6 +9,7 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
@ -26,6 +28,7 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
TangemTheme {
BottomSheetScaffold(
modifier = Modifier.background(TangemTheme.colors.background.secondary),
topBar = {
AppBarWithBackButton(
text = stringResource(R.string.swapping_swap),
@ -34,8 +37,8 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
)
},
sheetContent = {
SwapPermissionBottomSheetContent(
data = stateHolder.permissionState as? SwapPermissionState.ReadyForRequest,
SwapPermissionBottomSheetContent(
data = stateHolder.permissionState as? SwapPermissionState.ReadyForRequest,
onCancel = { coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.collapse() } },
)
},

View file

@ -18,13 +18,11 @@ import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -32,6 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.CardWithIcon
import com.tangem.core.ui.components.ClickableWarningCard
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconRight
import com.tangem.core.ui.components.RefreshableWaringCard
import com.tangem.core.ui.components.SimpleOkDialog
@ -57,13 +56,12 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Transparent),
.background(color = TangemTheme.colors.background.secondary),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.primary)
.verticalScroll(rememberScrollState())
.padding(
start = TangemTheme.dimens.spacing16,
@ -101,14 +99,24 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
)
}
PrimaryButtonIconRight(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.swapping_swap),
icon = painterResource(id = R.drawable.ic_tangem_24),
enabled = state.swapButton.enabled,
showProgress = state.swapButton.loading,
onClick = state.swapButton.onClick,
)
if (state.warnings.any { it is SwapWarning.InsufficientFunds }) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.swapping_insufficient_funds),
enabled = false,
showProgress = state.swapButton.loading,
onClick = state.swapButton.onClick,
)
} else {
PrimaryButtonIconRight(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.swapping_swap),
icon = painterResource(id = R.drawable.ic_tangem_24),
enabled = state.swapButton.enabled,
showProgress = state.swapButton.loading,
onClick = state.swapButton.onClick,
)
}
}
AnimatedVisibility(
@ -134,7 +142,10 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
}
if (state.alert != null) {
SimpleOkDialog(message = state.alert.message, onDismissDialog = state.alert.onClick)
SimpleOkDialog(
message = state.alert.message ?: stringResource(id = R.string.swapping_generic_error),
onDismissDialog = state.alert.onClick,
)
}
}
}
@ -222,7 +233,7 @@ private fun SwapWarnings(
) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colors.primary)
.background(color = TangemTheme.colors.background.secondary)
.fillMaxWidth(),
) {
warnings.forEach { warning ->
@ -250,10 +261,11 @@ private fun SwapWarnings(
is SwapWarning.GenericWarning -> {
RefreshableWaringCard(
title = stringResource(id = R.string.common_warning),
description = warning.message,
description = warning.message ?: stringResource(id = R.string.swapping_generic_error),
onClick = warning.onClick,
)
}
else -> {}
// is SwapWarning.RateExpired -> {
// RefreshableWaringCard(
// title = stringResource(id = R.string.),

View file

@ -49,10 +49,11 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit)
title = stringResource(R.string.swapping_token_list_your_title),
onBackClick = onBack,
placeholderSearchText = stringResource(id = R.string.search_tokens_title),
onSearchChange = state.onSearchEntered,
onSearchDisplayClose = { state.onSearchEntered("") },
onSearchChanged = state.onSearchEntered,
onSearchDisplayClosed = { state.onSearchEntered("") },
)
},
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
)
}
}
@ -61,12 +62,8 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit)
private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) {
LazyColumn(
modifier = modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.primary)
.padding(
start = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32,
),
.background(color = TangemTheme.colors.background.secondary)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@ -85,16 +82,17 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
}
@Composable
private fun TokenItem(token: TokenToSelect, onTokenClick: () -> Unit) {
private fun TokenItem(token: TokenToSelect, onTokenClick: (String) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing14,
end = TangemTheme.dimens.spacing16,
.clickable(
onClick = { onTokenClick(token.id) },
)
.clickable(onClick = onTokenClick),
.padding(
vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing16,
),
verticalAlignment = Alignment.CenterVertically,
) {
TokenIcon(token = token)

View file

@ -5,6 +5,9 @@ import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.ResultScreenContent
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
@ -18,7 +21,10 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) {
Scaffold(
content = { padding ->
ResultScreenContent(
resultMessage = state.message,
resultMessage = makeSuccessMessage(
fromTokenAmount = state.fromTokenAmount,
toTokenAmount = state.toTokenAmount,
),
resultColor = TangemTheme.colors.icon.attention,
onButtonClick = onBack,
icon = R.drawable.ic_clock_24,
@ -40,10 +46,24 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) {
}
}
@Composable
private fun makeSuccessMessage(fromTokenAmount: String, toTokenAmount: String): AnnotatedString {
val message = stringResource(id = R.string.swapping_swap_of_to, fromTokenAmount, toTokenAmount)
return buildAnnotatedString {
append(message)
addStyle(
style = SpanStyle(color = TangemTheme.colors.text.accent),
start = message.indexOf(toTokenAmount),
end = message.length,
)
}
}
// region preview
private val state = SwapSuccessStateHolder(
message = "Swap of 1 000 DAI to 1 131,46 MATIC",
fromTokenAmount = "1 000 DAI",
toTokenAmount = "1 131,46 MATIC",
) {}
@Preview(showBackground = true)

View file

@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
@ -23,17 +24,22 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@ -52,7 +58,6 @@ import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.CurrencyPlaceholderIcon
import com.tangem.core.ui.components.FontSizeRange
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.SpacerH4
@ -63,6 +68,7 @@ import com.valentinilk.shimmer.shimmer
@Suppress("LongParameterList")
@Composable
fun TransactionCard(
modifier: Modifier = Modifier,
type: TransactionCardType,
balance: String,
amount: String?,
@ -73,6 +79,7 @@ fun TransactionCard(
onChangeTokenClick: (() -> Unit)? = null,
) {
Card(
modifier = modifier,
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
backgroundColor = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
@ -112,7 +119,10 @@ fun TransactionCard(
.align(Alignment.CenterEnd)
.height(TangemTheme.dimens.size116)
.width(TangemTheme.dimens.size102)
.clickable { onChangeTokenClick() },
.clickable(
indication = rememberRipple(bounded = false),
interactionSource = remember { MutableInteractionSource() },
) { onChangeTokenClick() },
)
}
}
@ -209,7 +219,7 @@ private fun Content(
is TransactionCardType.SendCard -> {
AutoSizeTextField(
amount = amount ?: "1",
onAmountChange = { type.onAmountChanged(it) },
onAmoutChanged = { type.onAmountChanged(it) },
)
}
}
@ -245,7 +255,7 @@ private fun Content(
@Suppress("MagicNumber")
@Composable
private fun AutoSizeTextField(amount: String, onAmountChange: (String) -> Unit) {
private fun AutoSizeTextField(amount: String, onAmoutChanged: (String) -> Unit) {
val focusManager = LocalFocusManager.current
val textFieldFocusRequester = remember { FocusRequester() }
@ -272,31 +282,39 @@ private fun AutoSizeTextField(amount: String, onAmountChange: (String) -> Unit)
intrinsics = calculateIntrinsics()
}
}
BasicTextField(
value = amount,
onValueChange = {
onAmountChange(it)
},
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.focusRequester(textFieldFocusRequester),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Decimal,
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
},
),
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
fontSize = shrunkFontSize,
),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
val customTextSelectionColors = TextSelectionColors(
handleColor = Transparent,
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
)
CompositionLocalProvider(
LocalTextSelectionColors provides customTextSelectionColors,
) {
BasicTextField(
value = amount,
onValueChange = {
onAmoutChanged(it)
},
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.focusRequester(textFieldFocusRequester),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Decimal,
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
},
),
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
fontSize = shrunkFontSize,
),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
)
}
}
}
@ -333,8 +351,8 @@ fun Token(
.crossfade(true)
.build(),
loading = { TokenImageShimmer(modifier = tokenImageModifier) },
error = { CurrencyPlaceholderIcon(modifier = tokenImageModifier, id = tokenCurrency) },
contentDescription = null,
// error = { CurrencyPlaceholderIcon(modifier = tokenImageModifier, tokenCurrency) },
contentDescription = tokenCurrency,
)
if (networkIconRes != null) {