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

@ -194,13 +194,14 @@
<string name="common_close">Close</string>
<string name="exchange_send_view_header">You Send</string>
<string name="exchange_receive_view_header">You Receive</string>
<string name="swapping_generic_error">There was an error. Please try again.</string>
<string name="swapping_permission_header">Give Permission</string>
<string name="swapping_permission_subheader">To continue you need to allow 1inch smart contracts to use your %s</string>
<string name="swapping_permission_rows_amount">Amount %s</string>
<string name="swapping_permission_rows_your_wallet">Your Wallet</string>
<string name="swapping_permission_rows_spender">Spender</string>
<string name="swapping_permission_buttons_approve">Approve</string>
<string name="swapping_swap_of_to">Swap of %s to</string>
<string name="swapping_swap_of_to">Swap of %s to %s</string>
<string name="swapping_swap">Swap</string>
<string name="swapping_insufficient_funds">Insufficient funds</string>
<string name="swapping_give_permission">Give Permission</string>

View file

@ -194,13 +194,14 @@
<string name="common_close">Close</string>
<string name="exchange_send_view_header">You Send</string>
<string name="exchange_receive_view_header">You Receive</string>
<string name="swapping_generic_error">There was an error. Please try again.</string>
<string name="swapping_permission_header">Give Permission</string>
<string name="swapping_permission_subheader">To continue you need to allow 1inch smart contracts to use your %s</string>
<string name="swapping_permission_rows_amount">Amount %s</string>
<string name="swapping_permission_rows_your_wallet">Your Wallet</string>
<string name="swapping_permission_rows_spender">Spender</string>
<string name="swapping_permission_buttons_approve">Approve</string>
<string name="swapping_swap_of_to">Swap of %s to</string>
<string name="swapping_swap_of_to">Swap of %s to %s</string>
<string name="swapping_swap">Swap</string>
<string name="swapping_insufficient_funds">Insufficient funds</string>
<string name="swapping_give_permission">Give Permission</string>

View file

@ -194,13 +194,14 @@
<string name="common_close">Close</string>
<string name="exchange_send_view_header">You Send</string>
<string name="exchange_receive_view_header">You Receive</string>
<string name="swapping_generic_error">There was an error. Please try again.</string>
<string name="swapping_permission_header">Give Permission</string>
<string name="swapping_permission_subheader">To continue you need to allow 1inch smart contracts to use your %s</string>
<string name="swapping_permission_rows_amount">Amount %s</string>
<string name="swapping_permission_rows_your_wallet">Your Wallet</string>
<string name="swapping_permission_rows_spender">Spender</string>
<string name="swapping_permission_buttons_approve">Approve</string>
<string name="swapping_swap_of_to">Swap of %s to</string>
<string name="swapping_swap_of_to">Swap of %s to %s</string>
<string name="swapping_swap">Swap</string>
<string name="swapping_insufficient_funds">Insufficient funds</string>
<string name="swapping_give_permission">Give Permission</string>

View file

@ -194,13 +194,14 @@
<string name="common_close">Закрыть</string>
<string name="exchange_send_view_header">Вы отправляете</string>
<string name="exchange_receive_view_header">Вы получаете</string>
<string name="swapping_generic_error">Произошла ошибка. Пожалуйста, попробуйте еще раз.</string>
<string name="swapping_permission_header">Дать разрешение</string>
<string name="swapping_permission_subheader">Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s</string>
<string name="swapping_permission_rows_amount">Количество %s</string>
<string name="swapping_permission_rows_your_wallet">Ваш кошелек</string>
<string name="swapping_permission_rows_spender">Отправитель</string>
<string name="swapping_permission_buttons_approve">Подтвердить</string>
<string name="swapping_swap_of_to">Обмен %s на</string>
<string name="swapping_swap_of_to">Обмен %s на %s</string>
<string name="swapping_swap">Обмен</string>
<string name="swapping_insufficient_funds">Недостаточно средств</string>
<string name="swapping_give_permission">Дать разрешение</string>

View file

@ -194,13 +194,14 @@
<string name="common_close">Close</string>
<string name="exchange_send_view_header">You Send</string>
<string name="exchange_receive_view_header">You Receive</string>
<string name="swapping_generic_error">There was an error. Please try again.</string>
<string name="swapping_permission_header">Give Permission</string>
<string name="swapping_permission_subheader">To continue you need to allow 1inch smart contracts to use your %s</string>
<string name="swapping_permission_rows_amount">Amount %s</string>
<string name="swapping_permission_rows_your_wallet">Your Wallet</string>
<string name="swapping_permission_rows_spender">Spender</string>
<string name="swapping_permission_buttons_approve">Approve</string>
<string name="swapping_swap_of_to">Swap of %s to</string>
<string name="swapping_swap_of_to">Swap of %s to %s</string>
<string name="swapping_swap">Swap</string>
<string name="swapping_insufficient_funds">Insufficient funds</string>
<string name="swapping_give_permission">Give Permission</string>

View file

@ -0,0 +1,173 @@
package com.tangem.core.ui.components
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.TextUnit
/**
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
*/
@Composable
fun MiddleEllipsisText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
softWrap: Boolean = true,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
) {
// some letters, like "r", will have less width when placed right before "."
// adding a space to prevent such case
val layoutText = remember(text) { "$text $ellipsisText" }
val textLayoutResultState = remember(layoutText) {
mutableStateOf<TextLayoutResult?>(null)
}
SubcomposeLayout(modifier) { constraints ->
// result is ignored - we only need to fill our textLayoutResult
subcompose("measure") {
Text(
text = layoutText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
maxLines = 1,
onTextLayout = { textLayoutResultState.value = it },
style = style,
)
}.first().measure(Constraints())
// to allow smart cast
val textLayoutResult = textLayoutResultState.value
?: // shouldn't happen - onTextLayout is called before subcompose finishes
return@SubcomposeLayout layout(0, 0) {}
val placeable = subcompose("visible") {
val finalText = remember(text, textLayoutResult, constraints.maxWidth) {
if (text.isEmpty() || textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth) {
// text not including ellipsis fits on the first line.
return@remember text
}
val ellipsisWidth = layoutText.indices.toList()
.takeLast(ellipsisCharactersCount)
.let widthLet@{ indices ->
// fix this bug: https://issuetracker.google.com/issues/197146630
// in this case width is invalid
for (i in indices) {
val width = textLayoutResult.getBoundingBox(i).width
if (width > 0) {
return@widthLet width * ellipsisCharactersCount
}
}
// this should not happen, because
// this error occurs only for the last character in the string
throw IllegalStateException("all ellipsis chars have invalid width")
}
val availableWidth = constraints.maxWidth - ellipsisWidth
val startCounter = BoundCounter(text, textLayoutResult) { it }
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
while (availableWidth - startCounter.width - endCounter.width > 0) {
val possibleEndWidth = endCounter.widthWithNextChar()
if (
startCounter.width >= possibleEndWidth
&& availableWidth - startCounter.width - possibleEndWidth >= 0
) {
endCounter.addNextChar()
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
startCounter.addNextChar()
} else {
break
}
}
startCounter.string.trimEnd() + ellipsisText + endCounter.string.reversed().trimStart()
}
Text(
text = finalText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
onTextLayout = onTextLayout,
style = style,
)
}[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
private const val ellipsisCharactersCount = 3
private const val ellipsisCharacter = '.'
private val ellipsisText = List(ellipsisCharactersCount) { ellipsisCharacter }.joinToString(separator = "")
private class BoundCounter(
private val text: String,
private val textLayoutResult: TextLayoutResult,
private val charPosition: (Int) -> Int,
) {
var string = ""
private set
var width = 0f
private set
private var _nextCharWidth: Float? = null
private var invalidCharsCount = 0
fun widthWithNextChar(): Float =
width + nextCharWidth()
private fun nextCharWidth(): Float =
_nextCharWidth ?: run {
var boundingBox: Rect
// invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630
invalidCharsCount--
do {
boundingBox = textLayoutResult
.getBoundingBox(charPosition(string.count() + ++invalidCharsCount))
} while (boundingBox.right == 0f)
_nextCharWidth = boundingBox.width
boundingBox.width
}
fun addNextChar() {
string += text[charPosition(string.count())]
width += nextCharWidth()
_nextCharWidth = null
}
}

View file

@ -23,6 +23,7 @@ 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.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
@ -46,15 +47,15 @@ import com.tangem.core.ui.res.TangemTheme
*/
@Composable
fun ResultScreenContent(
resultMessage: String,
onButtonClick: () -> Unit,
modifier: Modifier = Modifier,
resultMessage: AnnotatedString,
@StringRes title: Int = R.string.common_success,
resultColor: Color = TangemTheme.colors.icon.accent,
@DrawableRes icon: Int = R.drawable.ic_check_24,
@DrawableRes secondaryButtonIcon: Int? = null,
@StringRes secondaryButtonText: Int? = null,
onSecondaryButtonClick: (() -> Unit)? = null,
onButtonClick: () -> Unit,
) {
Column(
modifier = modifier
@ -92,6 +93,8 @@ fun ResultScreenContent(
secondaryButtonText = secondaryButtonText,
secondaryButtonIcon = secondaryButtonIcon,
onSecondaryButtonClick = onSecondaryButtonClick,
modifier = Modifier
.fillMaxWidth(),
)
SpacerH12()
}
@ -140,22 +143,23 @@ fun SuccessImage(
@Composable
private fun SecondaryButtonForResultScreen(
modifier: Modifier = Modifier,
@StringRes secondaryButtonText: Int,
onSecondaryButtonClick: () -> Unit,
@DrawableRes secondaryButtonIcon: Int? = null,
onSecondaryButtonClick: (() -> Unit),
) {
if (secondaryButtonIcon != null) {
SecondaryButtonIconLeft(
text = stringResource(id = secondaryButtonText),
icon = painterResource(id = secondaryButtonIcon),
onClick = onSecondaryButtonClick,
modifier = Modifier.fillMaxWidth(),
modifier = modifier,
)
} else {
SecondaryButton(
text = stringResource(id = secondaryButtonText),
onClick = onSecondaryButtonClick,
modifier = Modifier.fillMaxWidth(),
modifier = modifier,
)
}
}
@ -165,7 +169,7 @@ private fun SecondaryButtonForResultScreen(
@Composable
private fun SuccessScreenPreview() {
ResultScreenContent(
resultMessage = "Swap of 1 000 DAI to 1 131,46 MATIC",
resultMessage = AnnotatedString("Swap of 1 000 DAI to 1 131,46 MATIC"),
secondaryButtonText = R.string.swapping_success_view_explorer_button_title,
onSecondaryButtonClick = {},
onButtonClick = {},
@ -174,7 +178,7 @@ private fun SuccessScreenPreview() {
@Preview(showBackground = true)
@Composable
private fun Preview_SuccessScreenContent_InLightTheme() {
fun Preview_SuccessScreenContent_InLightTheme() {
TangemTheme(isDark = false) {
SuccessScreenPreview()
}
@ -182,7 +186,7 @@ private fun Preview_SuccessScreenContent_InLightTheme() {
@Preview(showBackground = true)
@Composable
private fun Preview_SuccessScreenContent_InDarkTheme() {
fun Preview_SuccessScreenContent_InDarkTheme() {
TangemTheme(isDark = true) {
SuccessScreenPreview()
}

View file

@ -1,12 +1,12 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@ -25,11 +25,13 @@ import com.tangem.core.ui.res.TangemTheme
*/
@Composable
fun WarningCard(
modifier: Modifier = Modifier,
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
) {
WarningCardSurface(
modifier = modifier,
content = {
WarningBody(
title = title,
@ -52,12 +54,14 @@ fun WarningCard(
*/
@Composable
fun ClickableWarningCard(
modifier: Modifier = Modifier,
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
onClick: () -> Unit,
) {
WarningCardSurface(
modifier = modifier,
content = {
WarningBody(title = title, description = description, icon = icon) {
SpacerW12()
@ -84,12 +88,14 @@ fun ClickableWarningCard(
*/
@Composable
fun RefreshableWaringCard(
modifier: Modifier = Modifier,
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
onClick: () -> Unit,
) {
WarningCardSurface(
modifier = modifier,
content = {
WarningBody(title = title, description = description, icon = icon) {
SpacerW12()
@ -108,6 +114,7 @@ fun RefreshableWaringCard(
@Composable
private fun WarningBody(
modifier: Modifier = Modifier,
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
@ -126,19 +133,20 @@ private fun WarningBody(
)
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun WarningCardSurface(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
Surface(
Card(
shape = RoundedCornerShape(TangemTheme.dimens.size12),
color = TangemTheme.colors.background.primary,
backgroundColor = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
modifier = Modifier.clickable(
enabled = onClick != null,
onClick = onClick ?: {},
),
onClick = { onClick ?: Unit },
enabled = onClick != null,
modifier = modifier,
) {
content()
}

View file

@ -45,8 +45,8 @@ import com.tangem.core.ui.res.TangemTheme
* @param expandedInitially whether the search is expanded on launch
* @param tint tint for most of the visual elements of toolbar
* @param onBackClick action when close button is clicked
* @param onSearchChange action when search is modified
* @param onSearchDisplayClose action when search is closed
* @param onSearchChanged action when search is modified
* @param onSearchDisplayClosed action when search is closed
*
* @see <a href =
* "https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=1123%3A4068&t=xj8BBj5DfCWn2Mli-1"
@ -54,13 +54,14 @@ import com.tangem.core.ui.res.TangemTheme
*/
@Composable
fun ExpandableSearchView(
onBackClick: () -> Unit,
onSearchChange: (String) -> Unit,
onSearchDisplayClose: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
placeholderSearchText: String = "",
expandedInitially: Boolean = false,
tint: Color = TangemTheme.colors.text.primary1,
tint: Color = TangemTheme.colors.icon.primary1,
onBackClick: () -> Unit,
onSearchChanged: (String) -> Unit,
onSearchDisplayClosed: () -> Unit,
) {
val (expanded, onExpandedChanged) = remember {
mutableStateOf(expandedInitially)
@ -70,16 +71,18 @@ fun ExpandableSearchView(
if (isSearchFieldVisible) {
ExpandedSearchView(
placeholderSearchText = placeholderSearchText,
onSearchChange = onSearchChange,
onSearchDisplayClose = onSearchDisplayClose,
onExpandedChange = onExpandedChanged,
onSearchChanged = onSearchChanged,
onSearchDisplayClosed = onSearchDisplayClosed,
onExpandedChanged = onExpandedChanged,
modifier = modifier,
tint = tint,
)
} else {
CollapsedSearchView(
title = title,
onBackClick = onBackClick,
onExpandedChange = onExpandedChanged,
onExpandedChanged = onExpandedChanged,
modifier = modifier,
tint = tint,
)
}
@ -88,14 +91,15 @@ fun ExpandableSearchView(
@Composable
private fun CollapsedSearchView(
onBackClick: () -> Unit,
onExpandedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
tint: Color = TangemTheme.colors.background.primary,
onBackClick: () -> Unit,
onExpandedChanged: (Boolean) -> Unit,
tint: Color,
) {
Row(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.padding(TangemTheme.dimens.spacing16)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
@ -123,7 +127,7 @@ private fun CollapsedSearchView(
tint = tint,
contentDescription = null,
modifier = Modifier
.clickable { onExpandedChange(true) },
.clickable { onExpandedChanged(true) },
)
}
}
@ -131,10 +135,11 @@ private fun CollapsedSearchView(
@Composable
private fun ExpandedSearchView(
placeholderSearchText: String,
onSearchChange: (String) -> Unit,
onSearchDisplayClose: () -> Unit,
onExpandedChange: (Boolean) -> Unit,
tint: Color = TangemTheme.colors.background.primary,
onSearchChanged: (String) -> Unit,
onSearchDisplayClosed: () -> Unit,
onExpandedChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
tint: Color,
) {
val focusManager = LocalFocusManager.current
val textFieldFocusRequester = remember { FocusRequester() }
@ -146,16 +151,16 @@ private fun ExpandedSearchView(
var textFieldValue by remember { mutableStateOf(TextFieldValue("", TextRange("".length))) }
Row(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
onClick = {
onExpandedChange(false)
onSearchDisplayClose()
onExpandedChanged(false)
onSearchDisplayClosed()
},
) {
Icon(
@ -168,7 +173,7 @@ private fun ExpandedSearchView(
value = textFieldValue,
onValueChange = {
textFieldValue = it
onSearchChange(it.text)
onSearchChanged(it.text)
},
singleLine = true,
modifier = Modifier
@ -200,8 +205,8 @@ private fun CollapsedSearchViewPreview() {
title = "Choose Token",
onBackClick = {},
placeholderSearchText = "Search",
onSearchChange = {},
onSearchDisplayClose = {},
onSearchChanged = {},
onSearchDisplayClosed = {},
)
}
}
@ -214,9 +219,9 @@ private fun ExpandedSearchViewPreview() {
title = "Choose Token",
onBackClick = {},
placeholderSearchText = "Search",
onSearchChange = {},
onSearchChanged = {},
expandedInitially = true,
onSearchDisplayClose = {},
onSearchDisplayClosed = {},
)
}
}

View file

@ -56,6 +56,7 @@ dependencies {
implementation(AndroidX.appCompat)
implementation(AndroidX.fragmentKtx)
implementation(AndroidX.lifecycleViewModelKtx)
implementation(AndroidX.browser)
/** Compose */
implementation(Compose.foundation)

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) {