Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-27 15:29:08 +04:00
parent 615fb8d718
commit 9aaf4bd3c4
8 changed files with 205 additions and 62 deletions

View file

@ -38,6 +38,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.form.DataField
import com.tangem.domain.common.form.FieldId
@ -56,7 +57,6 @@ import com.tangem.tap.common.compose.AddCustomTokenWarning
import com.tangem.tap.common.compose.ClosePopupTrigger
import com.tangem.tap.common.compose.ComposeDialogManager
import com.tangem.tap.common.compose.ToggledRippleTheme
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCase
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCasesList
@ -66,7 +66,7 @@ import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
private class AddCustomTokenScreen {} // for simple search
private class AddCustomTokenScreen // for simple search
@OptIn(ExperimentalMaterialApi::class)
@Composable
@ -91,11 +91,13 @@ fun AddCustomTokenScreen(
},
sheetPeekHeight = 0.dp,
) {
Column() {
TestCasesList(onItemClick = {
selectedTestCase.value = it
toggleBottomSheet()
})
Column {
TestCasesList(
onItemClick = {
selectedTestCase.value = it
toggleBottomSheet()
},
)
ScreenContent(state, closePopupTrigger)
}
}

View file

@ -9,8 +9,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.Keyboard
import com.tangem.tangem_sdk_new.extensions.pxToDp
import com.tangem.tap.common.compose.Keyboard
/**
[REDACTED_AUTHOR]

View file

@ -26,16 +26,16 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.compose.Keyboard
import com.tangem.tap.common.compose.extensions.addAndNotify
import com.tangem.tap.common.compose.extensions.removeAndNotify
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.pixelsToDp
import com.tangem.tap.common.redux.AppDialog

View file

@ -1,8 +1,13 @@
package com.tangem.tap.common.compose
package com.tangem.core.ui.components
import android.graphics.Rect
import android.view.ViewTreeObserver
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView
sealed class Keyboard {
@ -10,6 +15,9 @@ sealed class Keyboard {
object Closed : Keyboard()
}
/**
* Allows to subscribe to a soft keyboard to detect when it's open/closed
*/
@Composable
fun keyboardAsState(): State<Keyboard> {
val keyboardState: MutableState<Keyboard> = remember { mutableStateOf(Keyboard.Closed) }
@ -25,7 +33,7 @@ fun keyboardAsState(): State<Keyboard> {
val screenHeight = view.rootView.height
val keypadHeight: Int = screenHeight - (rect.bottom + rect.top) - discrepancy.value
if (discrepancy.value == 0) {
discrepancy.value = keypadHeight;
discrepancy.value = keypadHeight
if (keypadHeight == 0) discrepancy.value = 1
}

View file

@ -20,6 +20,7 @@ data class SwapStateHolder(
val onChangeCardsClicked: () -> Unit,
val onSelectTokenClick: (() -> Unit)? = null,
val onSuccess: (() -> Unit)? = null,
val onMaxAmountSelected: (() -> Unit)? = null,
)
data class SwapCardData(
@ -53,6 +54,7 @@ sealed interface TransactionCardType {
data class SendCard(
val balance: String,
val permissionIsGiven: Boolean,
val onAmountChanged: ((String) -> Unit),
) : TransactionCardType
data class ReceiveCard(
@ -62,6 +64,7 @@ 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 RateExpired(val onClick: () -> Unit) : SwapWarning
// object HighPriceImpact : SwapWarning
}

View file

@ -1,9 +1,12 @@
package com.tangem.feature.swap.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -14,17 +17,24 @@ import androidx.compose.material.Card
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
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.ClickableWarningCard
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.PrimaryButtonIconRight
import com.tangem.core.ui.components.RefreshableWaringCard
import com.tangem.core.ui.components.SmallInfoCard
import com.tangem.core.ui.components.SmallInfoCardWithWarning
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.FeeState
import com.tangem.feature.swap.models.SwapButton
@ -40,40 +50,71 @@ internal fun SwapScreenContent(
state: SwapStateHolder,
onPermissionWarningClick: () -> Unit,
) {
Column(
modifier = modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.primary)
.verticalScroll(rememberScrollState())
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32,
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
val keyboard by keyboardAsState()
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Transparent),
) {
MainInfo(state)
Column(
modifier = modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.primary)
.verticalScroll(rememberScrollState())
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32,
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
FeeItem(feeState = state.fee, currency = state.networkCurrency)
MainInfo(state)
if (state.warnings.isNotEmpty()) {
SwapWarnings(
warnings = state.warnings,
onApproveWarningClick = onPermissionWarningClick,
FeeItem(feeState = state.fee, currency = state.networkCurrency)
if (state.warnings.isNotEmpty()) {
SwapWarnings(
warnings = state.warnings,
onApproveWarningClick = 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,
)
}
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(
visible = keyboard is Keyboard.Opened,
modifier = Modifier
.align(Alignment.BottomCenter),
) {
Text(
text = stringResource(id = R.string.send_max_amount_label),
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors.button.secondary)
.clickable { state.onMaxAmountSelected }
.padding(
horizontal = TangemTheme.dimens.spacing14,
vertical = TangemTheme.dimens.spacing16,
),
textAlign = TextAlign.Start,
)
}
}
}
@ -171,6 +212,13 @@ private fun SwapWarnings(
onClick = onApproveWarningClick,
)
}
is SwapWarning.GenericWarning -> {
RefreshableWaringCard(
title = stringResource(id = R.string.common_warning),
description = warning.message,
onClick = warning.onClick,
)
}
// is SwapWarning.RateExpired -> {
// RefreshableWaringCard(
// title = stringResource(id = R.string.),
@ -186,7 +234,7 @@ private fun SwapWarnings(
// region preview
private val sendCard = SwapCardData(
type = TransactionCardType.SendCard("123", false),
type = TransactionCardType.SendCard("123", false, {}),
amount = "1 000 000",
amountEquivalent = "1 000 000",
tokenIconUrl = "",

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -19,18 +20,31 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
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.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
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.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -192,24 +206,34 @@ private fun Content(
horizontalAlignment = Alignment.Start,
) {
if (amount != null) {
ResizableText(
text = amount,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
fontSizeRange =
FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
)
} else {
Box(
modifier = Modifier
.size(width = TangemTheme.dimens.size102, height = TangemTheme.dimens.size24)
.shimmer()
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.radius6),
),
)
when (type) {
is TransactionCardType.ReceiveCard -> {
if (amount != null) {
ResizableText(
text = amount,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
fontSizeRange =
FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
)
} else {
Box(
modifier = Modifier
.size(width = TangemTheme.dimens.size102, height = TangemTheme.dimens.size24)
.shimmer()
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.radius6),
),
)
}
}
is TransactionCardType.SendCard -> {
AutoSizeTextField(
amount = amount ?: "0",
onAmoutChanged = { type.onAmountChanged(it) },
)
}
}
Spacer(
@ -241,6 +265,65 @@ private fun Content(
}
}
@Composable
private fun AutoSizeTextField(
amount: String,
onAmoutChanged: (String) -> Unit,
) {
val focusManager = LocalFocusManager.current
val textFieldFocusRequester = remember { FocusRequester() }
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
var shrunkFontSize = TangemTheme.typography.h2.fontSize
val calculateIntrinsics = @Composable {
ParagraphIntrinsics(
text = amount,
style = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
fontSize = shrunkFontSize,
),
density = LocalDensity.current,
fontFamilyResolver = createFontFamilyResolver(LocalContext.current),
)
}
var intrinsics = calculateIntrinsics()
with(LocalDensity.current) {
while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
shrunkFontSize *= 0.9f
intrinsics = calculateIntrinsics()
}
}
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),
)
}
}
@Composable
fun Token(
tokenIconUrl: String,
@ -370,7 +453,7 @@ fun Preview_SwapMainCard_InDarkTheme() {
@Composable
private fun TransactionCardPreview() {
TransactionCard(
type = TransactionCardType.SendCard("123", false),
type = TransactionCardType.SendCard("123", false, {}),
amount = "1 000 000",
amountEquivalent = "1 000 000",
tokenIconUrl = "",

View file

@ -7,7 +7,6 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.models.ApprovePermissionButton
import com.tangem.feature.swap.models.CancelPermissionButton
import com.tangem.feature.swap.models.FeeState
@ -72,7 +71,7 @@ internal class SwapViewModel @Inject constructor(
private fun createTestUiState(): SwapStateHolder {
val sendCard = SwapCardData(
type = TransactionCardType.SendCard("123", false),
type = TransactionCardType.SendCard("123", false, {}),
amount = "1 000 000 000 000 000 000",
amountEquivalent = "1 000 000 000 000 000 000",
tokenIconUrl = "",