Updated on 2026-08-14
This commit is contained in:
commit
bd28df6443
40 changed files with 1503 additions and 388 deletions
|
|
@ -395,7 +395,7 @@
|
|||
<string name="send_amount_substract_footer">Сумма к получению %1$s</string>
|
||||
<string name="send_destination_hint_address">Адрес</string>
|
||||
<string name="send_destination_tag_field">Код назначения</string>
|
||||
<string name="send_datetime_formatter">%1$s в %2$s</string>
|
||||
<string name="send_date_format">%1$s в %2$s</string>
|
||||
<string name="send_enter_address_field">Введите адрес</string>
|
||||
<string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string>
|
||||
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
|
||||
|
|
@ -427,7 +427,7 @@
|
|||
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению.</string>
|
||||
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.</string>
|
||||
<string name="send_notification_invalid_reserve_amount_title">Сумма резерва не может быть менее %1$s.</string>
|
||||
<string name="send_notification_top_up_text">Пожалуйста, пополните свой баланс, чтобы продолжить.</string>
|
||||
<string name="send_notification_invalid_reserve_amount_text">Пожалуйста, пополните свой баланс, чтобы продолжить.</string>
|
||||
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
|
||||
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
|
||||
<string name="send_notification_exceed_fee_title">Комиссия превышает баланс</string>
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@
|
|||
<string name="send_amount_substract_footer">The recipient will receive %1$s</string>
|
||||
<string name="send_destination_hint_address">Address</string>
|
||||
<string name="send_destination_tag_field">Destination Tag</string>
|
||||
<string name="send_datetime_formatter">%1$s at %2$s</string>
|
||||
<string name="send_date_format">%1$s at %2$s</string>
|
||||
<string name="send_enter_address_field">Enter address</string>
|
||||
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
|
||||
<string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ dependencies {
|
|||
|
||||
/** Project - Core */
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.core.ui.components.appbar
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun AppBarWithBackButtonAndIcon(
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
text: String? = null,
|
||||
@DrawableRes backIconRes: Int? = null,
|
||||
@DrawableRes iconRes: Int? = null,
|
||||
onIconClick: (() -> Unit)? = null,
|
||||
backgroundColor: Color = TangemTheme.colors.background.secondary,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(color = backgroundColor)
|
||||
.fillMaxWidth()
|
||||
.padding(all = TangemTheme.dimens.spacing16),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(backIconRes ?: R.drawable.ic_back_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size24)
|
||||
.clickable { onBackClick() },
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = text,
|
||||
modifier = Modifier.weight(1f),
|
||||
transitionSpec = { fadeIn().togetherWith(fadeOut()) },
|
||||
label = "Toolbar title change",
|
||||
) {
|
||||
if (!it.isNullOrBlank()) {
|
||||
Text(
|
||||
text = it,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = iconRes,
|
||||
transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) },
|
||||
label = "Toolbar icon change",
|
||||
) {
|
||||
if (onIconClick != null && it != null) {
|
||||
Icon(
|
||||
painter = painterResource(it),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size24)
|
||||
.clickable { onIconClick() },
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 56, showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewAppBarWithBackButtonAndIconInLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = "Title",
|
||||
iconRes = R.drawable.ic_qrcode_scan_24,
|
||||
onBackClick = {},
|
||||
onIconClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 56, showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = "Title",
|
||||
iconRes = R.drawable.ic_qrcode_scan_24,
|
||||
onBackClick = {},
|
||||
onIconClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.core.ui.components.fields
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
|
||||
class AmountVisualTransformation(
|
||||
private val symbol: String,
|
||||
) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
return TransformedText(
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
if (text.isNotBlank()) {
|
||||
append(" ")
|
||||
append(symbol)
|
||||
}
|
||||
},
|
||||
object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,12 +9,14 @@ import java.util.Locale
|
|||
@Suppress("MagicNumber")
|
||||
object DateTimeFormatters {
|
||||
|
||||
private const val DDMMYYYY = "dd.MM.yyyy"
|
||||
|
||||
/**
|
||||
* Two SS means, SHORT style for date and time.
|
||||
* If pattern contains "a", it means time is in 12 hour format.
|
||||
* [Documentation](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html)
|
||||
*/
|
||||
val timeFormatter by lazy {
|
||||
val timeFormatter: DateTimeFormatter by lazy {
|
||||
val is12HourFormat = DateTimeFormat.patternForStyle("SS", Locale.getDefault()).contains("a")
|
||||
if (is12HourFormat) {
|
||||
DateTimeFormatterBuilder()
|
||||
|
|
@ -35,7 +37,7 @@ object DateTimeFormatters {
|
|||
}
|
||||
}
|
||||
|
||||
val dateFormatter by lazy {
|
||||
val dateFormatter: DateTimeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendDayOfMonth(1)
|
||||
.appendLiteral(' ')
|
||||
|
|
@ -46,6 +48,13 @@ object DateTimeFormatters {
|
|||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
||||
val dateDDMMYYYY: DateTimeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendPattern(DDMMYYYY)
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
||||
fun formatTime(formatter: DateTimeFormatter = timeFormatter, time: DateTime): String {
|
||||
return formatter.print(time)
|
||||
}
|
||||
|
|
|
|||
33
core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt
Normal file
33
core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import com.tangem.utils.extensions.isToday
|
||||
import com.tangem.utils.extensions.isYesterday
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* If [this] timestamp is today or yesterday, returns relative date,
|
||||
* otherwise returns formatting date.
|
||||
*/
|
||||
fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateFormatter): String {
|
||||
val localDate = DateTime(this, DateTimeZone.getDefault())
|
||||
return if (localDate.isToday() || localDate.isYesterday()) {
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
this,
|
||||
DateTime.now().millis,
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE,
|
||||
).toString()
|
||||
} else {
|
||||
DateTimeFormatters.formatDate(formatter = formatter, date = localDate)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns formatted time according to [formatter].
|
||||
*/
|
||||
fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String {
|
||||
return DateTimeFormatters.formatTime(formatter = formatter, time = DateTime(this, DateTimeZone.getDefault()))
|
||||
}
|
||||
13
core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml
Normal file
13
core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M4,4H10V10H4V4ZM20,4V10H14V4H20ZM14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15ZM16,15V18H18V15H16ZM4,20V14H10V20H4ZM6,6V8H8V6H6ZM16,6V8H18V6H16ZM6,16V18H8V16H6ZM4,11H6V13H4V11ZM9,11H13V15H11V13H9V11ZM11,6H13V10H11V6ZM2,2V6H0V2C0,1.47 0.211,0.961 0.586,0.586C0.961,0.211 1.47,0 2,0L6,0V2H2ZM22,0C22.53,0 23.039,0.211 23.414,0.586C23.789,0.961 24,1.47 24,2V6H22V2H18V0H22ZM2,18V22H6V24H2C1.47,24 0.961,23.789 0.586,23.414C0.211,23.039 0,22.53 0,22V18H2ZM22,22V18H24V22C24,22.53 23.789,23.039 23.414,23.414C23.039,23.789 22.53,24 22,24H18V22H22Z"
|
||||
android:fillColor="#1E1E1E"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -323,6 +323,10 @@ class DefaultWalletManagersFacade(
|
|||
}
|
||||
|
||||
override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address> {
|
||||
return getAddresses(userWalletId, network).sortedBy { it.type }
|
||||
}
|
||||
|
||||
override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address> {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
|
||||
return getOrCreateWalletManager(
|
||||
|
|
@ -332,7 +336,6 @@ class DefaultWalletManagersFacade(
|
|||
)
|
||||
?.wallet
|
||||
?.addresses
|
||||
?.sortedBy { it.type }
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,13 @@ interface WalletManagersFacade {
|
|||
*/
|
||||
suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address>
|
||||
|
||||
/** Returns list of all addresses for all currencies in selected wallet
|
||||
*
|
||||
* @param userWalletId selected wallet id
|
||||
* @param network required to create wallet manager
|
||||
*/
|
||||
suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address>
|
||||
|
||||
/**
|
||||
* Returns info about rent if wallet manager implemented [RentProvider], otherwise null
|
||||
*
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ dependencies {
|
|||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(deps.androidx.appCompat)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
||||
/** Other dependencies */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.material)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.jodatime)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
@ -29,6 +31,12 @@ dependencies {
|
|||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.paging)
|
||||
implementation(deps.compose.constraintLayout)
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common)
|
||||
|
|
@ -40,12 +48,15 @@ dependencies {
|
|||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
|
|
|
|||
|
|
@ -1,43 +1,53 @@
|
|||
package com.tangem.features.send.impl.presentation
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.ui.SendScreen
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Send fragment
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
internal class SendFragment : ComposeBottomSheetFragment() {
|
||||
internal class SendFragment : ComposeFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
override val expandedHeightFraction: Float = 1f
|
||||
private val viewModel by viewModels<SendViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycle.addObserver(viewModel)
|
||||
viewModel.setRouter(
|
||||
StateRouter(
|
||||
fragmentManager = WeakReference(parentFragmentManager),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val viewModel = hiltViewModel<SendViewModel>()
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
|
||||
SystemBarsEffect { setSystemBarsColor(color = Color.Transparent) }
|
||||
BackHandler { dismiss() }
|
||||
|
||||
when (val state = viewModel.uiState) {
|
||||
is SendUiState.Content -> SendScreen(state)
|
||||
SendUiState.Dismiss -> dismiss()
|
||||
val systemBarsColor = TangemTheme.colors.background.tertiary
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(systemBarsColor)
|
||||
}
|
||||
SendScreen(viewModel.uiState)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
lifecycle.removeObserver(viewModel)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.send.impl.presentation.domain
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Available wallet to send
|
||||
*
|
||||
* @property name wallet name
|
||||
* @property address blockchain address
|
||||
*/
|
||||
@Immutable
|
||||
data class AvailableWallet(
|
||||
val name: String,
|
||||
val address: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.send.impl.presentation.domain
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendRecipientListContent {
|
||||
data class Item(
|
||||
val id: String,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
val info: TextReference? = null,
|
||||
@DrawableRes val subtitleIconRes: Int? = null,
|
||||
) : SendRecipientListContent()
|
||||
|
||||
data class Wallets(
|
||||
val list: PersistentList<Item>,
|
||||
) : SendRecipientListContent()
|
||||
}
|
||||
|
|
@ -1,22 +1,35 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import arrow.core.Either
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.isNotAddressInWallet
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.validateMemo
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.verifyAddress
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class SendStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val walletAddressesProvider: Provider<Set<Address>>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val userWalletProvider: Provider<UserWallet?>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
|
@ -27,33 +40,129 @@ internal class SendStateFactory(
|
|||
|
||||
private val amountStateConverter by lazy {
|
||||
SendAmountStateConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
clickIntents = clickIntents,
|
||||
iconStateConverter = iconStateConverter,
|
||||
userWalletProvider = userWalletProvider,
|
||||
sendAmountFieldConverter = amountFieldConverter,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
|
||||
|
||||
fun getAmountState(cryptoCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>): SendUiState {
|
||||
return amountStateConverter.convert(cryptoCurrencyStatus)
|
||||
private val recipientStateConverter by lazy {
|
||||
SendRecipientStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
|
||||
private val recipientListStateConverter by lazy {
|
||||
SendRecipientListConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
// region UI states
|
||||
fun getInitialState(): SendUiState = SendUiState(
|
||||
clickIntents = clickIntents,
|
||||
currentState = MutableStateFlow(SendUiStateType.Amount),
|
||||
)
|
||||
|
||||
fun getReadyState(): SendUiState = currentStateProvider().copy(
|
||||
amountState = amountStateConverter.convert(Unit),
|
||||
recipientState = recipientStateConverter.convert(Unit),
|
||||
feeState = SendStates.FeeState(),
|
||||
)
|
||||
//endregion
|
||||
|
||||
//region amount state clicks
|
||||
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
|
||||
|
||||
fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state as? SendUiState.Content.AmountState ?: return state
|
||||
val amountState = state.amountState ?: return state
|
||||
|
||||
return if (amountState.isFiatValue == isFiat) {
|
||||
state
|
||||
} else {
|
||||
return state.copy(isFiatValue = isFiat)
|
||||
return state.copy(amountState = amountState.copy(isFiatValue = isFiat))
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region recipient
|
||||
fun onLoadedRecipientList(wallets: List<AvailableWallet?>, txHistory: PagingData<TxHistoryItem>) {
|
||||
recipientListStateConverter.convert(
|
||||
wallets = wallets,
|
||||
txHistory = txHistory,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnRecipientAddressValueChangeState(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
|
||||
val isValidMemo = validateMemo(
|
||||
memo = value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
val isAddressInWallet = isNotAddressInWallet(
|
||||
walletAddresses = walletAddressesProvider(),
|
||||
address = recipientState.addressTextField.value.value,
|
||||
)
|
||||
val isValidAddress = verifyAddress(
|
||||
address = recipientState.addressTextField.value.value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
|
||||
recipientState.addressTextField.update {
|
||||
it.copy(
|
||||
value = value,
|
||||
error = when {
|
||||
!isValidAddress -> TextReference.Res(R.string.send_recipient_address_error)
|
||||
!isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
|
||||
else -> null
|
||||
},
|
||||
isError = !isValidAddress || !isAddressInWallet,
|
||||
)
|
||||
}
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnRecipientMemoValueChangeState(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
|
||||
val isValidMemo = validateMemo(
|
||||
memo = value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
val isAddressInWallet = isNotAddressInWallet(
|
||||
walletAddresses = walletAddressesProvider(),
|
||||
address = recipientState.addressTextField.value.value,
|
||||
)
|
||||
val isValidAddress = verifyAddress(
|
||||
address = recipientState.addressTextField.value.value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
|
||||
// todo add memo validation error text
|
||||
recipientState.memoTextField?.update {
|
||||
it.copy(
|
||||
value = value,
|
||||
error = TextReference.Res(R.string.send_memo_destination_tag_error),
|
||||
isError = !isValidMemo,
|
||||
)
|
||||
}
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
|
||||
),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
|
@ -1,71 +1,77 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
* Ui states of the send screen
|
||||
*/
|
||||
@Immutable
|
||||
internal sealed class SendUiState {
|
||||
internal data class SendUiState(
|
||||
val clickIntents: SendClickIntents,
|
||||
val amountState: SendStates.AmountState? = null,
|
||||
val recipientState: SendStates.RecipientState? = null,
|
||||
val feeState: SendStates.FeeState? = null,
|
||||
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val currentState: MutableStateFlow<SendUiStateType>,
|
||||
)
|
||||
|
||||
/** States with content */
|
||||
sealed class Content : SendUiState() {
|
||||
@Stable
|
||||
internal sealed class SendStates {
|
||||
|
||||
/** Is primary button enabled */
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
abstract val type: SendUiStateType
|
||||
|
||||
/** Click intents */
|
||||
abstract val clickIntents: SendClickIntents
|
||||
/** Amount state */
|
||||
data class AmountState(
|
||||
override val type: SendUiStateType = SendUiStateType.Amount,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val walletName: String,
|
||||
val walletBalance: String,
|
||||
val tokenIconState: TokenIconState,
|
||||
val isFiatValue: Boolean,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: MutableStateFlow<SendTextField.Amount>,
|
||||
val isPrimaryButtonEnabled: Boolean,
|
||||
) : SendStates()
|
||||
|
||||
/** Initial state */
|
||||
data class Initial(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
/** Recipient state */
|
||||
data class RecipientState(
|
||||
override val type: SendUiStateType = SendUiStateType.Recipient,
|
||||
val addressTextField: MutableStateFlow<SendTextField.RecipientAddress>,
|
||||
val memoTextField: MutableStateFlow<SendTextField.RecipientMemo>?,
|
||||
val recipients: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val network: String,
|
||||
val isPrimaryButtonEnabled: Boolean,
|
||||
) : SendStates()
|
||||
|
||||
/** Amount state */
|
||||
data class AmountState(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
val walletName: String,
|
||||
val walletBalance: String,
|
||||
val tokenIconState: TokenIconState,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val isFiatValue: Boolean,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: SendTextField.Amount,
|
||||
) : Content()
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Fee and speed state */
|
||||
data class FeeState(
|
||||
override val type: SendUiStateType = SendUiStateType.Fee,
|
||||
) : SendStates()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Recipient state */
|
||||
data class RecipientState(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Send state */
|
||||
data class SendState(
|
||||
val isSuccess: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Fee and speed state */
|
||||
data class FeeState(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Send state */
|
||||
data class SendState(
|
||||
override val isPrimaryButtonEnabled: Boolean = true,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
}
|
||||
|
||||
/** Dismiss screen */
|
||||
object Dismiss : SendUiState()
|
||||
enum class SendUiStateType {
|
||||
Amount,
|
||||
Recipient,
|
||||
Fee,
|
||||
Send,
|
||||
Done,
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
internal class StateRouter(
|
||||
private val fragmentManager: WeakReference<FragmentManager>,
|
||||
) {
|
||||
var currentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(SendUiStateType.Amount)
|
||||
|
||||
fun onBackClick() {
|
||||
fragmentManager.get()?.popBackStack()
|
||||
}
|
||||
|
||||
fun onNextClick() {
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Amount -> {
|
||||
currentState.update { SendUiStateType.Recipient }
|
||||
}
|
||||
SendUiStateType.Recipient -> {
|
||||
currentState.update { SendUiStateType.Fee }
|
||||
}
|
||||
else -> {
|
||||
// todo implement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onPrevClick() {
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Amount -> onBackClick()
|
||||
SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount }
|
||||
SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient }
|
||||
else -> onBackClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +1,55 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class SendAmountStateConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val userWalletProvider: Provider<UserWallet?>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
|
||||
private val sendAmountFieldConverter: SendAmountFieldConverter,
|
||||
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, SendUiState> {
|
||||
) : Converter<Unit, SendStates.AmountState> {
|
||||
|
||||
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): SendUiState {
|
||||
val userWallet = userWalletProvider() ?: return currentStateProvider()
|
||||
override fun convert(value: Unit): SendStates.AmountState {
|
||||
val userWallet = userWalletProvider()
|
||||
val appCurrency = appCurrencyProvider()
|
||||
return value.fold(
|
||||
ifLeft = {
|
||||
// TODO add error handling
|
||||
currentStateProvider()
|
||||
},
|
||||
ifRight = {
|
||||
val fiat = formatFiatAmount(it.value.fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||
val crypto = formatCryptoAmount(it.value.amount, it.currency.symbol, it.currency.decimals)
|
||||
SendUiState.Content.AmountState(
|
||||
cryptoCurrencyStatus = it,
|
||||
walletName = userWallet.name,
|
||||
walletBalance = "$crypto ($fiat)",
|
||||
tokenIconState = iconStateConverter.convert(it),
|
||||
appCurrency = appCurrency,
|
||||
amountTextField = sendAmountFieldConverter.convert(Unit),
|
||||
isFiatValue = false,
|
||||
clickIntents = clickIntents,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(it.currency.symbol),
|
||||
iconState = iconStateConverter.convert(it),
|
||||
isFiat = false,
|
||||
),
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconState = iconStateConverter.convert(it),
|
||||
isFiat = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
val status = cryptoCurrencyStatusProvider()
|
||||
val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||
val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
|
||||
|
||||
return SendStates.AmountState(
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrencyStatus = status,
|
||||
walletName = userWallet.name,
|
||||
walletBalance = "$crypto ($fiat)",
|
||||
tokenIconState = iconStateConverter.convert(status),
|
||||
amountTextField = MutableStateFlow(sendAmountFieldConverter.convert(Unit)),
|
||||
isFiatValue = false,
|
||||
isPrimaryButtonEnabled = false,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(status.currency.symbol),
|
||||
iconState = iconStateConverter.convert(status),
|
||||
isFiat = false,
|
||||
),
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconState = iconStateConverter.convert(status),
|
||||
isFiat = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
|
||||
|
|
@ -11,21 +14,14 @@ internal class SendAmountFieldChangeConverter(
|
|||
) : Converter<String, SendUiState> {
|
||||
override fun convert(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state.amountState ?: return state
|
||||
|
||||
if (
|
||||
state !is SendUiState.Content.AmountState ||
|
||||
value.checkDecimalSeparatorDuplicate()
|
||||
) {
|
||||
return state
|
||||
}
|
||||
|
||||
if (value.checkDecimalSeparatorDuplicate()) return state
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
||||
val fiatRate = state.cryptoCurrencyStatus.value.fiatRate
|
||||
|
||||
val fiatRate = amountState.cryptoCurrencyStatus.value.fiatRate
|
||||
val trimmedValue = value.trim()
|
||||
|
||||
val cryptoValue = if (state.isFiatValue) {
|
||||
val cryptoValue = if (amountState.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
|
|
@ -35,7 +31,7 @@ internal class SendAmountFieldChangeConverter(
|
|||
trimmedValue
|
||||
}
|
||||
|
||||
val fiatValue = if (!state.isFiatValue) {
|
||||
val fiatValue = if (!amountState.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
|
|
@ -45,37 +41,48 @@ internal class SendAmountFieldChangeConverter(
|
|||
trimmedValue
|
||||
}
|
||||
|
||||
val isExceedBalance = value.checkExceedBalance(state)
|
||||
return state.copy(
|
||||
amountTextField = state.amountTextField.copy(
|
||||
val isExceedBalance = value.checkExceedBalance(amountState.cryptoCurrencyStatus, amountState)
|
||||
amountState.amountTextField.update {
|
||||
it.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isExceedBalance,
|
||||
)
|
||||
}
|
||||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = !isExceedBalance,
|
||||
),
|
||||
isPrimaryButtonEnabled = !isExceedBalance,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SendUiState.Content.AmountState.emptyState(): SendUiState {
|
||||
return copy(
|
||||
amountTextField = amountTextField.copy(
|
||||
value = if (!isFiatValue) "" else DEFAULT_VALUE,
|
||||
fiatValue = if (isFiatValue) "" else DEFAULT_VALUE,
|
||||
private fun SendUiState.emptyState(): SendUiState {
|
||||
amountState?.amountTextField?.update {
|
||||
it.copy(
|
||||
value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE,
|
||||
fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE,
|
||||
isError = false,
|
||||
)
|
||||
}
|
||||
return copy(
|
||||
amountState = amountState?.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
),
|
||||
isPrimaryButtonEnabled = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkDecimalSeparatorDuplicate(): Boolean {
|
||||
val regex = "[\\.\\,]".toRegex()
|
||||
val regex = TRIM_REGEX.toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(this).count()
|
||||
|
||||
return decimalSeparatorCount > 1
|
||||
}
|
||||
|
||||
private fun String.checkExceedBalance(state: SendUiState.Content.AmountState): Boolean {
|
||||
val currencyStatus = state.cryptoCurrencyStatus.value
|
||||
private fun String.checkExceedBalance(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
state: SendStates.AmountState,
|
||||
): Boolean {
|
||||
val currencyStatus = cryptoCurrencyStatus.value
|
||||
return if (state.isFiatValue) {
|
||||
toBigDecimal() > currencyStatus.fiatAmount
|
||||
} else {
|
||||
|
|
@ -88,10 +95,11 @@ internal class SendAmountFieldChangeConverter(
|
|||
if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1)
|
||||
|
||||
val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
|
||||
return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar)
|
||||
return trimmedValue.replace(TRIM_REGEX.toRegex(), separatorChar)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
|
||||
private const val TRIM_REGEX = "[.,]"
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ internal class SendAmountFieldConverter(
|
|||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
label = TextReference.Str(""),
|
||||
placeholder = TextReference.Str(DEFAULT_VALUE),
|
||||
isError = false,
|
||||
error = TextReference.Res(R.string.send_insufficient_funds),
|
||||
|
|
|
|||
|
|
@ -16,20 +16,43 @@ internal sealed class SendTextField {
|
|||
/** Keyboard options */
|
||||
abstract val keyboardOptions: KeyboardOptions
|
||||
|
||||
/** Label */
|
||||
abstract val label: TextReference
|
||||
|
||||
/** Placeholder (hint) */
|
||||
abstract val placeholder: TextReference
|
||||
// /** Placeholder (hint) */
|
||||
// abstract val placeholder: TextReference
|
||||
|
||||
data class Amount(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val placeholder: TextReference,
|
||||
val fiatValue: String,
|
||||
val isError: Boolean,
|
||||
val error: TextReference,
|
||||
) : SendTextField()
|
||||
|
||||
data class RecipientAddress(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val placeholder: TextReference,
|
||||
val label: TextReference,
|
||||
val isError: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
) : SendTextField()
|
||||
|
||||
data class RecipientMemo(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val placeholder: TextReference,
|
||||
val label: TextReference,
|
||||
val isError: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
) : SendTextField()
|
||||
|
||||
data class CustomFee(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val label: TextReference? = null,
|
||||
) : SendTextField()
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class SendRecipientAddressFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<Unit, MutableStateFlow<SendTextField.RecipientAddress>> {
|
||||
|
||||
override fun convert(value: Unit): MutableStateFlow<SendTextField.RecipientAddress> {
|
||||
return MutableStateFlow(
|
||||
SendTextField.RecipientAddress(
|
||||
value = "",
|
||||
onValueChange = clickIntents::onRecipientAddressValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
placeholder = TextReference.Res(R.string.send_enter_address_field),
|
||||
label = TextReference.Res(R.string.send_recipient),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import androidx.paging.*
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class SendRecipientListConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
fun convert(wallets: List<AvailableWallet?>, txHistory: PagingData<TxHistoryItem>) {
|
||||
val filteredWallets = wallets.filterNotNull()
|
||||
.groupBy { item -> item.name }
|
||||
.values.flatten()
|
||||
.mapIndexed { index, item ->
|
||||
item.copy(
|
||||
name = "${item.name} ${index.inc()}",
|
||||
)
|
||||
}
|
||||
|
||||
currentStateProvider().recipientList.update {
|
||||
txHistory.filter { item ->
|
||||
val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer
|
||||
val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User
|
||||
val isSingleAddress = if (item.isOutgoing) {
|
||||
item.destinationType is TxHistoryItem.DestinationType.Single
|
||||
} else {
|
||||
item.sourceType is TxHistoryItem.SourceType.Single
|
||||
}
|
||||
isTransfer && isSingleAddress && isNotContract
|
||||
}.map<TxHistoryItem, SendRecipientListContent> { tx ->
|
||||
SendRecipientListContent.Item(
|
||||
id = tx.txHash,
|
||||
title = tx.extractAddress(),
|
||||
subtitle = TextReference.Str(tx.getAmount()),
|
||||
info = tx.extractTimestamp(),
|
||||
subtitleIconRes = tx.extractIconRes(),
|
||||
)
|
||||
}.insertWallets(filteredWallets)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PagingData<SendRecipientListContent>.insertWallets(
|
||||
wallets: List<AvailableWallet>,
|
||||
): PagingData<SendRecipientListContent> {
|
||||
return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after ->
|
||||
return@insertSeparators when {
|
||||
before == null && after is SendRecipientListContent.Item -> {
|
||||
SendRecipientListContent.Wallets(
|
||||
wallets.map {
|
||||
SendRecipientListContent.Item(
|
||||
id = it.address,
|
||||
title = TextReference.Str(it.address),
|
||||
subtitle = TextReference.Str(it.name),
|
||||
)
|
||||
}.toPersistentList(),
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) {
|
||||
when (val destination = destinationType) {
|
||||
is TxHistoryItem.DestinationType.Multiple -> TextReference.Res(
|
||||
R.string.transaction_history_multiple_addresses,
|
||||
)
|
||||
is TxHistoryItem.DestinationType.Single -> TextReference.Str(destination.addressType.address)
|
||||
}
|
||||
} else {
|
||||
when (val source = sourceType) {
|
||||
is TxHistoryItem.SourceType.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses)
|
||||
is TxHistoryItem.SourceType.Single -> TextReference.Str(source.address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractIconRes() = if (isOutgoing) {
|
||||
R.drawable.ic_arrow_up_24
|
||||
} else {
|
||||
R.drawable.ic_arrow_down_24
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.getAmount(): String {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
return amount.toFormattedCurrencyString(
|
||||
currency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractTimestamp(): TextReference {
|
||||
val date = timestampInMillis.toDateFormat(
|
||||
formatter = DateTimeFormatters.dateDDMMYYYY,
|
||||
)
|
||||
val time = timestampInMillis.toTimeFormat()
|
||||
return TextReference.Res(R.string.send_date_format, wrappedList(date, time))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class SendRecipientMemoFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatus: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Int, MutableStateFlow<SendTextField.RecipientMemo>> {
|
||||
|
||||
fun convertOrNull(): MutableStateFlow<SendTextField.RecipientMemo>? {
|
||||
val cryptoCurrency = cryptoCurrencyStatus().currency
|
||||
|
||||
return when (cryptoCurrency.network.id.value) {
|
||||
Blockchain.XRP.id -> convert(R.string.send_destination_tag_field)
|
||||
Blockchain.Binance.id,
|
||||
Blockchain.TON.id,
|
||||
Blockchain.Cosmos.id,
|
||||
Blockchain.TerraV1.id,
|
||||
Blockchain.TerraV2.id,
|
||||
Blockchain.Stellar.id,
|
||||
-> convert(R.string.send_extras_hint_memo)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun convert(value: Int): MutableStateFlow<SendTextField.RecipientMemo> {
|
||||
return MutableStateFlow(
|
||||
SendTextField.RecipientMemo(
|
||||
value = "",
|
||||
onValueChange = clickIntents::onRecipientMemoValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
placeholder = TextReference.Res(R.string.send_optional_field),
|
||||
label = TextReference.Res(value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendRecipientStateConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Unit, SendStates.RecipientState> {
|
||||
|
||||
private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
|
||||
private val memoFieldConverter by lazy {
|
||||
SendRecipientMemoFieldConverter(
|
||||
clickIntents,
|
||||
cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: Unit): SendStates.RecipientState {
|
||||
return SendStates.RecipientState(
|
||||
addressTextField = addressFieldConverter.convert(Unit),
|
||||
memoTextField = memoFieldConverter.convertOrNull(),
|
||||
network = cryptoCurrencyStatusProvider().currency.network.name,
|
||||
isPrimaryButtonEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
@ -19,9 +20,10 @@ import com.tangem.core.ui.components.PrimaryButton
|
|||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
|
||||
@Composable
|
||||
internal fun SendNavigationButtons(uiState: SendUiState.Content) {
|
||||
internal fun SendNavigationButtons(uiState: SendUiState) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -38,9 +40,11 @@ internal fun SendNavigationButtons(uiState: SendUiState.Content) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) {
|
||||
private fun SendSecondaryNavigationButton(uiState: SendUiState) {
|
||||
val currentState = uiState.currentState.collectAsState()
|
||||
AnimatedVisibility(
|
||||
visible = uiState is SendUiState.Content.RecipientState || uiState is SendUiState.Content.FeeState,
|
||||
visible = currentState.value == SendUiStateType.Recipient ||
|
||||
currentState.value == SendUiStateType.Fee,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
|
|
@ -48,46 +52,52 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) {
|
|||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable {
|
||||
// todo add prev click
|
||||
uiState.clickIntents.onPrevClick()
|
||||
}
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
painter = painterResource(R.drawable.ic_back_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: Modifier = Modifier) {
|
||||
val buttonTextId = when (uiState) {
|
||||
is SendUiState.Content.AmountState,
|
||||
is SendUiState.Content.RecipientState,
|
||||
is SendUiState.Content.FeeState,
|
||||
private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) {
|
||||
val currentState = uiState.currentState.collectAsState()
|
||||
|
||||
val buttonTextId = when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Recipient,
|
||||
SendUiStateType.Fee,
|
||||
-> R.string.common_next
|
||||
is SendUiState.Content.SendState -> R.string.common_send
|
||||
SendUiStateType.Send -> R.string.common_send
|
||||
else -> R.string.common_close
|
||||
}
|
||||
|
||||
val isButtonEnabled = when (currentState.value) {
|
||||
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
|
||||
SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false
|
||||
else -> true
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = buttonTextId,
|
||||
label = "Update send screen state",
|
||||
modifier = modifier,
|
||||
) { textId ->
|
||||
if (uiState is SendUiState.Content.SendState) {
|
||||
if (currentState.value == SendUiStateType.Send) {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResource(textId),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = uiState.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
enabled = isButtonEnabled,
|
||||
onClick = uiState.clickIntents::onNextClick,
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
text = stringResource(textId),
|
||||
enabled = uiState.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
enabled = isButtonEnabled,
|
||||
onClick = uiState.clickIntents::onNextClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,95 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent
|
||||
import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent
|
||||
|
||||
@Composable
|
||||
internal fun SendScreen(uiState: SendUiState.Content) {
|
||||
internal fun SendScreen(uiState: SendUiState) {
|
||||
val currentState = uiState.currentState.collectAsStateWithLifecycle()
|
||||
BackHandler { uiState.clickIntents.onPrevClick() }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.imePadding()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens.radius24,
|
||||
topEnd = TangemTheme.dimens.radius24,
|
||||
),
|
||||
),
|
||||
.background(color = TangemTheme.colors.background.tertiary),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
TangemBottomSheetDraggableHeader(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical),
|
||||
) {
|
||||
SendScreenContent(uiState)
|
||||
val titleRes = when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Send,
|
||||
-> R.string.common_send
|
||||
SendUiStateType.Recipient -> R.string.send_recipient
|
||||
SendUiStateType.Fee -> R.string.common_fee_selector_title
|
||||
SendUiStateType.Done -> null
|
||||
}
|
||||
val iconRes = when (currentState.value) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Recipient,
|
||||
-> R.drawable.ic_qrcode_scan_24
|
||||
else -> null
|
||||
}
|
||||
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = titleRes?.let { stringResource(it) },
|
||||
onBackClick = uiState.clickIntents::onBackClick,
|
||||
onIconClick = uiState.clickIntents::onQrCodeScanClick,
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
iconRes = iconRes,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
SendScreenContent(
|
||||
uiState = uiState,
|
||||
currentState = currentState,
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
)
|
||||
SendNavigationButtons(uiState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendScreenContent(uiState: SendUiState.Content) {
|
||||
when (uiState) {
|
||||
is SendUiState.Content.AmountState -> SendAmountContent(uiState)
|
||||
else -> { /* [REDACTED_TODO_COMMENT]*/
|
||||
private fun SendScreenContent(
|
||||
uiState: SendUiState,
|
||||
currentState: State<SendUiStateType>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val recipientList = uiState.recipientList.collectAsLazyPagingItems()
|
||||
AnimatedContent(
|
||||
targetState = currentState.value,
|
||||
label = "Send Scree Navigation",
|
||||
modifier = modifier,
|
||||
) { state ->
|
||||
when (state) {
|
||||
SendUiStateType.Amount -> SendAmountContent(
|
||||
uiState.amountState,
|
||||
uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Recipient -> SendRecipientContent(
|
||||
uiState.recipientState,
|
||||
uiState.clickIntents,
|
||||
recipientList,
|
||||
)
|
||||
else -> { /* [REDACTED_TODO_COMMENT]*/ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,12 +18,8 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.fields.AmountVisualTransformation
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -140,29 +136,4 @@ private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: M
|
|||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class AmountVisualTransformation(
|
||||
private val symbol: String,
|
||||
) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
return TransformedText(
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
if (text.isNotBlank()) {
|
||||
append(" ")
|
||||
append(symbol)
|
||||
}
|
||||
},
|
||||
object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,12 +11,14 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
|
||||
@Composable
|
||||
internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) {
|
||||
internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) {
|
||||
val amountTextField = amountState.amountTextField.collectAsStateWithLifecycle()
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -52,10 +54,10 @@ internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState,
|
|||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
AmountField(
|
||||
sendField = amountState.amountTextField,
|
||||
sendField = amountTextField.value,
|
||||
isFiat = amountState.isFiatValue,
|
||||
cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol,
|
||||
fiatSymbol = amountState.appCurrency.symbol,
|
||||
isFiat = amountState.isFiatValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
package com.tangem.features.send.impl.presentation.ui.amount
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -6,10 +6,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
|
|
@ -18,29 +16,20 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.ui.amount.AmountFieldContainer
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
@Composable
|
||||
internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {
|
||||
internal fun SendAmountContent(amountState: SendStates.AmountState?, clickIntents: SendClickIntents) {
|
||||
if (amountState == null) return
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.common_send),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing16)
|
||||
.align(CenterHorizontally),
|
||||
)
|
||||
AmountFieldContainer(amountState = amountState)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
|
|
@ -49,16 +38,16 @@ internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {
|
|||
) {
|
||||
SegmentedButtons(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size40)
|
||||
.weight(1f),
|
||||
.weight(1f)
|
||||
.height(TangemTheme.dimens.size40),
|
||||
config = amountState.segmentedButtonConfig,
|
||||
onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) },
|
||||
onClick = { clickIntents.onCurrencyChangeClick(it.isFiat) },
|
||||
) {
|
||||
SendAmountCurrencyButton(it)
|
||||
}
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.send_max_amount),
|
||||
onClick = amountState.clickIntents::onMaxValueClick,
|
||||
onClick = clickIntents::onMaxValueClick,
|
||||
size = TangemButtonSize.Text,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
|
||||
modifier = Modifier
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Container for footer info below the text field
|
||||
*
|
||||
* @param modifier of component
|
||||
* @param footer text
|
||||
* @param footerTopPadding padding between footer and field
|
||||
* @param content field content
|
||||
*/
|
||||
@Composable
|
||||
internal fun FooterContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
footer: String? = null,
|
||||
footerTopPadding: Dp = TangemTheme.dimens.spacing8,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
content()
|
||||
footer?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = footerTopPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,9 +13,13 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import androidx.constraintlayout.compose.Visibility
|
||||
import com.tangem.core.ui.components.MiddleEllipsisText
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -28,75 +32,98 @@ import com.tangem.features.send.impl.R
|
|||
* @param subtitle subtitle
|
||||
* @param onClick click listener
|
||||
* @param modifier modifier
|
||||
* @param info info
|
||||
* @param subtitleIconRes icon
|
||||
*/
|
||||
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod")
|
||||
@Composable
|
||||
fun ListItemWithIcon(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
info: String? = null,
|
||||
@DrawableRes subtitleIconRes: Int? = null,
|
||||
) {
|
||||
Row(
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable { onClick() }
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
val (iconRef, titleRef, subtitleRef, subtitleIconRef, infoRef) = createRefs()
|
||||
|
||||
val spacing2 = TangemTheme.dimens.spacing2
|
||||
val spacing8 = TangemTheme.dimens.spacing8
|
||||
val spacing10 = TangemTheme.dimens.spacing10
|
||||
val spacing12 = TangemTheme.dimens.spacing12
|
||||
IdentIcon(
|
||||
address = title,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size40)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius20)),
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius20))
|
||||
.constrainAs(iconRef) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(parent.top, margin = spacing8)
|
||||
bottom.linkTo(parent.bottom, margin = spacing8)
|
||||
},
|
||||
)
|
||||
Column(
|
||||
MiddleEllipsisText(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing2,
|
||||
bottom = TangemTheme.dimens.spacing2,
|
||||
),
|
||||
) {
|
||||
MiddleEllipsisText(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
.constrainAs(titleRef) {
|
||||
start.linkTo(iconRef.end, margin = spacing12)
|
||||
end.linkTo(parent.end)
|
||||
top.linkTo(parent.top, margin = spacing10)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = subtitleIconRes ?: R.drawable.ic_arrow_down_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.background(TangemTheme.colors.background.tertiary, CircleShape)
|
||||
.constrainAs(subtitleIconRef) {
|
||||
start.linkTo(iconRef.end, margin = spacing12)
|
||||
top.linkTo(titleRef.bottom)
|
||||
bottom.linkTo(parent.bottom, margin = spacing10)
|
||||
visibility = if (subtitleIconRes == null) Visibility.Gone else Visibility.Visible
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.constrainAs(subtitleRef) {
|
||||
start.linkTo(subtitleIconRef.end, margin = spacing2, goneMargin = spacing12)
|
||||
end.linkTo(infoRef.start)
|
||||
top.linkTo(titleRef.bottom)
|
||||
bottom.linkTo(parent.bottom, margin = spacing10)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
info?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.constrainAs(infoRef) {
|
||||
start.linkTo(subtitleRef.end, goneMargin = spacing12)
|
||||
end.linkTo(parent.end)
|
||||
top.linkTo(titleRef.bottom)
|
||||
bottom.linkTo(parent.bottom, margin = spacing10)
|
||||
},
|
||||
)
|
||||
Row {
|
||||
subtitleIconRes?.let { iconRes ->
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.background(TangemTheme.colors.background.tertiary, CircleShape)
|
||||
.padding(TangemTheme.dimens.spacing3),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (subtitleIconRes != null) {
|
||||
Modifier.padding(start = TangemTheme.dimens.spacing4)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,6 +138,7 @@ private fun ListItemWithIconPreview_Light(
|
|||
ListItemWithIcon(
|
||||
title = config.title,
|
||||
subtitle = config.subtitle,
|
||||
info = config.info,
|
||||
subtitleIconRes = config.iconRes,
|
||||
onClick = {},
|
||||
)
|
||||
|
|
@ -126,6 +154,7 @@ private fun ListItemWithIconPreview_Dark(
|
|||
ListItemWithIcon(
|
||||
title = config.title,
|
||||
subtitle = config.subtitle,
|
||||
info = config.info,
|
||||
subtitleIconRes = config.iconRes,
|
||||
onClick = {},
|
||||
)
|
||||
|
|
@ -135,6 +164,7 @@ private fun ListItemWithIconPreview_Dark(
|
|||
private data class ListItemWithIconPreviewConfig(
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val info: String? = null,
|
||||
val iconRes: Int? = null,
|
||||
)
|
||||
|
||||
|
|
@ -142,7 +172,14 @@ private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvid
|
|||
collection = listOf(
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "Wallet",
|
||||
subtitle = "0.000000000000000000000000000000 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "1 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.recipient
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
import androidx.paging.compose.itemKey
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
|
||||
private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY"
|
||||
private const val MY_WALLETS_HEADER_KEY = "MY_WALLETS_HEADER_KEY"
|
||||
|
||||
@Composable
|
||||
internal fun SendRecipientContent(
|
||||
uiState: SendStates.RecipientState?,
|
||||
clickIntents: SendClickIntents,
|
||||
recipientList: LazyPagingItems<SendRecipientListContent>,
|
||||
) {
|
||||
if (uiState == null) return
|
||||
val address = uiState.addressTextField.collectAsState().value
|
||||
val memo = uiState.memoTextField?.collectAsState()?.value
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
item(key = ADDRESS_FIELD_KEY) {
|
||||
TextFieldWithPasteAndIcon(
|
||||
value = address.value,
|
||||
label = address.label,
|
||||
placeholder = address.placeholder,
|
||||
footer = stringResource(R.string.send_recipient_address_footer, uiState.network),
|
||||
onValueChange = address.onValueChange,
|
||||
onPasteClick = clickIntents::onRecipientAddressValueChange,
|
||||
singleLine = true,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing4),
|
||||
isError = address.isError,
|
||||
error = address.error,
|
||||
)
|
||||
}
|
||||
memo?.let { memoField ->
|
||||
item(key = MEMO_FIELD_KEY) {
|
||||
TextFieldWithPaste(
|
||||
value = memoField.value,
|
||||
label = memoField.label,
|
||||
placeholder = memoField.placeholder,
|
||||
footer = stringResource(R.string.send_recipient_memo_footer),
|
||||
onValueChange = memoField.onValueChange,
|
||||
onPasteClick = clickIntents::onRecipientMemoValueChange,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
|
||||
isError = memoField.isError,
|
||||
error = memoField.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
recipientListItem(
|
||||
recipientList = recipientList,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.recipientListItem(
|
||||
recipientList: LazyPagingItems<SendRecipientListContent>,
|
||||
clickIntents: SendClickIntents,
|
||||
) {
|
||||
items(
|
||||
count = recipientList.itemCount,
|
||||
key = recipientList.itemKey {
|
||||
when (it) {
|
||||
is SendRecipientListContent.Wallets -> MY_WALLETS_HEADER_KEY
|
||||
is SendRecipientListContent.Item -> it.id
|
||||
}
|
||||
},
|
||||
contentType = recipientList.itemContentType { it::class.java },
|
||||
) { index ->
|
||||
recipientList[index]?.let { item ->
|
||||
when (item) {
|
||||
is SendRecipientListContent.Wallets -> {
|
||||
RecipientWalletListItem(
|
||||
item = item,
|
||||
clickIntents = clickIntents,
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.padding(top = TangemTheme.dimens.spacing20)
|
||||
.then(
|
||||
if (index == 0) {
|
||||
Modifier.clip(
|
||||
RoundedCornerShape(
|
||||
topEnd = TangemTheme.dimens.radius12,
|
||||
topStart = TangemTheme.dimens.radius12,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
is SendRecipientListContent.Item -> {
|
||||
val title = item.title.resolveReference()
|
||||
ListItemWithIcon(
|
||||
title = item.title.resolveReference(),
|
||||
subtitle = item.subtitle.resolveReference(),
|
||||
info = item.info?.let { ", ${it.resolveReference()}" },
|
||||
subtitleIconRes = item.subtitleIconRes,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (index == recipientList.itemCount - 1) {
|
||||
Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing20)
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
bottomEnd = TangemTheme.dimens.radius12,
|
||||
bottomStart = TangemTheme.dimens.radius12,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
onClick = { clickIntents.onRecipientAddressValueChange(title) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecipientWalletListItem(
|
||||
item: SendRecipientListContent.Wallets,
|
||||
clickIntents: SendClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(top = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
if (item.list.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.send_recipient_wallets_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
}
|
||||
item.list.forEachIndexed { _, wallet ->
|
||||
val title = wallet.title.resolveReference()
|
||||
ListItemWithIcon(
|
||||
title = wallet.title.resolveReference(),
|
||||
subtitle = wallet.subtitle.resolveReference(),
|
||||
onClick = { clickIntents.onRecipientAddressValueChange(title) },
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.send_recent_transactions),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
bottom = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,14 +23,15 @@ import androidx.compose.ui.platform.LocalClipboardManager
|
|||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
|
||||
@Composable
|
||||
internal fun TextFieldWithPasteAndIcon(
|
||||
|
|
@ -42,7 +43,14 @@ internal fun TextFieldWithPasteAndIcon(
|
|||
modifier: Modifier = Modifier,
|
||||
footer: String? = null,
|
||||
singleLine: Boolean = false,
|
||||
error: TextReference? = null,
|
||||
isError: Boolean = false,
|
||||
) {
|
||||
val (title, color) = if (isError && error != null) {
|
||||
error to TangemTheme.colors.text.warning
|
||||
} else {
|
||||
label to TangemTheme.colors.text.secondary
|
||||
}
|
||||
FooterContainer(modifier, footer) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -53,9 +61,9 @@ internal fun TextFieldWithPasteAndIcon(
|
|||
),
|
||||
) {
|
||||
Text(
|
||||
text = label.resolveReference(),
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
color = color,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
|
|
@ -114,7 +122,14 @@ internal fun TextFieldWithPaste(
|
|||
onPasteClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
footer: String? = null,
|
||||
error: TextReference? = null,
|
||||
isError: Boolean = false,
|
||||
) {
|
||||
val (title, color) = if (isError && error != null) {
|
||||
error to TangemTheme.colors.text.warning
|
||||
} else {
|
||||
label to TangemTheme.colors.text.secondary
|
||||
}
|
||||
FooterContainer(modifier, footer) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -129,9 +144,9 @@ internal fun TextFieldWithPaste(
|
|||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = label.resolveReference(),
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
color = color,
|
||||
)
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
|
|
@ -160,6 +175,7 @@ internal fun TextFieldWithInfo(
|
|||
modifier: Modifier = Modifier,
|
||||
info: TextReference? = null,
|
||||
footer: String? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
) {
|
||||
FooterContainer(
|
||||
footer = footer,
|
||||
|
|
@ -189,6 +205,7 @@ internal fun TextFieldWithInfo(
|
|||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
visualTransformation = visualTransformation,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing6)
|
||||
.weight(1f),
|
||||
|
|
@ -208,27 +225,6 @@ internal fun TextFieldWithInfo(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FooterContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
footer: String? = null,
|
||||
footerTopPadding: Dp = TangemTheme.dimens.spacing8,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
content()
|
||||
footer?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = footerTopPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
|
@ -287,6 +283,7 @@ private fun SimpleTextField(
|
|||
modifier: Modifier = Modifier,
|
||||
placeholder: TextReference? = null,
|
||||
singleLine: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
BasicTextField(
|
||||
|
|
@ -295,6 +292,7 @@ private fun SimpleTextField(
|
|||
textStyle = TangemTheme.typography.body2,
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
visualTransformation = visualTransformation,
|
||||
decorationBox = { textValue ->
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
internal fun verifyAddress(address: String, cryptoCurrency: CryptoCurrency?): Boolean {
|
||||
if (address.isEmpty()) return true
|
||||
val blockchain = cryptoCurrency?.let {
|
||||
Blockchain.fromId(cryptoCurrency.id.rawNetworkId)
|
||||
} ?: return false
|
||||
|
||||
return blockchain.validateAddress(address)
|
||||
}
|
||||
|
||||
internal fun isNotAddressInWallet(walletAddresses: Set<Address>, address: String): Boolean {
|
||||
return walletAddresses.all { it.value != address }
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import java.math.BigInteger
|
||||
|
||||
internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean {
|
||||
if (cryptoCurrency == null) return false
|
||||
return when (cryptoCurrency.network.id.value) {
|
||||
Blockchain.XRP.id -> {
|
||||
val tag = memo.toLongOrNull()
|
||||
tag != null && tag <= XRP_TAG_MAX_NUMBER
|
||||
}
|
||||
Blockchain.Stellar.id -> {
|
||||
isAssignableValue(memo)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAssignableValue(value: String): Boolean {
|
||||
val memoType = when {
|
||||
value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID
|
||||
else -> XlmMemoType.TEXT
|
||||
}
|
||||
return when (memoType) {
|
||||
XlmMemoType.TEXT -> {
|
||||
// from org.stellar.sdk.MemoText
|
||||
value.toByteArray().size <= XLM_MEMO_MAX_LENGTH
|
||||
}
|
||||
XlmMemoType.ID -> {
|
||||
try {
|
||||
// from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo
|
||||
value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger()
|
||||
} catch (ex: NumberFormatException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class XlmMemoType { TEXT, ID }
|
||||
|
||||
private const val XRP_TAG_MAX_NUMBER = 4294967295
|
||||
private const val XLM_MEMO_MAX_LENGTH = 28
|
||||
|
|
@ -2,13 +2,25 @@ package com.tangem.features.send.impl.presentation.viewmodel
|
|||
|
||||
interface SendClickIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onNextClick()
|
||||
|
||||
fun onPrevClick()
|
||||
|
||||
fun onQrCodeScanClick()
|
||||
|
||||
// region Amount
|
||||
fun onAmountValueChange(value: String)
|
||||
|
||||
fun onCurrencyChangeClick(isFiat: Boolean)
|
||||
|
||||
fun onMaxValueClick()
|
||||
// endregion
|
||||
|
||||
// region Recipient
|
||||
fun onRecipientAddressValueChange(value: String)
|
||||
|
||||
fun onRecipientMemoValueChange(value: String)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -4,32 +4,53 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import androidx.paging.PagingData
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.SendStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class SendViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
|
||||
|
||||
|
|
@ -42,26 +63,41 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
private var innerRouter: StateRouter by Delegates.notNull()
|
||||
|
||||
private val stateFactory = SendStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
walletAddressesProvider = Provider { walletAddresses },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
)
|
||||
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
||||
private var userWallet: UserWallet? = null
|
||||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var walletAddresses = emptySet<Address>()
|
||||
|
||||
private var balanceJobHolder = JobHolder()
|
||||
private var recipientsJobHolder = JobHolder()
|
||||
private var walletAddressesJobHolder = JobHolder()
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
getWalletAddresses()
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
getWalletsAndRecent()
|
||||
}
|
||||
|
||||
fun setRouter(router: StateRouter) {
|
||||
innerRouter = router
|
||||
uiState = uiState.copy(currentState = router.currentState)
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
getUserWalletUseCase(userWalletId).fold(
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
|
|
@ -86,11 +122,12 @@ internal class SendViewModel @Inject constructor(
|
|||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { either ->
|
||||
uiState = stateFactory.getAmountState(
|
||||
cryptoCurrencyStatus = either,
|
||||
)
|
||||
either.onRight {
|
||||
cryptoCurrencyStatus = it
|
||||
uiState = stateFactory.getReadyState()
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceJobHolder)
|
||||
}
|
||||
|
|
@ -107,27 +144,81 @@ internal class SendViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
// region screen state navigation
|
||||
override fun onNextClick() {
|
||||
when (uiState) {
|
||||
is SendUiState.Content.AmountState -> onRecipientStateClick()
|
||||
is SendUiState.Content.RecipientState -> onFeeStateClick()
|
||||
else -> {
|
||||
// todo implement
|
||||
private fun getWalletsAndRecent() {
|
||||
combine(
|
||||
flow = getUserWallets().conflate(),
|
||||
flow2 = getTxHistory().conflate(),
|
||||
) { wallets, txHistory ->
|
||||
stateFactory.onLoadedRecipientList(
|
||||
wallets = wallets,
|
||||
txHistory = txHistory,
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(recipientsJobHolder)
|
||||
}
|
||||
|
||||
private fun getUserWallets(): Flow<List<AvailableWallet?>> {
|
||||
return getWalletsUseCase()
|
||||
.distinctUntilChanged()
|
||||
.map { userWallets ->
|
||||
coroutineScope {
|
||||
userWallets
|
||||
.filterNot { it.walletId == userWalletId || it.isLocked }
|
||||
.map { wallet ->
|
||||
async(dispatchers.io) {
|
||||
getCryptoCurrenciesUseCase(wallet.walletId)
|
||||
.fold(
|
||||
ifRight = { currencyItem ->
|
||||
val walletCurrency = currencyItem.firstOrNull {
|
||||
it.network.id == cryptoCurrency.network.id
|
||||
} ?: return@fold null
|
||||
val addresses = walletManagersFacade.getAddress(
|
||||
userWalletId = wallet.walletId,
|
||||
network = walletCurrency.network,
|
||||
)
|
||||
return@fold AvailableWallet(
|
||||
name = wallet.name,
|
||||
address = addresses.first().value,
|
||||
)
|
||||
},
|
||||
ifLeft = { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTxHistory(): Flow<PagingData<TxHistoryItem>> {
|
||||
return flow {
|
||||
txHistoryItemsUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
).fold(
|
||||
ifRight = { emitAll(it.distinctUntilChanged()) },
|
||||
ifLeft = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrevClick() {
|
||||
// todo implement
|
||||
private fun getWalletAddresses() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
walletAddresses = walletManagersFacade.getAddresses(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
}.saveIn(walletAddressesJobHolder)
|
||||
}
|
||||
|
||||
private fun onRecipientStateClick() {
|
||||
stateFactory.getOnReceiveState()
|
||||
}
|
||||
// region screen state navigation
|
||||
override fun onBackClick() = innerRouter.onBackClick()
|
||||
override fun onNextClick() = innerRouter.onNextClick()
|
||||
override fun onPrevClick() = innerRouter.onPrevClick()
|
||||
|
||||
private fun onFeeStateClick() {
|
||||
// todo implement
|
||||
override fun onQrCodeScanClick() {
|
||||
// TODO Add QR code scanning
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
@ -141,14 +232,44 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onMaxValueClick() {
|
||||
val amountState = uiState as? SendUiState.Content.AmountState ?: return
|
||||
|
||||
val amountState = uiState.amountState ?: return
|
||||
val amount = if (amountState.isFiatValue) {
|
||||
amountState.cryptoCurrencyStatus.value.fiatAmount
|
||||
} else {
|
||||
amountState.cryptoCurrencyStatus.value.amount
|
||||
}
|
||||
onAmountValueChange(amount?.toPlainString() ?: "0.00")
|
||||
onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region recipient state clicks
|
||||
override fun onRecipientAddressValueChange(value: String) {
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = stateFactory.getOnRecipientAddressValueChangeState(value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecipientMemoValueChange(value: String) {
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = stateFactory.getOnRecipientMemoValueChangeState(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkIfXrpAddressValue(value: String): Boolean {
|
||||
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val result = XrpAddressService.decodeXAddress(value)
|
||||
onRecipientAddressValueChange(result?.address.orEmpty())
|
||||
onRecipientMemoValueChange(result?.destinationTag.toString())
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val XRP_X_ADDRESS = 'X'
|
||||
private const val DEFAULT_VALUE = "0.00"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,19 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.paging.*
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isToday
|
||||
import com.tangem.utils.extensions.isYesterday
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
|
||||
internal class TokenDetailsTxHistoryItemFlowConverter(
|
||||
private val currentStateProvider: Provider<TokenDetailsState>,
|
||||
|
|
@ -101,7 +97,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
|||
) {
|
||||
val txContent = txHistoryItemState.state as TransactionState.Content
|
||||
txHistoryItemState.copy(
|
||||
state = txContent.copy(timestamp = txContent.timestamp.toTimeFormat()),
|
||||
state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()),
|
||||
)
|
||||
} else {
|
||||
txHistoryItemState
|
||||
|
|
@ -117,26 +113,4 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If [this] timestamp is today or yesterday, returns relative date,
|
||||
* otherwise returns formatting date.
|
||||
*/
|
||||
private fun Long.toDateFormat(): String {
|
||||
val localDate = DateTime(this, DateTimeZone.getDefault())
|
||||
return if (localDate.isToday() || localDate.isYesterday()) {
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
this,
|
||||
DateTime.now().millis,
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE,
|
||||
).toString()
|
||||
} else {
|
||||
DateTimeFormatters.formatDate(date = localDate)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toTimeFormat(): String {
|
||||
return DateTimeFormatters.formatTime(time = DateTime(this.toLong(), DateTimeZone.getDefault()))
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ compose-navigation = "2.7.4"
|
|||
compose-accompanist = "0.30.1"
|
||||
compose-paging = "3.2.1"
|
||||
compose-reorderable = "0.9.6"
|
||||
compoese-lifecycle-runtime = "2.6.2"
|
||||
compose-lifecycle-runtime = "2.6.2"
|
||||
# endregion Compose
|
||||
|
||||
# region Other libraries
|
||||
|
|
@ -142,7 +142,7 @@ androidx-palette = { module = "androidx.palette:palette", version.ref = "android
|
|||
lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" }
|
||||
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
|
||||
lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
|
||||
lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compoese-lifecycle-runtime" }
|
||||
lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle-runtime" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
|
||||
# region AndroidX
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue