Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-19 21:32:53 +04:00
parent 0b8d6e48ba
commit 05da18fa9c
76 changed files with 1748 additions and 248 deletions

View file

@ -60,6 +60,7 @@ dependencies {
implementation(Compose.foundation)
implementation(Compose.material)
implementation(Compose.uiTooling)
implementation(Compose.coil)
/** Domain */
implementation(project(":features:swap:domain"))

View file

@ -0,0 +1,22 @@
package com.tangem.feature.swap.models
data class SwapPermissionStateHolder(
val currency: String,
val amount: String,
val walletAddress: String,
val spenderAddress: String,
val fee: String,
val approveButton: ApprovePermissionButton,
val cancelButton: CancelPermissionButton,
)
data class ApprovePermissionButton(
val enabled: Boolean,
val loading: Boolean = false,
val onClick: () -> Unit,
)
data class CancelPermissionButton(
val enabled: Boolean,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,7 @@
package com.tangem.feature.swap.models
data class SwapSelectTokenStateHolder(
val tokens: List<TokenToSelect>,
val onSearchEntered: (String) -> Unit,
val onTokenSelected: (String) -> Unit,
)

View file

@ -0,0 +1,79 @@
package com.tangem.feature.swap.models
import androidx.annotation.DrawableRes
data class SwapStateHolder(
val sendCardData: SwapCardData,
val receiveCardData: SwapCardData,
val networkCurrency: String,
val fee: FeeState = FeeState.Loaded(),
val warnings: List<SwapWarning> = emptyList(),
val permissionState: SwapPermissionStateHolder? = null,
val successState: SwapSuccessStateHolder? = null,
val selectTokenState: SwapSelectTokenStateHolder? = null,
val swapButton: SwapButton,
val onRefresh: () -> Unit,
val onBackClicked: () -> Unit,
val onChangeCardsClicked: () -> Unit,
val onSelectTokenClick: (() -> Unit)? = null,
val onSuccess: (() -> Unit)? = null,
)
data class TokenToSelect(
val id: String,
val name: String,
val symbol: String,
val addedTokenBalanceData: TokenBalanceData? = null,
)
data class TokenBalanceData(
val amount: String?,
val amountEquivalent: String?,
)
data class SwapCardData(
val type: TransactionCardType,
val amount: String?,
val amountEquivalent: String?,
val tokenIconUrl: String,
val tokenCurrency: String,
@DrawableRes val networkIconRes: Int? = null,
val canSelectAnotherToken: Boolean = false,
)
data class SwapButton(
val enabled: Boolean,
val loading: Boolean = false,
val onClick: () -> Unit,
)
sealed interface FeeState {
data class Loaded(
val fee: String = "",
) : FeeState
object Loading : FeeState
data class NotEnoughFundsWarning(val fee: String) : FeeState
}
sealed interface TransactionCardType {
data class SendCard(
val balance: String,
val permissionIsGiven: Boolean,
) : TransactionCardType
data class ReceiveCard(
val highPriceImpact: String? = null,
) : TransactionCardType
}
sealed interface SwapWarning {
data class PermissionNeeded(val tokenCurrency: String) : SwapWarning
// data class RateExpired(val onClick: () -> Unit) : SwapWarning
// object HighPriceImpact : SwapWarning
}

View file

@ -0,0 +1,5 @@
package com.tangem.feature.swap.models
data class SwapSuccessStateHolder(
val message: String,
)

View file

@ -1,16 +1,21 @@
package com.tangem.feature.swap.presentation
import android.os.Bundle
import android.transition.TransitionInflater
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.animation.Crossfade
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.SwapRouter
import com.tangem.feature.swap.router.SwapScreen
import com.tangem.feature.swap.ui.SwapScreen
import com.tangem.feature.swap.ui.SwapSuccessScreen
import com.tangem.feature.swap.viewmodels.SwapViewModel
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
@AndroidEntryPoint
class SwapFragment : Fragment() {
@ -19,10 +24,24 @@ class SwapFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
viewModel
viewModel.setRouter(SwapRouter(fragmentManager = WeakReference(parentFragmentManager)))
return ComposeView(inflater.context).apply {
setContent {
//todo add compose screen here
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 -> SwapScreen(stateHolder = viewModel.uiState)// TODO: use proper screen
}
}
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.feature.swap.router
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.fragment.app.FragmentManager
import java.lang.ref.WeakReference
internal class SwapRouter(
private val fragmentManager: WeakReference<FragmentManager>,
) {
var currentScreen by mutableStateOf(SwapScreen.Main)
private set
fun openScreen(screen: SwapScreen) {
currentScreen = screen
}
fun back() {
if (currentScreen == SwapScreen.SelectToken) {
currentScreen = SwapScreen.Main
} else {
fragmentManager.get()?.popBackStack()
}
}
}
enum class SwapScreen {
Main, Success, SelectToken
}

View file

@ -0,0 +1,219 @@
package com.tangem.feature.swap.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.PrimaryButtonIconRight
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH28
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.ApprovePermissionButton
import com.tangem.feature.swap.models.CancelPermissionButton
import com.tangem.feature.swap.models.SwapPermissionStateHolder
import com.tangem.feature.swap.presentation.R
@Composable
fun SwapPermissionBottomSheetContent(
data: SwapPermissionStateHolder,
onCancel: () -> Unit,
) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Hand()
SpacerH10()
Text(
text = stringResource(id = R.string.swapping_permission_header),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
)
SpacerH10()
Text(
text = stringResource(id = R.string.swapping_permission_subheader, data.currency),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing8),
)
SpacerH16()
ApprovalBottomSheetInfo(
currency = data.currency,
amount = data.amount,
walletAddress = data.walletAddress,
spenderAddress = data.spenderAddress,
fee = data.fee,
)
SpacerH28()
PrimaryButtonIconRight(
text = stringResource(id = R.string.swapping_permission_buttons_approve),
icon = painterResource(id = R.drawable.ic_tangem_24),
modifier = Modifier.fillMaxWidth(),
onClick = data.approveButton.onClick,
)
SpacerH12()
SecondaryButton(
text = stringResource(id = R.string.common_cancel),
modifier = Modifier.fillMaxWidth(),
) {
data.cancelButton.onClick()
onCancel()
}
SpacerH32()
}
}
@Composable
private fun ApprovalBottomSheetInfo(
currency: String, amount: String, walletAddress: String, spenderAddress: String, fee: String,
) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing16,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AmountItem(currency = currency, amount = amount)
DividerBottomSheet()
WalletAddressItem(walletAddress = walletAddress)
DividerBottomSheet()
SpenderItem(spenderAddress = spenderAddress)
DividerBottomSheet()
FeeItem(fee = fee)
}
}
@Composable
private fun DividerBottomSheet() {
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
)
}
@Composable
private fun InformationItem(subtitle: String, value: String) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
) {
Text(
text = subtitle,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
maxLines = 1,
)
SpacerH16()
Text(
text = value,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
maxLines = 1,
)
}
}
@Composable
private fun AmountItem(currency: String, amount: String) {
InformationItem(
subtitle = stringResource(id = R.string.swapping_permission_rows_amount, currency),
value = amount,
)
}
@Composable
private fun WalletAddressItem(walletAddress: String) {
InformationItem(
subtitle = stringResource(id = R.string.swapping_permission_rows_your_wallet),
value = walletAddress,
)
}
@Composable
private fun SpenderItem(spenderAddress: String) {
InformationItem(
subtitle = stringResource(id = R.string.swapping_permission_rows_spender),
value = spenderAddress,
)
}
@Composable
private fun FeeItem(fee: String) {
InformationItem(
subtitle = stringResource(id = R.string.send_fee_label),
value = fee,
)
}
// region preview
@Preview
@Composable
fun Preview_AgreementBottomSheet_InLightTheme() {
TangemTheme(isDark = false) {
SwapPermissionBottomSheetContent(data = previewData) {}
}
}
@Preview
@Composable
fun Preview_AgreementBottomSheet_InDarkTheme() {
TangemTheme(isDark = true) {
SwapPermissionBottomSheetContent(data = previewData) {}
}
}
private val previewData = SwapPermissionStateHolder(
currency = "DAI",
amount = "",
walletAddress = "",
spenderAddress = "",
fee = "2,14$",
approveButton = ApprovePermissionButton(true) {},
cancelButton = CancelPermissionButton(true) {},
)
//endregion preview

View file

@ -0,0 +1,66 @@
package com.tangem.feature.swap.ui
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetState
import androidx.compose.material.BottomSheetValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.presentation.R
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun SwapScreen(stateHolder: SwapStateHolder) {
val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed),
)
TangemTheme {
BottomSheetScaffold(
topBar = {
AppBarWithBackButton(
text = stringResource(R.string.swapping_swap),
onBackClick = stateHolder.onBackClicked,
iconRes = R.drawable.ic_close_24,
)
},
sheetContent = {
if (stateHolder.permissionState != null) {
SwapPermissionBottomSheetContent(
data = stateHolder.permissionState,
onCancel = { coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.collapse() } },
)
}
},
scaffoldState = bottomSheetScaffoldState,
sheetShape = RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
sheetElevation = TangemTheme.dimens.elevation24,
sheetPeekHeight = TangemTheme.dimens.size0,
content = {
SwapScreenContent(
state = stateHolder,
onPermissionWarningClick = {
coroutineScope.launch {
if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) {
bottomSheetScaffoldState.bottomSheetState.expand()
} else {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
},
)
},
)
}
}

View file

@ -0,0 +1,226 @@
package com.tangem.feature.swap.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.ClickableWarningCard
import com.tangem.core.ui.components.PrimaryButtonIconRight
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.res.TangemTheme
import com.tangem.feature.swap.models.FeeState
import com.tangem.feature.swap.models.SwapButton
import com.tangem.feature.swap.models.SwapCardData
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.SwapWarning
import com.tangem.feature.swap.models.TransactionCardType
import com.tangem.feature.swap.presentation.R
@Composable
internal fun SwapScreenContent(
modifier: Modifier = Modifier,
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),
) {
MainInfo(state)
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,
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun MainInfo(state: SwapStateHolder) {
Box(
modifier = Modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Column {
TransactionCard(
type = state.sendCardData.type,
amount = state.sendCardData.amount,
amountEquivalent = state.sendCardData.amountEquivalent,
tokenIconUrl = state.sendCardData.tokenIconUrl,
tokenCurrency = state.sendCardData.tokenCurrency,
networkIconRes = state.sendCardData.networkIconRes,
onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null,
)
SpacerH16()
TransactionCard(
type = state.receiveCardData.type,
amount = state.receiveCardData.amount,
amountEquivalent = state.receiveCardData.amountEquivalent,
tokenIconUrl = state.receiveCardData.tokenIconUrl,
tokenCurrency = state.receiveCardData.tokenCurrency,
networkIconRes = state.receiveCardData.networkIconRes,
onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) state.onSelectTokenClick else null,
)
}
Card(
elevation = TangemTheme.dimens.elevation3,
shape = CircleShape,
backgroundColor = TangemTheme.colors.background.plain,
contentColor = TangemTheme.colors.text.primary1,
modifier = Modifier
.size(TangemTheme.dimens.size48),
onClick = state.onChangeCardsClicked,
) {
Icon(
painter = painterResource(id = com.tangem.core.ui.R.drawable.ic_exchange_vertical_24),
contentDescription = null,
tint = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(TangemTheme.dimens.spacing12),
)
}
}
}
@Composable
private fun FeeItem(feeState: FeeState, currency: String) {
val titleString = stringResource(id = R.string.send_fee_label)
when (feeState) {
is FeeState.Loaded -> {
SmallInfoCard(startText = titleString, endText = feeState.fee, isLoading = false)
}
FeeState.Loading -> {
SmallInfoCard(startText = titleString, endText = "", isLoading = true)
}
is FeeState.NotEnoughFundsWarning -> {
SmallInfoCardWithWarning(
startText = titleString,
endText = feeState.fee,
warningText = stringResource(id = R.string.token_details_send_blocked_fee_format, currency),
)
}
}
}
@Composable
private fun SwapWarnings(
warnings: List<SwapWarning>, onApproveWarningClick: () -> Unit,
) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colors.primary)
.fillMaxWidth(),
) {
warnings.forEach { warning ->
when (warning) {
// SwapWarning.HighPriceImpact -> {
// WarningCard(title = stringResource(id = R.string.), description = stringResource(id = R.string.))
// }
is SwapWarning.PermissionNeeded -> {
ClickableWarningCard(
title = stringResource(id = R.string.swapping_permission_header),
description = stringResource(
id = R.string.swapping_permission_subheader,
warning.tokenCurrency,
),
onClick = onApproveWarningClick,
)
}
// is SwapWarning.RateExpired -> {
// RefreshableWaringCard(
// title = stringResource(id = R.string.),
// description = stringResource(id = R.string.),
// onClick = warning.onClick,
// )
// }
}
}
}
}
// region preview
private val sendCard = SwapCardData(
type = TransactionCardType.SendCard("123", false),
amount = "1 000 000",
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
canSelectAnotherToken = false,
)
private val receiveCard = SwapCardData(
type = TransactionCardType.ReceiveCard(),
amount = "1 000 000",
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
canSelectAnotherToken = true,
)
private val state = SwapStateHolder(
sendCardData = sendCard,
receiveCardData = receiveCard,
fee = FeeState.Loaded(fee = "0.155 MATIC (0.14 $)"),
warnings = emptyList(),
networkCurrency = "MATIC",
swapButton = SwapButton(enabled = true, loading = false, onClick = {}),
onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {},
)
@Preview
@Composable
fun SwapScreenContentPreview() {
TangemTheme(isDark = false) {
SwapScreenContent(state = state) {}
}
}
// endregion preview

View file

@ -0,0 +1,31 @@
package com.tangem.feature.swap.ui
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.SuccessScreenContent
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.SwapSuccessStateHolder
import com.tangem.feature.swap.presentation.R
@Composable
fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) {
TangemTheme {
Scaffold(
content = {
SuccessScreenContent(
successMessage = state.message,
onButtonClick = onBack,
)
},
topBar = {
AppBarWithBackButton(
text = stringResource(R.string.swapping_swap),
onBackClick = onBack,
iconRes = R.drawable.ic_close_24,
)
},
)
}
}

View file

@ -0,0 +1,378 @@
package com.tangem.feature.swap.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
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.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
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 androidx.compose.ui.unit.dp
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
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.TransactionCardType
import com.valentinilk.shimmer.shimmer
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun TransactionCard(
type: TransactionCardType,
amount: String?,
amountEquivalent: String?,
tokenIconUrl: String,
tokenCurrency: String,
@DrawableRes networkIconRes: Int? = null,
onChangeTokenClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier,
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
backgroundColor = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
onClick = onChangeTokenClick ?: {},
enabled = onChangeTokenClick != null,
) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size116),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.Start,
) {
Header(type = type)
Content(
type = type,
amount = amount,
amountEquivalent = amountEquivalent,
)
}
Box(modifier = Modifier.align(Alignment.BottomEnd)) {
Token(
tokenIconUrl = tokenIconUrl,
tokenCurrency = tokenCurrency,
networkIconRes = networkIconRes,
)
}
if (onChangeTokenClick != null) {
Box(modifier = Modifier.align(Alignment.CenterEnd)) {
ChangeTokenSelector()
}
}
}
}
}
@Composable
private fun Header(
type: TransactionCardType,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(
bottom = dimensionResource(id = R.dimen.spacing8),
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
val title = when (type) {
is TransactionCardType.ReceiveCard -> R.string.exchange_receive_view_header
is TransactionCardType.SendCard -> R.string.exchange_send_view_header
}
Text(
text = stringResource(id = title),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = MaterialTheme.typography.subtitle2,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
)
if (type is TransactionCardType.SendCard) {
Text(
text = stringResource(R.string.common_balance, type.balance),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = MaterialTheme.typography.body2,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
)
}
}
}
@Composable
private fun Content(
type: TransactionCardType,
amount: String?,
amountEquivalent: String?,
) {
Row(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom,
) {
if (type is TransactionCardType.SendCard && !type.permissionIsGiven) {
Box(
modifier = Modifier
.size(TangemTheme.dimens.size56)
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.size12),
),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_locked_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
}
SpacerW12()
}
Column(
modifier = Modifier
.padding(
end = TangemTheme.dimens.spacing92,
),
verticalArrangement = Arrangement.SpaceBetween,
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),
),
)
}
Spacer(
modifier = Modifier
.size(TangemTheme.dimens.size4)
.weight(1f),
)
if (amountEquivalent != null) {
Text(
text = amountEquivalent,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = TangemTheme.typography.body2,
)
} else {
Box(
modifier = Modifier
.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
.shimmer()
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.radius3),
),
)
}
}
}
}
@Composable
fun Token(
tokenIconUrl: String,
tokenCurrency: String,
@DrawableRes networkIconRes: Int? = null,
) {
Column(
modifier = Modifier
.padding(
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.End,
) {
Box(
modifier = Modifier
.padding(
end = TangemTheme.dimens.spacing16,
)
.size(TangemTheme.dimens.size42),
) {
val tokenImageModifier = Modifier
.align(Alignment.BottomStart)
.size(TangemTheme.dimens.size36)
SubcomposeAsyncImage(
modifier = tokenImageModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(tokenIconUrl)
.crossfade(true)
.build(),
loading = { TokenImageShimmer(modifier = tokenImageModifier) },
error = { CurrencyPlaceholderIcon(modifier = tokenImageModifier, tokenCurrency) },
contentDescription = null,
)
if (networkIconRes != null) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens.size18)
.background(
color = Color.White,
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
Image(
modifier = Modifier.padding(all = (0.5).dp),
painter = painterResource(id = networkIconRes),
contentDescription = null,
)
}
}
}
SpacerH4()
Text(
text = tokenCurrency,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.subtitle2,
textAlign = TextAlign.Center,
modifier = Modifier
.defaultMinSize(minWidth = TangemTheme.dimens.size80),
)
}
}
@Composable
fun ChangeTokenSelector() {
Box(
modifier = Modifier
.fillMaxHeight()
.padding(
top = TangemTheme.dimens.spacing12,
start = TangemTheme.dimens.spacing24,
end = TangemTheme.dimens.spacing12,
),
contentAlignment = Alignment.CenterEnd,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = painterResource(id = R.drawable.ic_chevron_24),
contentDescription = null,
)
}
}
@Composable
private fun TokenImageShimmer(
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.shimmer()) {
Box(
modifier = Modifier
.matchParentSize()
.background(
color = TangemTheme.colors.button.secondary,
shape = CircleShape,
),
)
}
}
// region preview
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
@Composable
fun Preview_SwapMainCard_InLightTheme() {
TangemTheme(isDark = false) {
TransactionCardPreview()
}
}
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
@Composable
fun Preview_SwapMainCard_InDarkTheme() {
TangemTheme(isDark = true) {
TransactionCardPreview()
}
}
@Composable
private fun TransactionCardPreview() {
TransactionCard(
type = TransactionCardType.SendCard("123", false),
amount = "1 000 000",
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
onChangeTokenClick = {},
)
}
// endregion preview

View file

@ -1,14 +1,31 @@
package com.tangem.feature.swap.viewmodels
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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
import com.tangem.feature.swap.models.SwapButton
import com.tangem.feature.swap.models.SwapCardData
import com.tangem.feature.swap.models.SwapPermissionStateHolder
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.SwapSuccessStateHolder
import com.tangem.feature.swap.models.SwapWarning
import com.tangem.feature.swap.models.TransactionCardType
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.router.SwapRouter
import com.tangem.feature.swap.router.SwapScreen
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
@HiltViewModel
internal class SwapViewModel @Inject constructor(
@ -16,6 +33,27 @@ internal class SwapViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel() {
var uiState: SwapStateHolder by mutableStateOf(createInitialUiState())
private set
private var swapRouter: SwapRouter by Delegates.notNull()
var currentScreen = SwapScreen.Main
get() = swapRouter.currentScreen
fun setRouter(router: SwapRouter) {
swapRouter = router
uiState = uiState.copy(
onBackClicked = router::back,
onSelectTokenClick = { router.openScreen(SwapScreen.Success) },
onSuccess = { router.openScreen(SwapScreen.Success) },
)
}
private fun createInitialUiState(): SwapStateHolder { // TODO: create using real state
return createTestUiState()
}
// region temp for test
init {
viewModelScope.launch {
@ -39,5 +77,48 @@ internal class SwapViewModel @Inject constructor(
"1",
)
}
private fun createTestUiState(): SwapStateHolder {
val sendCard = SwapCardData(
type = TransactionCardType.SendCard("123", false),
amount = "1 000 000 000 000 000 000",
amountEquivalent = "1 000 000 000 000 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
)
val receiveCard = SwapCardData(
type = TransactionCardType.ReceiveCard(),
amount = "1 000 000",
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
canSelectAnotherToken = true,
)
val permissionState = SwapPermissionStateHolder(
currency = "DAI",
amount = "",
walletAddress = "",
spenderAddress = "",
fee = "2,14$",
approveButton = ApprovePermissionButton(true) {},
cancelButton = CancelPermissionButton(true) {},
)
return SwapStateHolder(
sendCardData = sendCard,
receiveCardData = receiveCard,
fee = FeeState.Loaded(fee = "0.155 MATIC (0.14 $)"),
warnings = listOf(SwapWarning.PermissionNeeded("DAI")),
networkCurrency = "MATIC",
swapButton = SwapButton(enabled = true, loading = false, onClick = {}),
onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {},
permissionState = permissionState,
successState = SwapSuccessStateHolder("Success"),
)
}
// endregion temp for test
}