diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index e62a78ccb6..606fc12069 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -194,13 +194,14 @@ Close You Send You Receive + There was an error. Please try again. Give Permission To continue you need to allow 1inch smart contracts to use your %s Amount %s Your Wallet Spender Approve - Swap of %s to + Swap of %s to %s Swap Insufficient funds Give Permission diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index ada856b314..f4973b4b18 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -194,13 +194,14 @@ Close You Send You Receive + There was an error. Please try again. Give Permission To continue you need to allow 1inch smart contracts to use your %s Amount %s Your Wallet Spender Approve - Swap of %s to + Swap of %s to %s Swap Insufficient funds Give Permission diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index d1e4f054a7..216dc84c8b 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -194,13 +194,14 @@ Close You Send You Receive + There was an error. Please try again. Give Permission To continue you need to allow 1inch smart contracts to use your %s Amount %s Your Wallet Spender Approve - Swap of %s to + Swap of %s to %s Swap Insufficient funds Give Permission diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 9fa9a20bee..b27bc3b7f7 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -194,13 +194,14 @@ Закрыть Вы отправляете Вы получаете + Произошла ошибка. Пожалуйста, попробуйте еще раз. Дать разрешение Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s Количество %s Ваш кошелек Отправитель Подтвердить - Обмен %s на + Обмен %s на %s Обмен Недостаточно средств Дать разрешение diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8a2711bb79..9614432e50 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -194,13 +194,14 @@ Close You Send You Receive + There was an error. Please try again. Give Permission To continue you need to allow 1inch smart contracts to use your %s Amount %s Your Wallet Spender Approve - Swap of %s to + Swap of %s to %s Swap Insufficient funds Give Permission diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt new file mode 100644 index 0000000000..3feb9ca15c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt @@ -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(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 + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SuccessScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SuccessScreen.kt index af3482b826..29d24784d5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/SuccessScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/SuccessScreen.kt @@ -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() } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt index 275531f94f..989b165a26 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt @@ -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() } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt index dfd9fba0e0..6ccbf2c858 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt @@ -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 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 = {}, ) } } diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 747a8690a8..ab809d50b1 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(AndroidX.appCompat) implementation(AndroidX.fragmentKtx) implementation(AndroidX.lifecycleViewModelKtx) + implementation(AndroidX.browser) /** Compose */ implementation(Compose.foundation) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 391a4fdfc4..1dc5412e8c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -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 } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index 5b6a026b1f..56dc3525a8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -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, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 71f56935ef..e7080813fa 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -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) } } - } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt new file mode 100644 index 0000000000..2ca16d01e8 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt @@ -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) { + fun openUrl(url: String) { + val customTabsIntent = CustomTabsIntent.Builder() + .setDefaultColorSchemeParams( + CustomTabColorSchemeParams.Builder() + .build(), + ) + .build() + context.get()?.let { customTabsIntent.launchUrl(it, Uri.parse(url)) } + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt index bf3f7446fd..f29bef3bbc 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt @@ -8,6 +8,7 @@ import java.lang.ref.WeakReference internal class SwapRouter( private val fragmentManager: WeakReference, + 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 { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt index 61c9aacf7c..526001e25a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt @@ -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), ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index d93ee56774..733c699f43 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -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() } }, ) }, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 52b0e8963c..045371003d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -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.), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 2c210be72d..bfbf78fd86 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -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) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 1d2d30df86..9dbffcc4a7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -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) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 1e20e35e79..2c49728c7a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -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) {