Updated on 2026-08-14
This commit is contained in:
parent
330ae73357
commit
e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions
1
common/ui/.gitignore
vendored
Normal file
1
common/ui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
46
common/ui/build.gradle.kts
Normal file
46
common/ui/build.gradle.kts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.common.ui"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.material)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.constraintLayout)
|
||||
|
||||
/** Deps */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
||||
/** Project - Common */
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.promo.models)
|
||||
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.common.ui.alerts
|
||||
|
||||
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
||||
import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class TransactionErrorAlertConverter(
|
||||
private val popBackStack: () -> Unit,
|
||||
private val onFailedTxEmailClick: (String) -> Unit,
|
||||
) : Converter<SendTransactionError, AlertUM?> {
|
||||
override fun convert(value: SendTransactionError): AlertUM? {
|
||||
return when (value) {
|
||||
is SendTransactionError.DemoCardError -> AlertDemoModeUM(
|
||||
onConfirmClick = popBackStack,
|
||||
)
|
||||
is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM(
|
||||
code = value.code.toString(),
|
||||
cause = null,
|
||||
causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)),
|
||||
onConfirmClick = { onFailedTxEmailClick(value.code.toString()) },
|
||||
)
|
||||
is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM(
|
||||
code = value.code.toString(),
|
||||
cause = value.message,
|
||||
onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
|
||||
)
|
||||
is SendTransactionError.DataError -> AlertTransactionErrorUM(
|
||||
code = "",
|
||||
cause = value.message,
|
||||
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.NetworkError -> AlertTransactionErrorUM(
|
||||
code = value.code.orEmpty(),
|
||||
cause = value.message.orEmpty(),
|
||||
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.UnknownError -> AlertTransactionErrorUM(
|
||||
code = "",
|
||||
cause = value.ex?.localizedMessage,
|
||||
onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.common.ui.alerts.models
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
||||
data class AlertDemoModeUM(
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : AlertUM {
|
||||
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
|
||||
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
|
||||
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.common.ui.alerts.models
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
|
||||
data class AlertTransactionErrorUM(
|
||||
val code: String,
|
||||
val cause: String?,
|
||||
val causeTextReference: TextReference? = null,
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : AlertUM {
|
||||
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
|
||||
override val message: TextReference = resourceReference(
|
||||
id = R.string.send_alert_transaction_failed_text,
|
||||
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
|
||||
)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.common_support)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.common.ui.alerts.models
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
interface AlertUM {
|
||||
val title: TextReference?
|
||||
val message: TextReference
|
||||
val confirmButtonText: TextReference
|
||||
val onConfirmClick: (() -> Unit)?
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.common.ui.amountScreen
|
||||
|
||||
/** Amount screen clicks */
|
||||
interface AmountScreenClickIntents {
|
||||
|
||||
/** On amount [value] changed */
|
||||
fun onAmountValueChange(value: String)
|
||||
|
||||
/** Click triggered on value paste */
|
||||
fun onAmountPasteTriggerDismiss()
|
||||
|
||||
/** On max amount click */
|
||||
fun onMaxValueClick()
|
||||
|
||||
/**
|
||||
* On currency change from crypto currency to app currency clicked
|
||||
*
|
||||
* @param isFiat indicates currency to change
|
||||
*/
|
||||
fun onCurrencyChangeClick(isFiat: Boolean)
|
||||
|
||||
/** On next screen click */
|
||||
fun onAmountNext()
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.common.ui.amountScreen
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub
|
||||
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
||||
import com.tangem.common.ui.amountScreen.ui.amountField
|
||||
import com.tangem.common.ui.amountScreen.ui.buttons
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
/**
|
||||
* Amount screen with field
|
||||
* @param amountState amount state
|
||||
* @param isBalanceHidden flag hidden balances
|
||||
* @param clickIntents amount screen clicks
|
||||
*/
|
||||
@Composable
|
||||
fun AmountScreenContent(
|
||||
amountState: AmountState,
|
||||
isBalanceHidden: Boolean,
|
||||
clickIntents: AmountScreenClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (amountState !is AmountState.Data) return
|
||||
|
||||
// Do not put fillMaxSize() in here
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
amountField(amountState = amountState, isBalanceHidden = isBalanceHidden)
|
||||
buttons(
|
||||
segmentedButtonConfig = amountState.segmentedButtonConfig,
|
||||
clickIntents = clickIntents,
|
||||
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
|
||||
selectedButton = amountState.selectedButton,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SendAmountContentPreview(
|
||||
@PreviewParameter(SendAmountContentPreviewProvider::class) amountState: AmountState,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
AmountScreenContent(
|
||||
amountState = amountState,
|
||||
isBalanceHidden = false,
|
||||
clickIntents = AmountScreenClickIntentsStub,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class SendAmountContentPreviewProvider : PreviewParameterProvider<AmountState> {
|
||||
override val values: Sequence<AmountState>
|
||||
get() = sequenceOf(
|
||||
AmountStatePreviewData.amountState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.common.ui.amountScreen.converters
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
/**
|
||||
* Selected currency change from crypto currency to app currency and vice versa
|
||||
*
|
||||
* @property cryptoCurrencyStatus current cryptocurrency status
|
||||
* @property value is crypto currency or app currency
|
||||
*/
|
||||
class AmountCurrencyTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val value: Boolean,
|
||||
) : Transformer<AmountState> {
|
||||
|
||||
override fun transform(prevState: AmountState): AmountState {
|
||||
if (prevState !is AmountState.Data) return prevState
|
||||
|
||||
val amountTextField = prevState.amountTextField
|
||||
|
||||
val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
|
||||
val isDoneActionEnabled = prevState.isPrimaryButtonEnabled
|
||||
return if (amountTextField.isFiatValue == value && !isValidFiatRate) {
|
||||
prevState
|
||||
} else {
|
||||
return prevState.copy(
|
||||
amountTextField = amountTextField.copy(
|
||||
isFiatValue = value,
|
||||
isValuePasted = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
selectedButton = prevState.segmentedButtonConfig.indexOfFirst { it.isFiat == value },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.common.ui.amountScreen.converters
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
/**
|
||||
* Dismisses indication on pasted value
|
||||
*/
|
||||
class AmountPastedTriggerDismissTransformer : Transformer<AmountState> {
|
||||
override fun transform(prevState: AmountState): AmountState {
|
||||
if (prevState !is AmountState.Data) return prevState
|
||||
|
||||
return prevState.copy(
|
||||
amountTextField = prevState.amountTextField.copy(
|
||||
isValuePasted = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.common.ui.amountScreen.converters
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatValue
|
||||
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Reduces amount by specific value
|
||||
*
|
||||
* @property cryptoCurrencyStatus current cryptocurrency status
|
||||
* @property value reduced by value
|
||||
*/
|
||||
class AmountReduceByTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val minimumTransactionAmount: EnterAmountBoundary?,
|
||||
private val value: ReduceByData,
|
||||
) : Transformer<AmountState> {
|
||||
|
||||
private val maxEnterAmountConverter = MaxEnterAmountConverter()
|
||||
|
||||
override fun transform(prevState: AmountState): AmountState {
|
||||
if (prevState !is AmountState.Data) return prevState
|
||||
|
||||
val amountTextField = prevState.amountTextField
|
||||
val cryptoDecimals = amountTextField.cryptoAmount.decimals
|
||||
val fiatDecimals = amountTextField.fiatAmount.decimals
|
||||
val amountValue = prevState.amountTextField.cryptoAmount.value ?: return prevState
|
||||
|
||||
val decimalCryptoValue = amountValue.minus(value.reduceAmountByDiff)
|
||||
val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals)
|
||||
val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue(
|
||||
fiatRate = cryptoCurrencyStatus.value.fiatRate,
|
||||
isFiatValue = false,
|
||||
decimals = fiatDecimals,
|
||||
)
|
||||
|
||||
val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus)
|
||||
|
||||
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
|
||||
val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField)
|
||||
val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false
|
||||
val isZero = if (amountTextField.isFiatValue) {
|
||||
decimalFiatValue.isNullOrZero()
|
||||
} else {
|
||||
decimalCryptoValue.isNullOrZero()
|
||||
}
|
||||
val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided
|
||||
return prevState.copy(
|
||||
isPrimaryButtonEnabled = !isZero && !isCheckFailed,
|
||||
amountTextField = amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isCheckFailed,
|
||||
error = when {
|
||||
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
|
||||
isLessThanMinimumIfProvided -> {
|
||||
val minimumAmount = minimumTransactionAmount
|
||||
?.amount
|
||||
?.format { crypto(cryptoCurrencyStatus.currency) }
|
||||
.orEmpty()
|
||||
resourceReference(
|
||||
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
|
||||
wrappedList(minimumAmount, minimumAmount),
|
||||
)
|
||||
}
|
||||
else -> TextReference.EMPTY
|
||||
},
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = getKeyboardAction(isCheckFailed, decimalCryptoValue),
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class ReduceByData(
|
||||
val reduceAmountBy: BigDecimal,
|
||||
val reduceAmountByDiff: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.tangem.common.ui.amountScreen.converters
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatValue
|
||||
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Reduces amount to specific value
|
||||
*
|
||||
* @property cryptoCurrencyStatus current cryptocurrency status
|
||||
* @property value reduced to value
|
||||
*/
|
||||
class AmountReduceToTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val minimumTransactionAmount: EnterAmountBoundary?,
|
||||
private val value: BigDecimal,
|
||||
) : Transformer<AmountState> {
|
||||
private val maxEnterAmountConverter = MaxEnterAmountConverter()
|
||||
|
||||
override fun transform(prevState: AmountState): AmountState {
|
||||
if (prevState !is AmountState.Data) return prevState
|
||||
|
||||
val amountTextField = prevState.amountTextField
|
||||
val cryptoDecimals = amountTextField.cryptoAmount.decimals
|
||||
val fiatDecimals = amountTextField.fiatAmount.decimals
|
||||
|
||||
val cryptoValue = value.parseBigDecimal(cryptoDecimals)
|
||||
val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals)
|
||||
val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue(
|
||||
fiatRate = cryptoCurrencyStatus.value.fiatRate,
|
||||
isFiatValue = false,
|
||||
decimals = fiatDecimals,
|
||||
)
|
||||
|
||||
val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus)
|
||||
|
||||
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
|
||||
val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField)
|
||||
val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false
|
||||
val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isNullOrZero()
|
||||
val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided
|
||||
return prevState.copy(
|
||||
isPrimaryButtonEnabled = !isZero && !isCheckFailed,
|
||||
amountTextField = amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isCheckFailed,
|
||||
error = when {
|
||||
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
|
||||
isLessThanMinimumIfProvided -> {
|
||||
val minimumAmount = minimumTransactionAmount
|
||||
?.amount
|
||||
?.format { crypto(cryptoCurrencyStatus.currency) }
|
||||
.orEmpty()
|
||||
resourceReference(
|
||||
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
|
||||
wrappedList(minimumAmount, minimumAmount),
|
||||
)
|
||||
}
|
||||
else -> TextReference.EMPTY
|
||||
},
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = value),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = getKeyboardAction(isCheckFailed, decimalCryptoValue),
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.tangem.common.ui.amountScreen.converters
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter
|
||||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Converts initial [String] to [AmountState]
|
||||
*
|
||||
* @property clickIntents amount screen clicks
|
||||
* @property appCurrencyProvider selected app currency provider
|
||||
* @property maxEnterAmount max enter amount data
|
||||
* @property cryptoCurrencyStatusProvider current cryptocurrency status provider
|
||||
* @property iconStateConverter currency icon converter
|
||||
*/
|
||||
class AmountStateConverter(
|
||||
private val clickIntents: AmountScreenClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val maxEnterAmount: EnterAmountBoundary,
|
||||
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
|
||||
) : Converter<AmountParameters, AmountState> {
|
||||
|
||||
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
AmountFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: AmountParameters): AmountState {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val status = cryptoCurrencyStatusProvider()
|
||||
val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) }
|
||||
val crypto = maxEnterAmount.amount.format { crypto(status.currency) }
|
||||
val noFeeRate = status.value.fiatRate.isNullOrZero()
|
||||
|
||||
return AmountState.Data(
|
||||
title = value.title,
|
||||
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
|
||||
tokenIconState = iconStateConverter.convert(status),
|
||||
amountTextField = amountFieldConverter.convert(value.value),
|
||||
isPrimaryButtonEnabled = false,
|
||||
appCurrencyCode = appCurrency.code,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
AmountSegmentedButtonsConfig(
|
||||
title = stringReference(status.currency.symbol),
|
||||
iconState = iconStateConverter.convertCustom(
|
||||
value = status,
|
||||
forceGrayscale = noFeeRate,
|
||||
showCustomTokenBadge = false,
|
||||
),
|
||||
isFiat = false,
|
||||
),
|
||||
AmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconUrl = appCurrency.iconSmallUrl,
|
||||
isFiat = true,
|
||||
),
|
||||
),
|
||||
isSegmentedButtonsEnabled = !noFeeRate,
|
||||
selectedButton = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.common.ui.amountScreen.converters
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converts [CryptoCurrencyStatus] to [EnterAmountBoundary]
|
||||
*/
|
||||
class MaxEnterAmountConverter : Converter<CryptoCurrencyStatus, EnterAmountBoundary> {
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): EnterAmountBoundary {
|
||||
return EnterAmountBoundary(
|
||||
amount = value.value.amount,
|
||||
fiatAmount = value.value.fiatAmount,
|
||||
fiatRate = value.value.fiatRate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.common.ui.amountScreen.converters.field
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
|
||||
import com.tangem.common.ui.amountScreen.utils.getCryptoValue
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatValue
|
||||
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Amount value change
|
||||
*
|
||||
* @property maxEnterAmount max amount to enter
|
||||
* @property value amount value
|
||||
*/
|
||||
class AmountFieldChangeTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val maxEnterAmount: EnterAmountBoundary,
|
||||
private val minimumTransactionAmount: EnterAmountBoundary?,
|
||||
private val value: String,
|
||||
) : Transformer<AmountState> {
|
||||
|
||||
override fun transform(prevState: AmountState): AmountState {
|
||||
if (prevState !is AmountState.Data) return prevState
|
||||
|
||||
val amountTextField = prevState.amountTextField
|
||||
|
||||
if (value.isEmpty()) return prevState.emptyState()
|
||||
val cryptoDecimals = amountTextField.cryptoAmount.decimals
|
||||
val fiatDecimals = amountTextField.fiatAmount.decimals
|
||||
|
||||
val trimmedValue = value.trim()
|
||||
val cryptoValue = trimmedValue.getCryptoValue(
|
||||
fiatRate = maxEnterAmount.fiatRate,
|
||||
isFiatValue = amountTextField.isFiatValue,
|
||||
decimals = cryptoDecimals,
|
||||
)
|
||||
val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals)
|
||||
val (fiatValue, decimalFiatValue) = trimmedValue.getFiatValue(
|
||||
fiatRate = maxEnterAmount.fiatRate,
|
||||
isFiatValue = amountTextField.isFiatValue,
|
||||
decimals = fiatDecimals,
|
||||
)
|
||||
|
||||
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
|
||||
val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField)
|
||||
val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false
|
||||
val isZero = if (amountTextField.isFiatValue) {
|
||||
decimalFiatValue.isNullOrZero()
|
||||
} else {
|
||||
decimalCryptoValue.isNullOrZero()
|
||||
}
|
||||
val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided
|
||||
return prevState.copy(
|
||||
isPrimaryButtonEnabled = !isZero && !isCheckFailed,
|
||||
amountTextField = amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isCheckFailed,
|
||||
error = when {
|
||||
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
|
||||
isLessThanMinimumIfProvided -> {
|
||||
val minimumAmount = minimumTransactionAmount
|
||||
?.amount
|
||||
?.format { crypto(cryptoCurrencyStatus.currency) }
|
||||
.orEmpty()
|
||||
resourceReference(
|
||||
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
|
||||
wrappedList(minimumAmount, minimumAmount),
|
||||
)
|
||||
}
|
||||
else -> TextReference.EMPTY
|
||||
},
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = getKeyboardAction(isCheckFailed, decimalCryptoValue),
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun AmountState.Data.emptyState(): AmountState.Data {
|
||||
return copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
amountTextField = amountTextField.copy(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO),
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.common.ui.amountScreen.converters.field
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.convertToAmount
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Converts initial [String] to [AmountField]
|
||||
*
|
||||
* @property clickIntents amount screen clicks
|
||||
* @property appCurrencyProvider selected app currency provider
|
||||
* @property cryptoCurrencyStatusProvider current cryptocurrency status provider
|
||||
*/
|
||||
class AmountFieldConverter(
|
||||
private val clickIntents: AmountScreenClickIntents,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Converter<String, AmountFieldModel> {
|
||||
|
||||
override fun convert(value: String): AmountFieldModel {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
|
||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
||||
val (fiatValue, fiatDecimal) = when {
|
||||
fiatRate.isNullOrZero() -> "" to null
|
||||
value.isEmpty() -> "" to BigDecimal.ZERO
|
||||
else -> {
|
||||
val fiatDecimal = fiatRate?.multiply(cryptoDecimal)
|
||||
val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty()
|
||||
fiatValue to fiatDecimal
|
||||
}
|
||||
}
|
||||
val isDoneActionEnabled = !cryptoDecimal.isNullOrZero()
|
||||
return AmountFieldModel(
|
||||
value = value,
|
||||
fiatValue = fiatValue,
|
||||
onValueChange = clickIntents::onAmountValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { clickIntents.onAmountNext() },
|
||||
),
|
||||
isFiatValue = false,
|
||||
cryptoAmount = cryptoAmount,
|
||||
fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()),
|
||||
isError = false,
|
||||
isWarning = false,
|
||||
error = TextReference.EMPTY,
|
||||
isFiatUnavailable = fiatRate == null,
|
||||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount(
|
||||
currencySymbol = appCurrency.symbol,
|
||||
value = fiatValue,
|
||||
decimals = FIAT_DECIMALS,
|
||||
type = AmountType.FiatType(appCurrency.code),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private const val FIAT_DECIMALS = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.common.ui.amountScreen.converters.field
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* Selects maximum amount value
|
||||
*
|
||||
* @property maxAmount maximum enter amount
|
||||
*/
|
||||
class AmountFieldSetMaxAmountTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val maxAmount: EnterAmountBoundary,
|
||||
private val minAmount: EnterAmountBoundary?,
|
||||
) : Transformer<AmountState> {
|
||||
|
||||
override fun transform(prevState: AmountState): AmountState {
|
||||
if (prevState !is AmountState.Data) return prevState
|
||||
|
||||
val amountTextField = prevState.amountTextField
|
||||
|
||||
val cryptoDecimals = amountTextField.cryptoAmount.decimals
|
||||
val fiatDecimals = amountTextField.fiatAmount.decimals
|
||||
val decimalCryptoValue = maxAmount.amount
|
||||
val decimalFiatValue = maxAmount.fiatAmount
|
||||
|
||||
if (decimalCryptoValue == null || decimalCryptoValue.isZero()) return prevState
|
||||
|
||||
val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals)
|
||||
val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty()
|
||||
val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } ?: false
|
||||
return prevState.copy(
|
||||
isPrimaryButtonEnabled = !isLessThanMinimumIfProvided,
|
||||
amountTextField = amountTextField.copy(
|
||||
isValuePasted = true,
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isLessThanMinimumIfProvided,
|
||||
error = when {
|
||||
isLessThanMinimumIfProvided -> {
|
||||
val minimumAmount = minAmount
|
||||
?.amount
|
||||
?.format { crypto(cryptoCurrencyStatus.currency) }
|
||||
.orEmpty()
|
||||
resourceReference(
|
||||
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
|
||||
wrappedList(minimumAmount, minimumAmount),
|
||||
)
|
||||
}
|
||||
else -> TextReference.EMPTY
|
||||
},
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = getKeyboardAction(isLessThanMinimumIfProvided, decimalCryptoValue),
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.common.ui.amountScreen.models
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
|
||||
/**
|
||||
* Model for amount field
|
||||
*
|
||||
* @param value entered value
|
||||
* @param onValueChange on value change
|
||||
* @param keyboardOptions keyboard options
|
||||
* @param keyboardActions keyboard actions
|
||||
* @param cryptoAmount value as amount
|
||||
* @param fiatAmount value in fiat as amount
|
||||
* @param isFiatValue indicates if app currency or crypto currency is selected
|
||||
* @param fiatValue value in fiat
|
||||
* @param isFiatUnavailable indicates if fiat rates are unavailable
|
||||
* @param isValuePasted indicated if value was pasted
|
||||
* @param onValuePastedTriggerDismiss on value pasted action
|
||||
* @param isError indicates is value invalid
|
||||
* @param error error text
|
||||
*/
|
||||
data class AmountFieldModel(
|
||||
val value: String,
|
||||
val onValueChange: (String) -> Unit,
|
||||
val keyboardOptions: KeyboardOptions,
|
||||
val keyboardActions: KeyboardActions,
|
||||
val cryptoAmount: Amount,
|
||||
val fiatAmount: Amount,
|
||||
val isFiatValue: Boolean,
|
||||
val fiatValue: String,
|
||||
val isFiatUnavailable: Boolean,
|
||||
val isValuePasted: Boolean,
|
||||
val onValuePastedTriggerDismiss: () -> Unit,
|
||||
val isError: Boolean,
|
||||
val isWarning: Boolean,
|
||||
val error: TextReference,
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.common.ui.amountScreen.models
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class AmountParameters(
|
||||
val title: TextReference,
|
||||
val value: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.common.ui.amountScreen.models
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* Segmented buttons config
|
||||
*
|
||||
* @param title button title
|
||||
* @param iconState currency icon state
|
||||
* @param iconUrl currency icon url
|
||||
* @param isFiat is fiat currency
|
||||
*/
|
||||
@Immutable
|
||||
data class AmountSegmentedButtonsConfig(
|
||||
val title: TextReference,
|
||||
val iconState: CurrencyIconState? = null,
|
||||
val iconUrl: String? = null,
|
||||
val isFiat: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.common.ui.amountScreen.models
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
/** Model for amount state */
|
||||
@Stable
|
||||
sealed class AmountState {
|
||||
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
/**
|
||||
* @param isPrimaryButtonEnabled indicates if next state button enabled
|
||||
* @param title title
|
||||
* @param availableBalance user crypto currency balance
|
||||
* @param tokenIconState crypto currency icon state
|
||||
* @param segmentedButtonConfig currency switcher config
|
||||
* @param selectedButton selected currency index
|
||||
* @param isSegmentedButtonsEnabled indicates if currency switches is enabled
|
||||
* @param amountTextField amount field state
|
||||
* @param appCurrencyCode app currency code
|
||||
*/
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val title: TextReference,
|
||||
val availableBalance: TextReference,
|
||||
val tokenIconState: CurrencyIconState,
|
||||
val segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,
|
||||
val selectedButton: Int,
|
||||
val isSegmentedButtonsEnabled: Boolean,
|
||||
val amountTextField: AmountFieldModel,
|
||||
val appCurrencyCode: String,
|
||||
) : AmountState()
|
||||
|
||||
data class Empty(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : AmountState()
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.common.ui.amountScreen.models
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class EnterAmountBoundary(
|
||||
val amount: BigDecimal? = null,
|
||||
val fiatAmount: BigDecimal? = null,
|
||||
val fiatRate: BigDecimal? = null,
|
||||
) {
|
||||
constructor(
|
||||
amount: BigDecimal? = null,
|
||||
fiatRate: BigDecimal? = null,
|
||||
) : this(
|
||||
amount = amount,
|
||||
fiatAmount = if (amount != null && fiatRate != null) {
|
||||
amount * fiatRate
|
||||
} else {
|
||||
null
|
||||
},
|
||||
fiatRate = fiatRate,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.common.ui.amountScreen.preview
|
||||
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
|
||||
object AmountScreenClickIntentsStub : AmountScreenClickIntents {
|
||||
|
||||
override fun onAmountValueChange(value: String) {}
|
||||
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {}
|
||||
|
||||
override fun onMaxValueClick() {}
|
||||
|
||||
override fun onAmountPasteTriggerDismiss() {}
|
||||
|
||||
override fun onAmountNext() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.common.ui.amountScreen.preview
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
object AmountStatePreviewData {
|
||||
|
||||
val amountState = AmountState.Data(
|
||||
isPrimaryButtonEnabled = false,
|
||||
title = stringReference("Family Wallet"),
|
||||
availableBalance = stringReference("2 130,88 USDT (2 129,92 \$)"),
|
||||
tokenIconState = CurrencyIconState.Loading,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
AmountSegmentedButtonsConfig(
|
||||
title = stringReference("USDT"),
|
||||
iconState = CurrencyIconState.Locked,
|
||||
isFiat = false,
|
||||
),
|
||||
AmountSegmentedButtonsConfig(
|
||||
title = stringReference("USD"),
|
||||
isFiat = true,
|
||||
),
|
||||
),
|
||||
appCurrencyCode = "usd",
|
||||
amountTextField = AmountFieldModel(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions = KeyboardActions.Default,
|
||||
cryptoAmount = Amount(
|
||||
currencySymbol = "USDT",
|
||||
value = BigDecimal.ZERO,
|
||||
decimals = 18,
|
||||
type = AmountType.CoinType,
|
||||
),
|
||||
fiatAmount = Amount(
|
||||
currencySymbol = "$",
|
||||
value = BigDecimal.ZERO,
|
||||
decimals = 2,
|
||||
type = AmountType.CoinType,
|
||||
),
|
||||
isFiatValue = false,
|
||||
fiatValue = "123.123",
|
||||
isFiatUnavailable = false,
|
||||
isError = false,
|
||||
isWarning = false,
|
||||
error = TextReference.EMPTY,
|
||||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
),
|
||||
isSegmentedButtonsEnabled = true,
|
||||
selectedButton = 0,
|
||||
)
|
||||
|
||||
val amountWithValueState = amountState.copy(
|
||||
amountTextField = amountState.amountTextField.copy(
|
||||
value = "100.00",
|
||||
cryptoAmount = amountState.amountTextField.cryptoAmount.copy(
|
||||
value = BigDecimal("100.00"),
|
||||
),
|
||||
fiatValue = "99.98",
|
||||
fiatAmount = amountState.amountTextField.fiatAmount.copy(
|
||||
value = BigDecimal("99.98"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val amountWithValueFiatState = amountWithValueState.copy(
|
||||
amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false),
|
||||
)
|
||||
|
||||
val amountErrorState = amountWithValueState.copy(
|
||||
amountTextField = amountWithValueState.amountTextField.copy(
|
||||
isError = true,
|
||||
error = stringReference("Insufficient funds for transfer"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.tangem.common.ui.amountScreen.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
||||
import com.tangem.core.ui.components.ResizableText
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.format.bigdecimal.anyDecimals
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Composable
|
||||
fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit) {
|
||||
if (amountState !is AmountState.Data) return
|
||||
val amount = amountState.amountTextField
|
||||
|
||||
val cryptoAmount = formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol)
|
||||
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = amount.fiatAmount.value,
|
||||
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
|
||||
fiatCurrencyCode = amountState.appCurrencyCode,
|
||||
)
|
||||
val backgroundColor = if (isEditingDisabled) {
|
||||
TangemTheme.colors.button.disabled
|
||||
} else {
|
||||
TangemTheme.colors.background.action
|
||||
}
|
||||
|
||||
val (firstAmount, secondAmount) = if (amount.isFiatValue) {
|
||||
fiatAmount to cryptoAmount
|
||||
} else {
|
||||
cryptoAmount to fiatAmount
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
CurrencyIcon(state = amountState.tokenIconState)
|
||||
ResizableText(
|
||||
text = firstAmount,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens.spacing24),
|
||||
)
|
||||
Text(
|
||||
text = secondAmount,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatWithSymbol(amount: String, symbol: String) =
|
||||
BigDecimal.ZERO.format { crypto(symbol, 0).anyDecimals() }.replace("0", amount)
|
||||
|
||||
// region Preview
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::class) value: AmountState) {
|
||||
TangemThemePreview {
|
||||
AmountBlock(
|
||||
amountState = value,
|
||||
isClickDisabled = false,
|
||||
isEditingDisabled = false,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class AmountBlockPreviewProvider : PreviewParameterProvider<AmountState> {
|
||||
override val values: Sequence<AmountState>
|
||||
get() = sequenceOf(
|
||||
AmountStatePreviewData.amountState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package com.tangem.common.ui.amountScreen.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey"
|
||||
|
||||
internal fun LazyListScope.buttons(
|
||||
segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,
|
||||
clickIntents: AmountScreenClickIntents,
|
||||
isSegmentedButtonsEnabled: Boolean,
|
||||
selectedButton: Int,
|
||||
) {
|
||||
item(
|
||||
key = AMOUNT_BUTTONS_KEY,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Row(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
if (segmentedButtonConfig.isNotEmpty()) {
|
||||
SegmentedButtons(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(TangemTheme.dimens.size40),
|
||||
config = segmentedButtonConfig,
|
||||
showIndication = false,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
clickIntents.onCurrencyChangeClick(it.isFiat)
|
||||
},
|
||||
initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton),
|
||||
isEnabled = isSegmentedButtonsEnabled,
|
||||
) {
|
||||
AmountCurrencyButton(
|
||||
button = it,
|
||||
isSegmentedButtonsEnabled = isSegmentedButtonsEnabled,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
SpacerWMax()
|
||||
}
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.send_max_amount),
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.height(TangemTheme.dimens.size40)
|
||||
.clip(shape = RoundedCornerShape(TangemTheme.dimens.radius26))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
clickIntents.onMaxValueClick()
|
||||
}
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing10,
|
||||
horizontal = TangemTheme.dimens.spacing34,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing10,
|
||||
),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val iconModifier = Modifier
|
||||
.size(TangemTheme.dimens.size18)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing1)
|
||||
if (button.isFiat) {
|
||||
FiatIcon(
|
||||
url = button.iconUrl,
|
||||
size = TangemTheme.dimens.size18,
|
||||
isGrayscale = !isSegmentedButtonsEnabled,
|
||||
modifier = iconModifier,
|
||||
)
|
||||
} else if (button.iconState != null) {
|
||||
CurrencyIcon(
|
||||
state = button.iconState,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = iconModifier,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = button.title.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.button,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package com.tangem.common.ui.amountScreen.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment.Companion.BottomCenter
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val isFiatValue = amountField.isFiatValue
|
||||
val currencyCode = if (isFiatValue) appCurrencyCode else null
|
||||
val (primaryAmount, primaryValue) = if (isFiatValue) {
|
||||
amountField.fiatAmount to amountField.fiatValue
|
||||
} else {
|
||||
amountField.cryptoAmount to amountField.value
|
||||
}
|
||||
val requester = remember { FocusRequester() }
|
||||
|
||||
AmountTextField(
|
||||
value = primaryValue,
|
||||
decimals = primaryAmount.decimals,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = primaryAmount.decimals,
|
||||
symbol = primaryAmount.currencySymbol,
|
||||
currencyCode = currencyCode,
|
||||
decimalFormat = decimalFormat,
|
||||
),
|
||||
onValueChange = amountField.onValueChange,
|
||||
keyboardOptions = amountField.keyboardOptions,
|
||||
keyboardActions = amountField.keyboardActions,
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
isAutoResize = true,
|
||||
isValuePasted = amountField.isValuePasted,
|
||||
onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss,
|
||||
modifier = Modifier
|
||||
.focusRequester(requester)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.requiredHeightIn(min = TangemTheme.dimens.size32),
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
delay(timeMillis = 200)
|
||||
requester.requestFocus()
|
||||
}
|
||||
|
||||
AmountSecondary(amountField, appCurrencyCode)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) {
|
||||
val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
val text = if (amountField.isFiatValue) {
|
||||
secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) }
|
||||
} else {
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = secondaryAmount.value,
|
||||
fiatCurrencySymbol = secondaryAmount.currencySymbol,
|
||||
fiatCurrencyCode = appCurrencyCode,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(TopCenter)
|
||||
.padding(bottom = TangemTheme.dimens.spacing32),
|
||||
)
|
||||
AmountFieldError(
|
||||
isError = amountField.isError,
|
||||
isWarning = amountField.isWarning,
|
||||
error = amountField.error,
|
||||
modifier = Modifier
|
||||
.align(BottomCenter)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing20,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountFieldError(
|
||||
isError: Boolean,
|
||||
isWarning: Boolean,
|
||||
error: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isError || isWarning,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = modifier,
|
||||
) {
|
||||
val errorText = remember(this, error) { error }
|
||||
val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention
|
||||
Text(
|
||||
text = errorText.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = color,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.common.ui.amountScreen.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
private const val AMOUNT_FIELD_KEY = "amountFieldKey"
|
||||
|
||||
internal fun LazyListScope.amountField(
|
||||
amountState: AmountState.Data,
|
||||
isBalanceHidden: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
item(key = AMOUNT_FIELD_KEY) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
Text(
|
||||
text = amountState.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing14),
|
||||
)
|
||||
|
||||
val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference()
|
||||
AnimatedContent(
|
||||
targetState = balance,
|
||||
label = "Hide Balance Animation",
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
CurrencyIcon(
|
||||
state = amountState.tokenIconState,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing32),
|
||||
)
|
||||
AmountField(
|
||||
amountField = amountState.amountTextField,
|
||||
appCurrencyCode = amountState.appCurrencyCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.common.ui.amountScreen.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.SecondaryButtonIconStart
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
fun SendDoneButtons(
|
||||
txUrl: String,
|
||||
onExploreClick: () -> Unit,
|
||||
onShareClick: (String) -> Unit,
|
||||
isVisible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisible && txUrl.isNotBlank(),
|
||||
modifier = modifier,
|
||||
enter = slideInVertically().plus(fadeIn()),
|
||||
exit = slideOutVertically().plus(fadeOut()),
|
||||
label = "Animate show sent state buttons",
|
||||
) {
|
||||
Row(modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12)) {
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResourceSafe(id = R.string.common_explore),
|
||||
iconResId = R.drawable.ic_web_24,
|
||||
onClick = onExploreClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
SpacerW12()
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResourceSafe(id = R.string.common_share),
|
||||
iconResId = R.drawable.ic_share_24,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onShareClick(txUrl)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 328)
|
||||
@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SendDoneButtons_Preview() {
|
||||
TangemThemePreview {
|
||||
SendDoneButtons(
|
||||
txUrl = "txUrl",
|
||||
onShareClick = {},
|
||||
onExploreClick = {},
|
||||
isVisible = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.common.ui.amountScreen.utils
|
||||
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal fun String.getCryptoValue(fiatRate: BigDecimal?, isFiatValue: Boolean, decimals: Int): String {
|
||||
return if (isFiatValue && fiatRate != null) {
|
||||
parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN)
|
||||
.parseBigDecimal(decimals)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
internal fun String.getFiatValue(
|
||||
fiatRate: BigDecimal?,
|
||||
isFiatValue: Boolean,
|
||||
decimals: Int,
|
||||
): Pair<String, BigDecimal?> {
|
||||
return if (fiatRate != null) {
|
||||
val fiatValue = if (!isFiatValue) {
|
||||
parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
val decimalFiatValue = fiatValue.parseToBigDecimal(decimals)
|
||||
fiatValue to decimalFiatValue
|
||||
} else {
|
||||
"" to null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun String.checkExceedBalance(
|
||||
maxEnterAmount: EnterAmountBoundary,
|
||||
amountTextField: AmountFieldModel,
|
||||
): Boolean {
|
||||
val currencyCryptoAmount = maxEnterAmount.amount ?: BigDecimal.ZERO
|
||||
val currencyFiatAmount = maxEnterAmount.fiatAmount ?: BigDecimal.ZERO
|
||||
val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals)
|
||||
val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals)
|
||||
return if (amountTextField.isFiatValue) {
|
||||
fiatDecimal > currencyFiatAmount
|
||||
} else {
|
||||
cryptoDecimal > currencyCryptoAmount
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getKeyboardAction(isCheckFailed: Boolean, decimalCryptoValue: BigDecimal) =
|
||||
if (!isCheckFailed && !decimalCryptoValue.isZero()) {
|
||||
ImeAction.Done
|
||||
} else {
|
||||
ImeAction.None
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.common.ui.amountScreen.utils
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import java.math.BigDecimal
|
||||
|
||||
fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? {
|
||||
if (value == null || rate == null) return null
|
||||
val formattedFiat = getFiatString(value = value, rate = rate, appCurrency = appCurrency)
|
||||
return stringReference(formattedFiat)
|
||||
}
|
||||
|
||||
fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String {
|
||||
if (value == null || rate == null) return EMPTY_BALANCE_SIGN
|
||||
val feeValue = value.multiply(rate)
|
||||
return BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = feeValue,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
package com.tangem.common.ui.bottomsheet.permission
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.*
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
import com.tangem.core.ui.components.inputrow.InputRowDefault
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
|
||||
var isPermissionAlertShow by remember { mutableStateOf(false) }
|
||||
|
||||
TangemBottomSheet(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
titleText = resourceReference(R.string.give_permission_title),
|
||||
titleAction = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_information_24,
|
||||
onIconClicked = { isPermissionAlertShow = true },
|
||||
),
|
||||
content = { content: GiveTxPermissionBottomSheetConfig ->
|
||||
GiveTxPermissionBottomSheetContent(content = content)
|
||||
|
||||
if (isPermissionAlertShow) {
|
||||
BasicDialog(
|
||||
message = content.data.dialogText.resolveReference(),
|
||||
title = stringResourceSafe(id = R.string.common_approve),
|
||||
confirmButton = DialogButtonUM { isPermissionAlertShow = false },
|
||||
onDismissDialog = {},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSheetConfig) {
|
||||
val data = content.data
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = content.data.subtitle.resolveReference(),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24),
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
|
||||
ApprovalBottomSheetInfo(data)
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing20)
|
||||
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResourceSafe(id = R.string.common_approve),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
showProgress = data.approveButton.loading,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
onClick = data.approveButton.onClick,
|
||||
enabled = data.approveButton.enabled,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
SecondaryButton(
|
||||
text = stringResourceSafe(id = R.string.common_cancel),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
onClick = content.onCancel,
|
||||
enabled = data.cancelButton.enabled,
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) {
|
||||
FooterContainer(
|
||||
footer = resourceReference(R.string.give_permission_policy_type_footer),
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
AmountItem(
|
||||
currency = data.currency,
|
||||
approveType = data.approveType,
|
||||
onChangeApproveType = data.onChangeApproveType,
|
||||
approveItems = data.approveItems,
|
||||
)
|
||||
}
|
||||
SpacerH16()
|
||||
FooterContainer(
|
||||
footer = data.footerText,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
FeeItem(fee = data.fee)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeItem(fee: TextReference) {
|
||||
InputRowDefault(
|
||||
title = resourceReference(R.string.common_network_fee_title),
|
||||
text = fee,
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountItem(
|
||||
currency: String,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: ((ApproveType) -> Unit)?,
|
||||
) {
|
||||
var isExpandSelector by remember { mutableStateOf(false) }
|
||||
var amountSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
enabled = onChangeApproveType != null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = { isExpandSelector = true },
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onSizeChanged { amountSize = it }
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.give_permission_rows_amount, currency),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerWMax()
|
||||
Text(
|
||||
text = approveType.text.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
)
|
||||
if (onChangeApproveType != null) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (onChangeApproveType != null) {
|
||||
DropdownSelector(
|
||||
isExpanded = isExpandSelector,
|
||||
onDismiss = { isExpandSelector = false },
|
||||
onItemClick = { approveType ->
|
||||
onChangeApproveType.let {
|
||||
isExpandSelector = false
|
||||
onChangeApproveType.invoke(approveType)
|
||||
}
|
||||
},
|
||||
items = approveItems,
|
||||
selectedType = approveType,
|
||||
amountSize = amountSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun DropdownSelector(
|
||||
isExpanded: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onItemClick: (ApproveType) -> Unit,
|
||||
items: ImmutableList<ApproveType>,
|
||||
selectedType: ApproveType,
|
||||
amountSize: IntSize,
|
||||
) {
|
||||
var dropDownWidth by remember { mutableStateOf(IntSize.Zero) }
|
||||
val offsetY = amountSize.height.times(-1)
|
||||
val offsetX = amountSize.width - dropDownWidth.width
|
||||
|
||||
// Workaround to set color and shape of dropdown menu
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(surface = TangemTheme.colors.background.action),
|
||||
shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(TangemTheme.dimens.radius16)),
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss,
|
||||
properties = PopupProperties(clippingEnabled = false),
|
||||
offset = with(LocalDensity.current) {
|
||||
DpOffset(x = offsetX.toDp(), y = offsetY.toDp())
|
||||
},
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.onSizeChanged { dropDownWidth = it },
|
||||
) {
|
||||
items.forEach { item ->
|
||||
val color = if (item == selectedType) TangemTheme.colors.icon.accent else Color.Transparent
|
||||
|
||||
DropdownMenuItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = {
|
||||
Row {
|
||||
Text(
|
||||
text = when (item) {
|
||||
ApproveType.LIMITED -> stringResourceSafe(
|
||||
id = R.string.give_permission_current_transaction,
|
||||
)
|
||||
ApproveType.UNLIMITED -> stringResourceSafe(id = R.string.give_permission_unlimited)
|
||||
},
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerWMax()
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(id = R.drawable.ic_check_24),
|
||||
),
|
||||
tint = color,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.size20),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onItemClick.invoke(item)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, locale = "ru")
|
||||
private fun Preview_GiveTxPermissionBottomSheet() {
|
||||
TangemThemePreview {
|
||||
GiveTxPermissionBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = previewData,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val previewData = GiveTxPermissionBottomSheetConfig(
|
||||
data = GiveTxPermissionState.ReadyForRequest(
|
||||
currency = "DAI",
|
||||
amount = "1",
|
||||
walletAddress = "",
|
||||
spenderAddress = "",
|
||||
fee = TextReference.Str("0.1233 BTC (2,14$)"),
|
||||
approveType = ApproveType.LIMITED,
|
||||
approveButton = ApprovePermissionButton(true) {},
|
||||
cancelButton = CancelPermissionButton(true),
|
||||
onChangeApproveType = { ApproveType.LIMITED },
|
||||
subtitle = resourceReference(R.string.give_permission_staking_subtitle, wrappedList("1")),
|
||||
dialogText = resourceReference(R.string.give_permission_staking_footer),
|
||||
footerText = resourceReference(R.string.swap_give_permission_fee_footer),
|
||||
),
|
||||
onCancel = {},
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.common.ui.bottomsheet.permission.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
data class GiveTxPermissionBottomSheetConfig(
|
||||
val data: GiveTxPermissionState.ReadyForRequest,
|
||||
val onCancel: () -> Unit,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.common.ui.bottomsheet.permission.state
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
sealed class GiveTxPermissionState {
|
||||
|
||||
data object InProgress : GiveTxPermissionState()
|
||||
|
||||
data object Empty : GiveTxPermissionState()
|
||||
|
||||
data class ReadyForRequest(
|
||||
val subtitle: TextReference,
|
||||
val dialogText: TextReference,
|
||||
val footerText: TextReference,
|
||||
val currency: String,
|
||||
val amount: String,
|
||||
val walletAddress: String,
|
||||
val spenderAddress: String,
|
||||
val fee: TextReference,
|
||||
val approveType: ApproveType,
|
||||
val approveItems: ImmutableList<ApproveType> = ApproveType.entries.toImmutableList(),
|
||||
val approveButton: ApprovePermissionButton,
|
||||
val cancelButton: CancelPermissionButton,
|
||||
val onChangeApproveType: ((ApproveType) -> Unit)? = null,
|
||||
) : GiveTxPermissionState()
|
||||
|
||||
fun GiveTxPermissionState.getApproveTypeOrNull(): ApproveType? {
|
||||
return (this as? ReadyForRequest)?.approveType
|
||||
}
|
||||
}
|
||||
|
||||
enum class ApproveType(val text: TextReference) {
|
||||
LIMITED(resourceReference(R.string.give_permission_current_transaction)),
|
||||
UNLIMITED(resourceReference(R.string.give_permission_unlimited)),
|
||||
}
|
||||
|
||||
data class ApprovePermissionButton(
|
||||
val enabled: Boolean,
|
||||
val loading: Boolean = false,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
data class CancelPermissionButton(
|
||||
val enabled: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.inputrow.InputRowApprox
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun ExpressEstimate(
|
||||
timestamp: TextReference,
|
||||
fromTokenIconState: CurrencyIconState,
|
||||
toTokenIconState: CurrencyIconState,
|
||||
fromCryptoAmount: TextReference,
|
||||
fromCryptoSymbol: String,
|
||||
toCryptoAmount: TextReference,
|
||||
toCryptoSymbol: String,
|
||||
fromFiatAmount: TextReference?,
|
||||
toFiatAmount: TextReference?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing2,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.express_estimated_amount),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
Text(
|
||||
text = timestamp.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
InputRowApprox(
|
||||
leftIcon = fromTokenIconState,
|
||||
leftTitle = fromCryptoAmount,
|
||||
leftSubtitle = fromFiatAmount,
|
||||
leftTitleEllipsisOffset = fromCryptoSymbol.length,
|
||||
rightIcon = toTokenIconState,
|
||||
rightTitle = toCryptoAmount,
|
||||
rightSubtitle = toFiatAmount,
|
||||
rightTitleEllipsisOffset = toCryptoSymbol.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.ExpressHideButton(isTerminal: Boolean, isAutoDisposable: Boolean, onClick: () -> Unit) {
|
||||
AnimatedVisibility(
|
||||
visible = isTerminal && !isAutoDisposable,
|
||||
label = "Hide button visibility animation",
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.express_status_hide_button_text),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.button,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, end = 12.dp, top = 14.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(bounded = false),
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(vertical = 10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.inputrow.InputRowBestRate
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
fun ExpressProvider(
|
||||
providerName: TextReference,
|
||||
providerType: TextReference,
|
||||
providerTxId: String?,
|
||||
imageUrl: String,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.express_provider),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SpacerWMax()
|
||||
if (!providerTxId.isNullOrEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.clickable {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
clipboardManager.setText(AnnotatedString(providerTxId))
|
||||
Toast
|
||||
.makeText(context, R.string.express_transaction_id_copied, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.padding(end = TangemTheme.dimens.spacing4)
|
||||
.align(Alignment.CenterVertically),
|
||||
painter = painterResource(id = R.drawable.ic_copy_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = stringResourceSafe(R.string.express_transaction_id, providerTxId),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
InputRowBestRate(
|
||||
imageUrl = imageUrl,
|
||||
title = providerName,
|
||||
titleExtra = providerType,
|
||||
subtitle = TextReference.Res(R.string.express_floating_rate),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ExpressProvider_Preview() {
|
||||
TangemThemePreview {
|
||||
ExpressProvider(
|
||||
providerName = TextReference.Str("Changelly"),
|
||||
providerType = TextReference.Str("CEX"),
|
||||
providerTxId = "hjsbajcqbhjsbajcqbhjsbajcqbhjsbajcqbhjsbajcqb",
|
||||
imageUrl = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Block with express statuses
|
||||
*
|
||||
* @param state ui holder
|
||||
* @param modifier modifier
|
||||
* @see [Figma](https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=18459-26521&t=4jox7bfqUiXnm2h1-4)
|
||||
*/
|
||||
@Composable
|
||||
fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SpacerWMax()
|
||||
AnimatedVisibility(visible = state.link is ExpressLinkUM.Content) {
|
||||
val link = remember(this) { state.link as ExpressLinkUM.Content }
|
||||
Row(
|
||||
modifier = Modifier.clickable { link.onClick() },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = link.icon),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.spacing16)
|
||||
.padding(end = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
Text(
|
||||
text = link.text.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
state.statuses.forEachIndexed { index, item ->
|
||||
ExpressStatusStep(item, index == state.statuses.lastIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpressStatusStep(status: ExpressStatusItemUM, isLast: Boolean) {
|
||||
AnimatedContent(
|
||||
targetState = status,
|
||||
label = "Exchange Step Change Success",
|
||||
transitionSpec = {
|
||||
fadeIn(tween(durationMillis = 220)) togetherWith
|
||||
fadeOut(tween(durationMillis = 220))
|
||||
},
|
||||
) { content ->
|
||||
Row {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
when (content.state) {
|
||||
ExpressStatusItemState.Active -> StepInProgress()
|
||||
ExpressStatusItemState.Default -> StepDefault()
|
||||
ExpressStatusItemState.Done -> Step(
|
||||
iconRes = R.drawable.ic_check_24,
|
||||
iconColor = TangemTheme.colors.icon.primary1,
|
||||
borderColor = TangemTheme.colors.field.focused,
|
||||
)
|
||||
ExpressStatusItemState.Error -> Step(
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
iconColor = TangemTheme.colors.icon.warning,
|
||||
)
|
||||
ExpressStatusItemState.Warning -> Step(
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
iconColor = TangemTheme.colors.icon.attention,
|
||||
)
|
||||
}
|
||||
if (!isLast) {
|
||||
StepSeparator()
|
||||
}
|
||||
}
|
||||
val textColor = when (status.state) {
|
||||
ExpressStatusItemState.Active -> TangemTheme.colors.text.primary1
|
||||
ExpressStatusItemState.Default -> TangemTheme.colors.text.disabled
|
||||
ExpressStatusItemState.Done -> TangemTheme.colors.text.primary1
|
||||
ExpressStatusItemState.Error -> TangemTheme.colors.text.warning
|
||||
ExpressStatusItemState.Warning -> TangemTheme.colors.text.attention
|
||||
}
|
||||
Text(
|
||||
text = content.text.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = textColor,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StepDefault() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.border(
|
||||
width = TangemTheme.dimens.size1_5,
|
||||
color = TangemTheme.colors.field.focused,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.padding(TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step(iconColor: Color, @DrawableRes iconRes: Int, borderColor: Color = iconColor) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.border(
|
||||
width = TangemTheme.dimens.size1_5,
|
||||
color = borderColor,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.padding(TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StepInProgress() {
|
||||
CircularProgressIndicator(
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing2)
|
||||
.size(TangemTheme.dimens.size14),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StepSeparator() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing2)
|
||||
.size(
|
||||
width = TangemTheme.dimens.size1_5,
|
||||
height = TangemTheme.dimens.size10,
|
||||
)
|
||||
.background(
|
||||
color = TangemTheme.colors.field.focused,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_ExchangeStatusBlock() {
|
||||
val state = ExpressStatusUM(
|
||||
title = resourceReference(R.string.express_exchange_status_title),
|
||||
link = ExpressLinkUM.Content(
|
||||
icon = R.drawable.ic_alert_24,
|
||||
text = resourceReference(R.string.common_go_to_provider),
|
||||
onClick = {},
|
||||
),
|
||||
statuses = persistentListOf(
|
||||
ExpressStatusItemUM(text = stringReference("Done"), state = ExpressStatusItemState.Done),
|
||||
ExpressStatusItemUM(text = stringReference("Active"), state = ExpressStatusItemState.Active),
|
||||
ExpressStatusItemUM(text = stringReference("Warning"), state = ExpressStatusItemState.Warning),
|
||||
ExpressStatusItemUM(text = stringReference("Error"), state = ExpressStatusItemState.Error),
|
||||
ExpressStatusItemUM(text = stringReference("Default"), state = ExpressStatusItemState.Default),
|
||||
),
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
ExpressStatusBlock(state = state)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
data class ExpressStatusBottomSheetConfig(
|
||||
val value: ExpressTransactionStateUM,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
||||
@Composable
|
||||
fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
) { content: ExpressStatusBottomSheetConfig ->
|
||||
when (val state = content.value) {
|
||||
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.constraintlayout.compose.*
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod", "LongParameterList")
|
||||
@Composable
|
||||
internal fun ExpressStatusItem(
|
||||
title: TextReference,
|
||||
fromTokenIconState: CurrencyIconState,
|
||||
toTokenIconState: CurrencyIconState,
|
||||
fromAmount: TextReference,
|
||||
fromSymbol: String,
|
||||
toSymbol: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
toAmount: TextReference = TextReference.EMPTY,
|
||||
@DrawableRes infoIconRes: Int? = null,
|
||||
infoIconTint: Color? = null,
|
||||
) {
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.clickable { onClick() }
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
val (titleRef, iconRef, infoIconRef, swapIconRef, fromRef, toRef, fromIconRef, toIconRef) = createRefs()
|
||||
val padding6 = TangemTheme.dimens.spacing6
|
||||
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.constrainAs(titleRef) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(parent.top)
|
||||
},
|
||||
)
|
||||
CurrencyIcon(
|
||||
state = fromTokenIconState,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.constrainAs(fromIconRef) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(titleRef.bottom, padding6)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
)
|
||||
EllipsisText(
|
||||
text = fromAmount.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
ellipsis = TextEllipsis.OffsetEnd(fromSymbol.length),
|
||||
modifier = Modifier.constrainAs(fromRef) {
|
||||
start.linkTo(fromIconRef.end, padding6)
|
||||
top.linkTo(titleRef.bottom, padding6)
|
||||
end.linkTo(swapIconRef.start)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints.atMostWrapContent
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_forward_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size12)
|
||||
.constrainAs(swapIconRef) {
|
||||
start.linkTo(fromRef.end, padding6)
|
||||
top.linkTo(titleRef.bottom, padding6)
|
||||
end.linkTo(toIconRef.start)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
)
|
||||
CurrencyIcon(
|
||||
state = toTokenIconState,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.constrainAs(toIconRef) {
|
||||
start.linkTo(swapIconRef.end, padding6)
|
||||
top.linkTo(titleRef.bottom, padding6)
|
||||
end.linkTo(toRef.start)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
)
|
||||
EllipsisText(
|
||||
text = toAmount.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
ellipsis = TextEllipsis.OffsetEnd(toSymbol.length),
|
||||
modifier = Modifier.constrainAs(toRef) {
|
||||
start.linkTo(toIconRef.end, padding6)
|
||||
top.linkTo(titleRef.bottom, padding6)
|
||||
end.linkTo(infoIconRef.start, padding6, padding6)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints.atLeastWrapContent
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = infoIconRes ?: R.drawable.ic_alert_triangle_20),
|
||||
contentDescription = null,
|
||||
tint = infoIconTint ?: TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.constrainAs(infoIconRef) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
end.linkTo(iconRef.start)
|
||||
visibility = if (infoIconRes == null) {
|
||||
Visibility.Gone
|
||||
} else {
|
||||
Visibility.Visible
|
||||
}
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size24)
|
||||
.constrainAs(iconRef) {
|
||||
end.linkTo(parent.end)
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region Preview
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ExpressStatusItemPreview(
|
||||
@PreviewParameter(ExpressStatusItemPreviewParameterProvider::class) amount: String,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
ExpressStatusItem(
|
||||
title = stringReference("ChangeNow"),
|
||||
fromTokenIconState = CurrencyIconState.Loading,
|
||||
toTokenIconState = CurrencyIconState.Loading,
|
||||
fromAmount = stringReference(amount),
|
||||
fromSymbol = "USDT",
|
||||
toAmount = stringReference(amount),
|
||||
toSymbol = "USDT",
|
||||
onClick = {},
|
||||
infoIconRes = null,
|
||||
infoIconTint = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ExpressStatusItemPreviewParameterProvider : PreviewParameterProvider<String> {
|
||||
override val values: Sequence<String>
|
||||
get() = sequenceOf(
|
||||
"1111111111111111111111111111 USDT",
|
||||
"11111 USDT",
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
fun LazyListScope.expressTransactionsItems(
|
||||
expressTxs: PersistentList<ExpressTransactionStateUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
items(
|
||||
count = expressTxs.size,
|
||||
key = { expressTxs[it].info.txId },
|
||||
contentType = { expressTxs[it]::class.java },
|
||||
) {
|
||||
val itemInfo = expressTxs[it].info
|
||||
val (iconRes, tint) = when (itemInfo.iconState) {
|
||||
ExpressTransactionStateIconUM.Warning -> {
|
||||
R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
|
||||
}
|
||||
ExpressTransactionStateIconUM.Error -> {
|
||||
R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
|
||||
}
|
||||
ExpressTransactionStateIconUM.None -> null to null
|
||||
}
|
||||
|
||||
ExpressStatusItem(
|
||||
title = itemInfo.title,
|
||||
fromTokenIconState = itemInfo.fromCurrencyIcon,
|
||||
toTokenIconState = itemInfo.toCurrencyIcon,
|
||||
fromAmount = itemInfo.fromAmount,
|
||||
fromSymbol = itemInfo.fromAmountSymbol,
|
||||
toAmount = itemInfo.toAmount,
|
||||
toSymbol = itemInfo.toAmountSymbol,
|
||||
onClick = itemInfo.onClick,
|
||||
infoIconRes = iconRes,
|
||||
infoIconTint = tint,
|
||||
modifier = modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.notifications.ExpressNotificationsUM
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun ExpressStatusNotificationBlock(state: NotificationUM?) {
|
||||
AnimatedVisibility(
|
||||
visible = state?.config != null,
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
label = "Express Status Notification Change",
|
||||
) {
|
||||
val wrappedNotification = remember(this) { requireNotNull(state?.config) }
|
||||
Notification(
|
||||
config = wrappedNotification,
|
||||
iconTint = when (state) {
|
||||
is ExpressNotificationsUM.NeedVerification -> TangemTheme.colors.icon.attention
|
||||
is ExpressNotificationsUM.FailedByProvider -> TangemTheme.colors.icon.warning
|
||||
else -> null
|
||||
},
|
||||
containerColor = TangemTheme.colors.background.action,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.common.ui.expressStatus
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH10
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH24
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
SpacerH10()
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.common_transaction_status),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.align(CenterHorizontally),
|
||||
)
|
||||
SpacerH10()
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.express_exchange_status_subtitle),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally),
|
||||
)
|
||||
SpacerH16()
|
||||
ExpressEstimate(
|
||||
timestamp = state.info.timestampFormatted,
|
||||
fromTokenIconState = state.info.fromCurrencyIcon,
|
||||
toTokenIconState = state.info.toCurrencyIcon,
|
||||
fromCryptoAmount = state.info.fromAmount,
|
||||
fromCryptoSymbol = state.info.fromAmountSymbol,
|
||||
toCryptoAmount = state.info.toAmount,
|
||||
toCryptoSymbol = state.info.toAmountSymbol,
|
||||
fromFiatAmount = state.info.fromFiatAmount,
|
||||
toFiatAmount = state.info.toFiatAmount,
|
||||
)
|
||||
SpacerH12()
|
||||
|
||||
ExpressProvider(
|
||||
providerName = stringReference(state.providerName),
|
||||
providerType = stringReference(state.providerType),
|
||||
providerTxId = state.info.txExternalId,
|
||||
imageUrl = state.providerImageUrl,
|
||||
)
|
||||
SpacerH12()
|
||||
ExpressStatusBlock(state = state.info.status)
|
||||
ExpressStatusNotificationBlock(state = state.info.notification)
|
||||
ExpressHideButton(
|
||||
isTerminal = state.activeStatus.isTerminal,
|
||||
isAutoDisposable = state.activeStatus.isAutoDisposable,
|
||||
onClick = state.info.onDisposeExpressStatus,
|
||||
)
|
||||
SpacerH24()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.common.ui.expressStatus.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* UI data holder for express status block
|
||||
*
|
||||
* @property title block title
|
||||
* @property link provider web link
|
||||
* @property statuses list of possible and active statuses
|
||||
*/
|
||||
data class ExpressStatusUM(
|
||||
val title: TextReference,
|
||||
val link: ExpressLinkUM,
|
||||
val statuses: ImmutableList<ExpressStatusItemUM>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Provider web link for express status block.
|
||||
* [Empty] if no link needed
|
||||
* [Content] if link is provided and displayed
|
||||
*/
|
||||
@Stable
|
||||
sealed class ExpressLinkUM {
|
||||
data object Empty : ExpressLinkUM()
|
||||
data class Content(
|
||||
@DrawableRes val icon: Int,
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
) : ExpressLinkUM()
|
||||
}
|
||||
|
||||
/**
|
||||
* Single status item in express status block
|
||||
*/
|
||||
data class ExpressStatusItemUM(
|
||||
val text: TextReference,
|
||||
val state: ExpressStatusItemState,
|
||||
)
|
||||
|
||||
/**
|
||||
* Available status states for express status block
|
||||
*/
|
||||
enum class ExpressStatusItemState {
|
||||
Active,
|
||||
Default,
|
||||
Done,
|
||||
Warning,
|
||||
Error,
|
||||
;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.common.ui.expressStatus.state
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.onramp.model.OnrampStatus
|
||||
|
||||
interface ExpressTransactionStateUM {
|
||||
|
||||
val info: ExpressTransactionStateInfoUM
|
||||
|
||||
data class OnrampUM(
|
||||
override val info: ExpressTransactionStateInfoUM,
|
||||
val providerName: String, // todo onramp fix after SwapProvider moved to own module
|
||||
val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module
|
||||
val providerType: String, // todo onramp fix after SwapProvider moved to own module
|
||||
val activeStatus: OnrampStatus.Status,
|
||||
val fromCurrencyCode: String,
|
||||
) : ExpressTransactionStateUM
|
||||
}
|
||||
|
||||
data class ExpressTransactionStateInfoUM(
|
||||
val title: TextReference,
|
||||
val status: ExpressStatusUM,
|
||||
val notification: NotificationUM?,
|
||||
val txId: String,
|
||||
val txExternalId: String?,
|
||||
val txExternalUrl: String?,
|
||||
val timestamp: Long,
|
||||
val timestampFormatted: TextReference,
|
||||
val onGoToProviderClick: (String) -> Unit,
|
||||
val onClick: () -> Unit,
|
||||
val onDisposeExpressStatus: () -> Unit,
|
||||
val iconState: ExpressTransactionStateIconUM,
|
||||
val toAmount: TextReference,
|
||||
val toFiatAmount: TextReference?,
|
||||
val toAmountSymbol: String,
|
||||
val toCurrencyIcon: CurrencyIconState,
|
||||
|
||||
val fromAmount: TextReference,
|
||||
val fromFiatAmount: TextReference?,
|
||||
val fromAmountSymbol: String,
|
||||
val fromCurrencyIcon: CurrencyIconState,
|
||||
)
|
||||
|
||||
enum class ExpressTransactionStateIconUM {
|
||||
Warning,
|
||||
Error,
|
||||
None,
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
package com.tangem.common.ui.navigationButtons
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.isNullOrEmpty
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun NavigationButtonsBlock(
|
||||
buttonState: NavigationButtonsState,
|
||||
modifier: Modifier = Modifier,
|
||||
footerText: TextReference? = null,
|
||||
) {
|
||||
val state = buttonState as? NavigationButtonsState.Data
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
InfoText(footerText)
|
||||
ExtraButtons(state?.extraButtons, state?.txUrl)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
PreviousButton(state?.prevButton)
|
||||
NavigationPrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
|
||||
val wrappedButton by rememberNavigationButton(primaryButton)
|
||||
AnimatedContent(
|
||||
targetState = wrappedButton,
|
||||
transitionSpec = { navigationButtonsTransition() },
|
||||
contentAlignment = Alignment.Center,
|
||||
label = "Animate show primary button",
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) { button ->
|
||||
if (button != null && button.textReference != TextReference.EMPTY) {
|
||||
val icon = if (button.iconRes != null && button.isIconVisible) {
|
||||
TangemButtonIconPosition.End(iconResId = button.iconRes)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
}
|
||||
TangemButton(
|
||||
text = button.textReference.resolveReference(),
|
||||
enabled = button.isEnabled,
|
||||
onClick = button.onClick,
|
||||
showProgress = button.showProgress,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
icon = icon,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PreviousButton(prevButton: NavigationButton?) {
|
||||
AnimatedVisibility(
|
||||
visible = prevButton != null,
|
||||
enter = expandHorizontally(expandFrom = Alignment.End),
|
||||
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
|
||||
label = "Animate show prev button",
|
||||
) {
|
||||
val button = remember(this) { requireNotNull(prevButton) }
|
||||
if (button.iconRes != null && button.isIconVisible) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(button.iconRes),
|
||||
),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(onClick = button.onClick)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtraButtons(extraButtons: ImmutableList<NavigationButton>?, txUrl: String?) {
|
||||
AnimatedVisibility(
|
||||
visible = !txUrl.isNullOrBlank() && extraButtons != null,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()),
|
||||
label = "Animate show sent state buttons",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
val buttons = remember(this) { requireNotNull(extraButtons) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
buttons.forEach { button ->
|
||||
val icon = button.iconRes?.let { TangemButtonIconPosition.Start(iconResId = it) }
|
||||
?: TangemButtonIconPosition.None
|
||||
TangemButton(
|
||||
text = button.textReference.resolveReference(),
|
||||
icon = icon,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
onClick = rememberHapticFeedback(state = button, onAction = button.onClick),
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = button.isEnabled,
|
||||
showProgress = false,
|
||||
colors = TangemButtonsDefaults.secondaryButtonColors,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoText(footerText: TextReference?, modifier: Modifier = Modifier) {
|
||||
var isVisibleProxy by remember { mutableStateOf(!footerText.isNullOrEmpty()) }
|
||||
val keyboard by keyboardAsState()
|
||||
|
||||
// the text should appear when the keyboard is closed
|
||||
LaunchedEffect(footerText, keyboard) {
|
||||
if (footerText.isNullOrEmpty() && keyboard is Keyboard.Opened) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
isVisibleProxy = !footerText.isNullOrEmpty()
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisibleProxy,
|
||||
modifier = modifier,
|
||||
enter = slideInVertically() + fadeIn(),
|
||||
exit = fadeOut(tween(durationMillis = 300)),
|
||||
label = "Animate footer text appearance",
|
||||
) {
|
||||
val text = remember(this) { requireNotNull(footerText) }
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberNavigationButton(button: NavigationButton?): MutableState<NavigationButton?> {
|
||||
return remember(
|
||||
button?.iconRes,
|
||||
button?.isIconVisible,
|
||||
button?.isEnabled,
|
||||
button?.showProgress,
|
||||
button?.textReference,
|
||||
) { mutableStateOf(button) }
|
||||
}
|
||||
|
||||
private fun <T> AnimatedContentTransitionScope<T>.navigationButtonsTransition(): ContentTransform {
|
||||
val isPrimaryToHide = targetState != null && initialState == null
|
||||
val isPrimaryWasVisible = targetState == null && initialState != null
|
||||
return if (isPrimaryToHide || isPrimaryWasVisible) {
|
||||
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
|
||||
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
|
||||
} else {
|
||||
fadeIn().togetherWith(fadeOut())
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun NavigationButtonsBlock_Preview(
|
||||
@PreviewParameter(NavigationButtonsBlockDataProvider::class) navigationButtonsState: NavigationButtonsState,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
NavigationButtonsBlock(navigationButtonsState)
|
||||
}
|
||||
}
|
||||
|
||||
private class NavigationButtonsBlockDataProvider : PreviewParameterProvider<NavigationButtonsState> {
|
||||
override val values: Sequence<NavigationButtonsState>
|
||||
get() = sequenceOf(NavigationButtonsPreview.allButtons)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.common.ui.navigationButtons
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
sealed class NavigationButtonsState {
|
||||
data object Empty : NavigationButtonsState()
|
||||
|
||||
data class Data(
|
||||
val primaryButton: NavigationButton,
|
||||
val prevButton: NavigationButton?,
|
||||
val extraButtons: ImmutableList<NavigationButton>,
|
||||
val txUrl: String? = null,
|
||||
val onTextClick: (String) -> Unit,
|
||||
) : NavigationButtonsState()
|
||||
}
|
||||
|
||||
data class NavigationButton(
|
||||
val textReference: TextReference,
|
||||
@DrawableRes val iconRes: Int? = null,
|
||||
val isSecondary: Boolean = false,
|
||||
val isIconVisible: Boolean = false,
|
||||
val showProgress: Boolean = false,
|
||||
val isEnabled: Boolean = true,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.common.ui.navigationButtons.preview
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object NavigationButtonsPreview {
|
||||
|
||||
private val extraButtons = persistentListOf(
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
private val prev = NavigationButton(
|
||||
textReference = TextReference.EMPTY,
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
private val finished = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_close),
|
||||
isSecondary = false,
|
||||
isIconVisible = false,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
val allButtons = NavigationButtonsState.Data(
|
||||
primaryButton = finished,
|
||||
prevButton = prev,
|
||||
extraButtons = extraButtons,
|
||||
txUrl = "https://tangem.com",
|
||||
onTextClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.common.ui.notifications
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
||||
object ExpressNotificationsUM {
|
||||
|
||||
data class NeedVerification(val onGoToProviderClick: (() -> Unit)?) : NotificationUM.Warning(
|
||||
title = resourceReference(R.string.express_exchange_notification_verification_title),
|
||||
subtitle = resourceReference(R.string.express_exchange_notification_verification_text),
|
||||
iconResId = R.drawable.ic_alert_triangle_20,
|
||||
buttonsState = onGoToProviderClick?.let { onClick ->
|
||||
NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.common_go_to_provider),
|
||||
onClick = onClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
data class FailedByProvider(val onGoToProviderClick: (() -> Unit)?) : NotificationUM.Error(
|
||||
title = resourceReference(R.string.express_exchange_notification_failed_title),
|
||||
subtitle = resourceReference(R.string.express_exchange_notification_failed_text),
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
buttonState = onGoToProviderClick?.let { onClick ->
|
||||
NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.common_go_to_provider),
|
||||
onClick = onClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
package com.tangem.common.ui.notifications
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.shorted
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class NotificationUM(val config: NotificationConfig) {
|
||||
|
||||
open class Error(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
iconResId: Int = R.drawable.ic_alert_24,
|
||||
buttonState: NotificationConfig.ButtonsState? = null,
|
||||
onCloseClick: (() -> Unit)? = null,
|
||||
) : NotificationUM(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = iconResId,
|
||||
buttonsState = buttonState,
|
||||
onCloseClick = onCloseClick,
|
||||
),
|
||||
) {
|
||||
|
||||
data object TotalExceedsBalance : Error(
|
||||
title = resourceReference(R.string.send_notification_exceed_balance_title),
|
||||
subtitle = resourceReference(R.string.send_notification_exceed_balance_text),
|
||||
)
|
||||
|
||||
data object InvalidAmount : Error(
|
||||
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
||||
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
|
||||
)
|
||||
|
||||
data class MinimumAmountError(val amount: String) : Error(
|
||||
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_notification_invalid_minimum_amount_text,
|
||||
wrappedList(amount, amount),
|
||||
),
|
||||
)
|
||||
|
||||
data class MinimumSendAmountError(val amount: String) : Error(
|
||||
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.transfer_notification_invalid_minimum_transaction_amount_text,
|
||||
wrappedList(amount, amount),
|
||||
),
|
||||
)
|
||||
|
||||
data class TransactionLimitError(
|
||||
val cryptoCurrency: String,
|
||||
val utxoLimit: String,
|
||||
val amountLimit: String,
|
||||
val onConfirmClick: () -> Unit,
|
||||
) : Error(
|
||||
title = resourceReference(R.string.send_notification_transaction_limit_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_notification_transaction_limit_text,
|
||||
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
|
||||
),
|
||||
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)),
|
||||
onClick = onConfirmClick,
|
||||
),
|
||||
)
|
||||
|
||||
data class TokenExceedsBalance(
|
||||
val networkIconId: Int,
|
||||
val currencyName: String,
|
||||
val feeName: String,
|
||||
val feeSymbol: String,
|
||||
val networkName: String,
|
||||
val mergeFeeNetworkName: Boolean = false,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
) : Error(
|
||||
title = resourceReference(
|
||||
id = R.string.warning_send_blocked_funds_for_fee_title,
|
||||
wrappedList(feeName),
|
||||
),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.warning_send_blocked_funds_for_fee_message,
|
||||
formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol),
|
||||
),
|
||||
iconResId = networkIconId,
|
||||
buttonState = onClick?.let {
|
||||
NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(
|
||||
R.string.common_buy_currency,
|
||||
wrappedList(
|
||||
if (mergeFeeNetworkName) {
|
||||
"$currencyName ($feeSymbol)"
|
||||
} else {
|
||||
feeName
|
||||
},
|
||||
),
|
||||
),
|
||||
onClick = onClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
data class ExceedsBalance(
|
||||
val networkIconId: Int,
|
||||
val currencyName: String,
|
||||
val feeName: String,
|
||||
val feeSymbol: String,
|
||||
val networkName: String,
|
||||
val mergeFeeNetworkName: Boolean = false,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
) : Error(
|
||||
title = resourceReference(
|
||||
id = R.string.warning_blocked_funds_for_fee_title,
|
||||
wrappedList(feeName),
|
||||
),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.warning_blocked_funds_for_fee_message,
|
||||
formatArgs = wrappedList(currencyName),
|
||||
),
|
||||
iconResId = networkIconId,
|
||||
buttonState = onClick?.let {
|
||||
NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(
|
||||
R.string.common_buy_currency,
|
||||
wrappedList(
|
||||
if (mergeFeeNetworkName) {
|
||||
"$currencyName ($feeSymbol)"
|
||||
} else {
|
||||
feeName
|
||||
},
|
||||
),
|
||||
),
|
||||
onClick = onClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error(
|
||||
title = resourceReference(R.string.send_notification_existential_deposit_title),
|
||||
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
|
||||
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)),
|
||||
onClick = onConfirmClick,
|
||||
),
|
||||
)
|
||||
|
||||
data class ReserveAmount(val amount: String) : Error(
|
||||
title = resourceReference(
|
||||
id = R.string.send_notification_invalid_reserve_amount_title,
|
||||
wrappedList(amount),
|
||||
),
|
||||
subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text),
|
||||
)
|
||||
}
|
||||
|
||||
open class Warning(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
iconResId: Int = R.drawable.img_attention_20,
|
||||
buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
onCloseClick: (() -> Unit)? = null,
|
||||
) : NotificationUM(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = iconResId,
|
||||
buttonsState = buttonsState,
|
||||
onCloseClick = onCloseClick,
|
||||
),
|
||||
) {
|
||||
data class HighFeeError(
|
||||
val currencyName: String,
|
||||
val amount: String,
|
||||
val onConfirmClick: () -> Unit,
|
||||
val onCloseClick: () -> Unit,
|
||||
) : Warning(
|
||||
title = resourceReference(R.string.send_notification_high_fee_title),
|
||||
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)),
|
||||
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)),
|
||||
onClick = onConfirmClick,
|
||||
),
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
|
||||
data object FeeTooLow : Warning(
|
||||
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
|
||||
)
|
||||
|
||||
data class TooHigh(
|
||||
val value: String,
|
||||
) : Warning(
|
||||
title = resourceReference(id = R.string.send_notification_fee_too_high_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)),
|
||||
)
|
||||
|
||||
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
|
||||
title = resourceReference(R.string.send_fee_unreachable_error_title),
|
||||
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.warning_button_refresh),
|
||||
onClick = onRefresh,
|
||||
),
|
||||
)
|
||||
|
||||
data class TronAccountNotActivated(val tokenName: String) : Warning(
|
||||
title = resourceReference(R.string.send_fee_unreachable_error_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_tron_account_activation_error,
|
||||
wrappedList(tokenName),
|
||||
),
|
||||
)
|
||||
|
||||
data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning(
|
||||
title = resourceReference(R.string.send_network_fee_warning_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.common_network_fee_warning_content,
|
||||
wrappedList(cryptoAmount, fiatAmount),
|
||||
),
|
||||
)
|
||||
|
||||
data class OnrampErrorNotification(val errorCode: String?, val onRefresh: () -> Unit) : Warning(
|
||||
title = resourceReference(R.string.common_error),
|
||||
subtitle = if (errorCode != null) {
|
||||
resourceReference(R.string.express_error_code, wrappedList(errorCode))
|
||||
} else {
|
||||
resourceReference(R.string.common_unknown_error)
|
||||
},
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.warning_button_refresh),
|
||||
onClick = onRefresh,
|
||||
),
|
||||
)
|
||||
|
||||
data object SwapNoAvailablePair : Warning(
|
||||
title = resourceReference(id = R.string.action_buttons_swap_no_available_pair_notification_title),
|
||||
subtitle = resourceReference(id = R.string.action_buttons_swap_no_available_pair_notification_message),
|
||||
)
|
||||
|
||||
data object SellingRegionalRestriction : Warning(
|
||||
title = resourceReference(id = R.string.selling_regional_restriction_alert_title),
|
||||
subtitle = resourceReference(id = R.string.selling_regional_restriction_alert_message),
|
||||
)
|
||||
|
||||
data object InsufficientBalanceForSelling : Warning(
|
||||
title = resourceReference(id = R.string.selling_insufficient_balance_alert_title),
|
||||
subtitle = resourceReference(id = R.string.selling_insufficient_balance_alert_message),
|
||||
)
|
||||
}
|
||||
|
||||
open class Info(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
iconResId: Int = R.drawable.ic_alert_circle_24,
|
||||
buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
onCloseClick: (() -> Unit)? = null,
|
||||
) : NotificationUM(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = iconResId,
|
||||
buttonsState = buttonsState,
|
||||
onCloseClick = onCloseClick,
|
||||
),
|
||||
)
|
||||
|
||||
sealed interface Cardano {
|
||||
|
||||
data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning(
|
||||
title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.cardano_coin_will_be_send_with_token_description,
|
||||
formatArgs = wrappedList(minAdaValue, tokenName),
|
||||
),
|
||||
)
|
||||
|
||||
data object InsufficientBalanceToTransferCoin : Error(
|
||||
title = resourceReference(id = R.string.cardano_max_amount_has_token_title),
|
||||
subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description),
|
||||
)
|
||||
|
||||
data class InsufficientBalanceToTransferToken(val tokenName: String) : Error(
|
||||
title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.cardano_insufficient_balance_to_send_token_description,
|
||||
formatArgs = wrappedList(tokenName),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface Koinos {
|
||||
data class InsufficientRecoverableMana(
|
||||
val mana: BigDecimal,
|
||||
val maxMana: BigDecimal,
|
||||
) : Error(
|
||||
title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.koinos_insufficient_mana_to_send_koin_description,
|
||||
formatArgs = wrappedList(
|
||||
mana.format { crypto("", Blockchain.Koinos.decimals()).shorted() },
|
||||
maxMana.format { crypto("", Blockchain.Koinos.decimals()).shorted() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
data object InsufficientBalance : Error(
|
||||
title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title),
|
||||
subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description),
|
||||
)
|
||||
|
||||
data class ManaExceedsBalance(
|
||||
val availableKoinForTransfer: BigDecimal,
|
||||
val onReduceClick: () -> Unit,
|
||||
) : Error(
|
||||
title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.koinos_mana_exceeds_koin_balance_description,
|
||||
formatArgs = wrappedList(
|
||||
availableKoinForTransfer.format {
|
||||
crypto(Blockchain.Koinos.currency, Blockchain.Koinos.decimals())
|
||||
},
|
||||
),
|
||||
),
|
||||
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)),
|
||||
onClick = onReduceClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface Solana {
|
||||
|
||||
data class RentInfo(
|
||||
private val rentInfo: CryptoCurrencyWarning.Rent,
|
||||
) : Error(
|
||||
title = TextReference.Res(R.string.send_notification_invalid_amount_title),
|
||||
subtitle = TextReference.Res(
|
||||
id = R.string.send_notification_invalid_amount_rent_fee,
|
||||
formatArgs = wrappedList(rentInfo.exemptionAmount),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
package com.tangem.common.ui.notifications
|
||||
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatString
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.uncapped
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LargeClass")
|
||||
object NotificationsFactory {
|
||||
|
||||
fun MutableList<NotificationUM>.addFeeUnreachableNotification(
|
||||
feeError: GetFeeError?,
|
||||
tokenName: String,
|
||||
onReload: () -> Unit,
|
||||
) {
|
||||
when (feeError) {
|
||||
is GetFeeError.BlockchainErrors.TronActivationError -> add(
|
||||
NotificationUM.Warning.TronAccountNotActivated(tokenName),
|
||||
)
|
||||
is GetFeeError.DataError,
|
||||
is GetFeeError.UnknownError,
|
||||
-> add(
|
||||
NotificationUM.Warning.NetworkFeeUnreachable(onReload),
|
||||
)
|
||||
else -> {
|
||||
/* do nothing */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<NotificationUM>.addFeeUnreachableNotification(
|
||||
tokenStatus: CryptoCurrencyStatus,
|
||||
coinStatus: CryptoCurrencyStatus,
|
||||
feeError: GetFeeError?,
|
||||
onReload: () -> Unit,
|
||||
onClick: (currency: CryptoCurrency) -> Unit,
|
||||
) {
|
||||
when (feeError) {
|
||||
is GetFeeError.BlockchainErrors.TronActivationError -> add(
|
||||
NotificationUM.Warning.TronAccountNotActivated(coinStatus.currency.name),
|
||||
)
|
||||
is GetFeeError.BlockchainErrors.KaspaZeroUtxo -> add(
|
||||
NotificationUM.Error.TokenExceedsBalance(
|
||||
networkIconId = coinStatus.currency.networkIconResId,
|
||||
networkName = coinStatus.currency.name,
|
||||
currencyName = tokenStatus.currency.name,
|
||||
feeName = coinStatus.currency.name,
|
||||
feeSymbol = coinStatus.currency.symbol,
|
||||
mergeFeeNetworkName = false,
|
||||
onClick = {
|
||||
onClick(coinStatus.currency)
|
||||
},
|
||||
),
|
||||
)
|
||||
is GetFeeError.DataError,
|
||||
is GetFeeError.UnknownError,
|
||||
-> add(
|
||||
NotificationUM.Warning.NetworkFeeUnreachable(onReload),
|
||||
)
|
||||
else -> {
|
||||
/* do nothing */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<NotificationUM>.addExceedBalanceNotification(
|
||||
feeAmount: BigDecimal,
|
||||
sendingAmount: BigDecimal,
|
||||
isSubtractionAvailable: Boolean,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
minimumRequirement: BigDecimal? = null,
|
||||
) {
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
if (!isSubtractionAvailable) return
|
||||
|
||||
val showNotification = sendingAmount + feeAmount > balance - minimumRequirement.orZero()
|
||||
if (showNotification) {
|
||||
add(NotificationUM.Error.TotalExceedsBalance)
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<NotificationUM>.addReserveAmountErrorNotification(
|
||||
reserveAmount: BigDecimal?,
|
||||
sendingAmount: BigDecimal,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
isAccountFunded: Boolean,
|
||||
) {
|
||||
if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) {
|
||||
add(
|
||||
NotificationUM.Error.ReserveAmount(
|
||||
reserveAmount.format {
|
||||
crypto(cryptoCurrency)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<NotificationUM>.addMinimumAmountErrorNotification(
|
||||
minimumSendAmount: BigDecimal?,
|
||||
sendingAmount: BigDecimal,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
) {
|
||||
if (minimumSendAmount != null && minimumSendAmount > sendingAmount) {
|
||||
add(
|
||||
NotificationUM.Error.MinimumSendAmountError(
|
||||
amount = minimumSendAmount.format { crypto(cryptoCurrency) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun MutableList<NotificationUM>.addTransactionLimitErrorNotification(
|
||||
currencyCheck: CryptoCurrencyCheck?,
|
||||
sendingAmount: BigDecimal,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
feeValue: BigDecimal,
|
||||
onReduceClick: (
|
||||
reduceAmountTo: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
) -> Unit,
|
||||
) {
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val utxoLimit = currencyCheck?.utxoAmountLimit
|
||||
val availableToSend = utxoLimit?.availableToSend
|
||||
val isDustLimit = checkDustLimits(
|
||||
feeAmount = feeValue,
|
||||
sendingAmount = sendingAmount,
|
||||
dustValue = currencyCheck?.dustValue.orZero(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCurrencyStatus = feeCurrencyStatus,
|
||||
)
|
||||
if (availableToSend != null && !feeValue.isZero() && !isDustLimit) {
|
||||
add(
|
||||
NotificationUM.Error.TransactionLimitError(
|
||||
cryptoCurrency = cryptoCurrency.name,
|
||||
utxoLimit = utxoLimit.limit.toPlainString(),
|
||||
amountLimit = availableToSend.format { crypto(cryptoCurrency) },
|
||||
onConfirmClick = {
|
||||
onReduceClick(
|
||||
availableToSend,
|
||||
NotificationUM.Error.TransactionLimitError::class.java,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Existential Warning
|
||||
*
|
||||
* @param existentialDeposit existential deposit of blockchain
|
||||
* @param feeAmount amount of fee spending for transaction
|
||||
* @param sendingAmount amount sending by user (excluding fee for coins)
|
||||
* @param cryptoCurrencyStatus blockchain currency status
|
||||
* @param onReduceClick action to leave existential amount in balance after transaction
|
||||
*/
|
||||
fun MutableList<NotificationUM>.addExistentialWarningNotification(
|
||||
existentialDeposit: BigDecimal?,
|
||||
feeAmount: BigDecimal,
|
||||
sendingAmount: BigDecimal,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
onReduceClick: (
|
||||
reduceAmountBy: BigDecimal,
|
||||
reduceAmountByDiff: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
) -> Unit,
|
||||
) {
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return
|
||||
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
|
||||
feeAmount
|
||||
} else {
|
||||
sendingAmount + feeAmount
|
||||
}
|
||||
val diff = balance.minus(spendingAmount)
|
||||
if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) {
|
||||
add(
|
||||
NotificationUM.Error.ExistentialDeposit(
|
||||
deposit = existentialDeposit.format { crypto(cryptoCurrency).uncapped() },
|
||||
onConfirmClick = {
|
||||
onReduceClick(
|
||||
existentialDeposit,
|
||||
existentialDeposit.minus(diff),
|
||||
NotificationUM.Error.ExistentialDeposit::class.java,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<NotificationUM>.addFeeCoverageNotification(
|
||||
isFeeCoverage: Boolean,
|
||||
amountField: AmountFieldModel,
|
||||
sendingValue: BigDecimal,
|
||||
appCurrency: AppCurrency,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
) {
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
||||
val amountValue = amountField.cryptoAmount.value ?: return
|
||||
|
||||
val cryptoDiff = amountValue.minus(sendingValue)
|
||||
if (isFeeCoverage) {
|
||||
add(
|
||||
NotificationUM.Warning.FeeCoverageNotification(
|
||||
cryptoAmount = cryptoDiff.format { crypto(cryptoCurrency).uncapped() },
|
||||
fiatAmount = getFiatString(
|
||||
value = cryptoDiff,
|
||||
rate = fiatRate,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<NotificationUM>.addDustWarningNotification(
|
||||
dustValue: BigDecimal?,
|
||||
feeValue: BigDecimal,
|
||||
sendingAmount: BigDecimal,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
) {
|
||||
if (dustValue == null) return
|
||||
val isExceedsLimit = checkDustLimits(
|
||||
feeAmount = feeValue,
|
||||
sendingAmount = sendingAmount,
|
||||
dustValue = dustValue,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCurrencyStatus = feeCurrencyStatus,
|
||||
)
|
||||
if (isExceedsLimit) {
|
||||
add(
|
||||
NotificationUM.Error.MinimumAmountError(
|
||||
amount = dustValue.format { crypto(cryptoCurrencyStatus.currency) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<NotificationUM>.addExceedsBalanceNotification(
|
||||
cryptoCurrencyWarning: CryptoCurrencyWarning?,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
shouldMergeFeeNetworkName: Boolean,
|
||||
onClick: (CryptoCurrency) -> Unit,
|
||||
onAnalyticsEvent: (CryptoCurrency) -> Unit,
|
||||
) {
|
||||
when (cryptoCurrencyWarning) {
|
||||
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
|
||||
add(
|
||||
NotificationUM.Error.TokenExceedsBalance(
|
||||
networkIconId = cryptoCurrencyWarning.coinCurrency.networkIconResId,
|
||||
networkName = cryptoCurrencyWarning.coinCurrency.name,
|
||||
currencyName = cryptoCurrencyStatus.currency.name,
|
||||
feeName = cryptoCurrencyWarning.coinCurrency.name,
|
||||
feeSymbol = cryptoCurrencyWarning.coinCurrency.symbol,
|
||||
mergeFeeNetworkName = shouldMergeFeeNetworkName,
|
||||
onClick = {
|
||||
onClick(cryptoCurrencyWarning.coinCurrency)
|
||||
},
|
||||
),
|
||||
)
|
||||
onAnalyticsEvent(cryptoCurrencyStatus.currency)
|
||||
}
|
||||
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
|
||||
val currency = cryptoCurrencyWarning.feeCurrency
|
||||
add(
|
||||
NotificationUM.Error.TokenExceedsBalance(
|
||||
networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24,
|
||||
currencyName = cryptoCurrencyWarning.currency.name,
|
||||
feeName = cryptoCurrencyWarning.feeCurrencyName,
|
||||
feeSymbol = cryptoCurrencyWarning.feeCurrencySymbol,
|
||||
networkName = cryptoCurrencyWarning.networkName,
|
||||
mergeFeeNetworkName = shouldMergeFeeNetworkName,
|
||||
onClick = {
|
||||
currency?.let {
|
||||
onClick(currency)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
onAnalyticsEvent(cryptoCurrencyWarning.currency)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<NotificationUM>.addValidateTransactionNotifications(
|
||||
dustValue: BigDecimal,
|
||||
validationError: Throwable?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
minAdaValue: BigDecimal?, // TODO revert to Fee, after swap TxFee refactored
|
||||
onReduceClick: (
|
||||
reduceAmountTo: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
) -> Unit,
|
||||
) {
|
||||
when (validationError) {
|
||||
is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError(
|
||||
error = validationError,
|
||||
sendingCurrency = cryptoCurrency,
|
||||
dustValue = dustValue,
|
||||
)
|
||||
is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError(
|
||||
error = validationError,
|
||||
onReduceClick = onReduceClick,
|
||||
)
|
||||
null -> minAdaValue?.let {
|
||||
add(
|
||||
NotificationUM.Cardano.MinAdaValueCharged(
|
||||
tokenName = cryptoCurrency.name,
|
||||
minAdaValue = minAdaValue.parseBigDecimal(cryptoCurrency.decimals),
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addCardanoTransactionValidationError(
|
||||
error: BlockchainSdkError.Cardano,
|
||||
sendingCurrency: CryptoCurrency,
|
||||
dustValue: BigDecimal?,
|
||||
) {
|
||||
when (error) {
|
||||
BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> {
|
||||
add(NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name))
|
||||
}
|
||||
BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> {
|
||||
when (sendingCurrency) {
|
||||
is CryptoCurrency.Coin -> NotificationUM.Cardano.InsufficientBalanceToTransferCoin
|
||||
is CryptoCurrency.Token -> {
|
||||
NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)
|
||||
}
|
||||
}.let(::add)
|
||||
}
|
||||
BlockchainSdkError.Cardano.InsufficientRemainingBalance,
|
||||
BlockchainSdkError.Cardano.InsufficientSendingAdaAmount,
|
||||
-> {
|
||||
dustValue?.let {
|
||||
add(
|
||||
NotificationUM.Error.MinimumAmountError(
|
||||
amount = it.format { crypto(sendingCurrency) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addKoinosTransactionValidationError(
|
||||
error: BlockchainSdkError.Koinos,
|
||||
onReduceClick: (
|
||||
reduceAmountTo: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
) -> Unit,
|
||||
) {
|
||||
when (error) {
|
||||
is BlockchainSdkError.Koinos.InsufficientBalance -> {
|
||||
add(NotificationUM.Koinos.InsufficientBalance)
|
||||
}
|
||||
is BlockchainSdkError.Koinos.InsufficientMana -> {
|
||||
add(
|
||||
NotificationUM.Koinos.InsufficientRecoverableMana(
|
||||
mana = error.manaBalance ?: BigDecimal.ZERO,
|
||||
maxMana = error.maxMana ?: BigDecimal.ZERO,
|
||||
),
|
||||
)
|
||||
}
|
||||
is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> {
|
||||
add(
|
||||
NotificationUM.Koinos.ManaExceedsBalance(
|
||||
availableKoinForTransfer = error.availableKoinForTransfer,
|
||||
onReduceClick = {
|
||||
onReduceClick(
|
||||
error.availableKoinForTransfer,
|
||||
NotificationUM.Koinos.InsufficientRecoverableMana::class.java,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<NotificationUM>.addRentExemptionNotification(rentWarning: CryptoCurrencyWarning.Rent?) {
|
||||
if (rentWarning == null) return
|
||||
add(NotificationUM.Solana.RentInfo(rentWarning))
|
||||
}
|
||||
|
||||
private fun checkDustLimits(
|
||||
feeAmount: BigDecimal,
|
||||
sendingAmount: BigDecimal,
|
||||
dustValue: BigDecimal,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
): Boolean {
|
||||
val change = when (cryptoCurrencyStatus.currency) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
val balance = cryptoCurrencyStatus.value.amount.orZero()
|
||||
balance - (feeAmount + sendingAmount)
|
||||
}
|
||||
is CryptoCurrency.Token -> {
|
||||
val balance = feeCurrencyStatus?.value?.amount.orZero()
|
||||
balance - feeAmount
|
||||
}
|
||||
}
|
||||
|
||||
val dust = when (cryptoCurrencyStatus.currency) {
|
||||
is CryptoCurrency.Coin -> dustValue
|
||||
is CryptoCurrency.Token -> BigDecimal.ZERO
|
||||
}
|
||||
|
||||
val isChangeLowerThanDust = change < dust && change > BigDecimal.ZERO
|
||||
return when (cryptoCurrencyStatus.currency) {
|
||||
is CryptoCurrency.Coin -> sendingAmount < dust || isChangeLowerThanDust
|
||||
is CryptoCurrency.Token -> isChangeLowerThanDust
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.common.ui.swapStoriesScreen
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.promo.models.StoryContent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
object SwapStoriesFactory {
|
||||
|
||||
// WARNING! Be careful with indices. Temporary solution.
|
||||
// Use all data from v1/stories api (image url, title, subtitle)
|
||||
@Suppress("MagicNumber")
|
||||
fun createStoriesState(swapStory: StoryContent, onStoriesClose: (Int) -> Unit): SwapStoriesUM {
|
||||
val storyOrderedImageUrls = swapStory.getImageUrls()
|
||||
if (storyOrderedImageUrls.size != 5) return SwapStoriesUM.Empty
|
||||
|
||||
return SwapStoriesUM.Content(
|
||||
stories = persistentListOf(
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[0],
|
||||
title = resourceReference(R.string.swap_story_first_title),
|
||||
subtitle = resourceReference(R.string.swap_story_first_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[1],
|
||||
title = resourceReference(R.string.swap_story_second_title),
|
||||
subtitle = resourceReference(R.string.swap_story_second_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[2],
|
||||
title = resourceReference(R.string.swap_story_third_title),
|
||||
subtitle = resourceReference(R.string.swap_story_third_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[3],
|
||||
title = resourceReference(R.string.swap_story_forth_title),
|
||||
subtitle = resourceReference(R.string.swap_story_forth_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[4],
|
||||
title = resourceReference(R.string.swap_story_fifth_title),
|
||||
subtitle = resourceReference(R.string.swap_story_fifth_subtitle),
|
||||
),
|
||||
),
|
||||
onClose = onStoriesClose,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.common.ui.swapStoriesScreen
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.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.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.core.ui.components.stories.StoriesContainer
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private val SubtitleColor = Color(0xFFB0B0B0)
|
||||
private const val STORIES_RELATIVE_PADDING = 0.7
|
||||
|
||||
@Composable
|
||||
fun SwapStoriesScreen(config: SwapStoriesUM) {
|
||||
if (config !is SwapStoriesUM.Content) return
|
||||
|
||||
SystemBarsIconsDisposable(darkIcons = false)
|
||||
|
||||
StoriesContainer(
|
||||
config = config,
|
||||
) { current, _ ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemColorPalette.Black),
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(current.imageUrl)
|
||||
.crossfade(enable = false)
|
||||
.allowHardware(true)
|
||||
.memoryCacheKey(current.imageUrl)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build(),
|
||||
loading = { },
|
||||
error = { },
|
||||
contentDescription = null,
|
||||
)
|
||||
SwapStoriesText(current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwapStoriesText(current: SwapStoriesUM.Content.Config) {
|
||||
val height = LocalWindowSize.current.height.value
|
||||
val textAlign = (height.dp.value * STORIES_RELATIVE_PADDING).dp
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = textAlign,
|
||||
start = 44.dp,
|
||||
end = 44.dp,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = current.title.resolveReference(),
|
||||
style = TextStyle(
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
|
||||
lineHeight = TextUnit(value = 34f, type = TextUnitType.Sp),
|
||||
),
|
||||
color = TangemTheme.colors.text.constantWhite,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = current.subtitle.resolveReference(),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
|
||||
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
|
||||
),
|
||||
color = SubtitleColor,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SwapStoriesScreen_Preview() {
|
||||
TangemThemePreview {
|
||||
SwapStoriesScreen(
|
||||
SwapStoriesUM.Content(
|
||||
stories = persistentListOf(
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = "https://devweb.tangem.com/images/stories/swap/image1.png",
|
||||
title = stringReference("Exchange With Us"),
|
||||
subtitle = stringReference(
|
||||
"Trusted exchange providers let you swap assets effortlessly",
|
||||
),
|
||||
),
|
||||
),
|
||||
onClose = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.common.ui.swapStoriesScreen
|
||||
|
||||
import com.tangem.core.ui.components.stories.inner.STORY_DURATION
|
||||
import com.tangem.core.ui.components.stories.model.StoriesContentConfig
|
||||
import com.tangem.core.ui.components.stories.model.StoryConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
sealed class SwapStoriesUM {
|
||||
|
||||
data object Empty : SwapStoriesUM()
|
||||
|
||||
data class Content(
|
||||
override val stories: ImmutableList<Config>,
|
||||
override val onClose: (Int) -> Unit,
|
||||
) : SwapStoriesUM(), StoriesContentConfig<Content.Config> {
|
||||
override val isRestartable: Boolean = false
|
||||
|
||||
data class Config(
|
||||
val imageUrl: String,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
) : StoryConfig {
|
||||
override val duration: Int = STORY_DURATION
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.common.ui.tokens
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
|
||||
fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
|
||||
return when (val unavailabilityReason = this) {
|
||||
is ScenarioUnavailabilityReason.StakingUnavailable -> {
|
||||
resourceReference(
|
||||
id = R.string.token_button_unavailability_reason_staking_unavailable,
|
||||
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
|
||||
)
|
||||
}
|
||||
is ScenarioUnavailabilityReason.PendingTransaction -> unavailabilityReason.getDescription()
|
||||
is ScenarioUnavailabilityReason.EmptyBalance -> unavailabilityReason.getDescription()
|
||||
is ScenarioUnavailabilityReason.BuyUnavailable -> {
|
||||
resourceReference(
|
||||
id = R.string.token_button_unavailability_reason_buy_unavailable,
|
||||
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
|
||||
)
|
||||
}
|
||||
is ScenarioUnavailabilityReason.NotExchangeable -> {
|
||||
resourceReference(
|
||||
id = R.string.token_button_unavailability_reason_not_exchangeable,
|
||||
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
|
||||
)
|
||||
}
|
||||
is ScenarioUnavailabilityReason.NotSupportedBySellService -> {
|
||||
resourceReference(
|
||||
id = R.string.token_button_unavailability_reason_sell_unavailable,
|
||||
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
|
||||
)
|
||||
}
|
||||
ScenarioUnavailabilityReason.Unreachable -> {
|
||||
resourceReference(
|
||||
id = R.string.token_button_unavailability_generic_description,
|
||||
)
|
||||
}
|
||||
ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference(
|
||||
id = R.string.warning_receive_blocked_hedera_token_association_required_message,
|
||||
)
|
||||
ScenarioUnavailabilityReason.UsedOutdatedData -> {
|
||||
resourceReference(id = R.string.token_button_unavailability_reason_out_of_date_balance)
|
||||
}
|
||||
ScenarioUnavailabilityReason.None -> {
|
||||
throw IllegalArgumentException("The unavailability reason must be other than None")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ScenarioUnavailabilityReason.PendingTransaction.getDescription(): TextReference {
|
||||
return resourceReference(
|
||||
id = when (withdrawalScenario) {
|
||||
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> {
|
||||
R.string.token_button_unavailability_reason_pending_transaction_send
|
||||
}
|
||||
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> {
|
||||
R.string.token_button_unavailability_reason_pending_transaction_sell
|
||||
}
|
||||
},
|
||||
formatArgs = wrappedList(networkName),
|
||||
)
|
||||
}
|
||||
|
||||
private fun ScenarioUnavailabilityReason.EmptyBalance.getDescription(): TextReference {
|
||||
return resourceReference(
|
||||
id = when (withdrawalScenario) {
|
||||
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> {
|
||||
R.string.token_button_unavailability_reason_empty_balance_send
|
||||
}
|
||||
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> {
|
||||
R.string.token_button_unavailability_reason_empty_balance_sell
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
package com.tangem.common.ui.tokens
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Token item state converter from [CryptoCurrencyStatus] to [TokenItemState]
|
||||
*
|
||||
* @property appCurrency app currency
|
||||
* @property titleStateProvider title state provider
|
||||
* @property subtitleStateProvider subtitle state provider
|
||||
* @property onItemClick callback is invoked when item is clicked
|
||||
* @property onItemLongClick callback is invoked when item is long clicked
|
||||
*/
|
||||
class TokenItemStateConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
|
||||
CryptoCurrencyToIconStateConverter().convert(it)
|
||||
},
|
||||
private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = Companion::createTitleState,
|
||||
private val subtitleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.SubtitleState? = {
|
||||
createSubtitleState(it, appCurrency)
|
||||
},
|
||||
private val subtitle2StateProvider: (CryptoCurrencyStatus) -> TokenItemState.Subtitle2State? = {
|
||||
createSubtitle2State(status = it)
|
||||
},
|
||||
private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = {
|
||||
createFiatAmountState(status = it, appCurrency = appCurrency)
|
||||
},
|
||||
private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
|
||||
private val onItemLongClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
|
||||
) : Converter<CryptoCurrencyStatus, TokenItemState> {
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
|
||||
return when (value.value) {
|
||||
is CryptoCurrencyStatus.Loading -> value.mapToLoadingState()
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
-> value.mapToTokenItemState()
|
||||
is CryptoCurrencyStatus.MissedDerivation -> value.mapToNoAddressTokenItemState()
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> value.mapToUnreachableTokenItemState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.mapToLoadingState(): TokenItemState.Loading {
|
||||
return TokenItemState.Loading(
|
||||
id = currency.id.value,
|
||||
iconState = iconStateProvider(this),
|
||||
titleState = titleStateProvider(this) as TokenItemState.TitleState.Content,
|
||||
subtitleState = requireNotNull(subtitleStateProvider(this)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content {
|
||||
return TokenItemState.Content(
|
||||
id = currency.id.value,
|
||||
iconState = iconStateProvider(this),
|
||||
titleState = titleStateProvider(this),
|
||||
subtitleState = requireNotNull(subtitleStateProvider(this)),
|
||||
fiatAmountState = requireNotNull(fiatAmountStateProvider(this)),
|
||||
subtitle2State = requireNotNull(subtitle2StateProvider(this)),
|
||||
onItemClick = onItemClick?.let { onItemClick ->
|
||||
{ onItemClick(it, this) }
|
||||
},
|
||||
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
|
||||
{ onItemLongClick(it, this) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState(): TokenItemState.Unreachable {
|
||||
return TokenItemState.Unreachable(
|
||||
id = currency.id.value,
|
||||
iconState = iconStateProvider(this),
|
||||
titleState = titleStateProvider(this),
|
||||
subtitleState = subtitleStateProvider(this),
|
||||
onItemClick = onItemClick?.let { onItemClick ->
|
||||
{ onItemClick(it, this) }
|
||||
},
|
||||
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
|
||||
{ onItemLongClick(it, this) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState(): TokenItemState.NoAddress {
|
||||
return TokenItemState.NoAddress(
|
||||
id = currency.id.value,
|
||||
iconState = iconStateProvider(this),
|
||||
titleState = titleStateProvider(this),
|
||||
subtitleState = subtitleStateProvider(this),
|
||||
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
|
||||
{ onItemLongClick(it, this) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun CryptoCurrencyStatus.getFormattedFiatAmount(appCurrency: AppCurrency, includeStaking: Boolean): String {
|
||||
val fiatAmount = value.fiatAmount ?: return DASH_SIGN
|
||||
|
||||
val totalAmount = if (includeStaking) {
|
||||
val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero()
|
||||
|
||||
fiatAmount.plus(fiatYieldBalance)
|
||||
} else {
|
||||
fiatAmount
|
||||
}
|
||||
|
||||
return totalAmount.format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
}
|
||||
|
||||
fun CryptoCurrencyStatus.getFormattedCryptoAmount(includeStaking: Boolean): String {
|
||||
val cryptoAmount = value.amount ?: return DASH_SIGN
|
||||
|
||||
val totalAmount = if (includeStaking) {
|
||||
cryptoAmount.plus(getStakedBalance())
|
||||
} else {
|
||||
cryptoAmount
|
||||
}
|
||||
|
||||
return totalAmount.format { crypto(currency) }
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getStakedBalance() =
|
||||
(value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero()
|
||||
|
||||
private fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState {
|
||||
return when (val value = currencyStatus.value) {
|
||||
is CryptoCurrencyStatus.Loading,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> {
|
||||
TokenItemState.TitleState.Content(text = stringReference(currencyStatus.currency.name))
|
||||
}
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
-> {
|
||||
TokenItemState.TitleState.Content(
|
||||
text = stringReference(currencyStatus.currency.name),
|
||||
hasPending = value.hasCurrentNetworkTransactions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSubtitleState(
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
appCurrency: AppCurrency,
|
||||
): TokenItemState.SubtitleState? {
|
||||
return when (currencyStatus.value) {
|
||||
is CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
-> currencyStatus.getCryptoPriceState(appCurrency)
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? {
|
||||
return when (status.value) {
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
-> {
|
||||
TokenItemState.Subtitle2State.TextContent(
|
||||
text = status.getFormattedCryptoAmount(includeStaking = true),
|
||||
isFlickering = status.value.isFlickering(),
|
||||
)
|
||||
}
|
||||
is CryptoCurrencyStatus.Loading,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFiatAmountState(
|
||||
status: CryptoCurrencyStatus,
|
||||
appCurrency: AppCurrency,
|
||||
): TokenItemState.FiatAmountState? {
|
||||
return when (status.value) {
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
-> {
|
||||
TokenItemState.FiatAmountState.Content(
|
||||
text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = true),
|
||||
isFlickering = status.value.isFlickering(),
|
||||
icons = buildList {
|
||||
if (!status.getStakedBalance().isZero()) {
|
||||
TokenItemState.FiatAmountState.Content.IconUM(
|
||||
iconRes = R.drawable.ic_staking_24,
|
||||
useAccentColor = true,
|
||||
).let(::add)
|
||||
}
|
||||
if (status.value.sources.total == StatusSource.ONLY_CACHE) {
|
||||
TokenItemState.FiatAmountState.Content.IconUM(
|
||||
iconRes = R.drawable.ic_error_sync_24,
|
||||
useAccentColor = false,
|
||||
).let(::add)
|
||||
}
|
||||
}.toImmutableList(),
|
||||
)
|
||||
}
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.Loading,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState {
|
||||
val fiatRate = value.fiatRate
|
||||
val priceChange = value.priceChange
|
||||
|
||||
return if (fiatRate != null && priceChange != null) {
|
||||
TokenItemState.SubtitleState.CryptoPriceContent(
|
||||
price = fiatRate.getFormattedCryptoPrice(appCurrency),
|
||||
priceChangePercent = priceChange.format { percent() },
|
||||
type = priceChange.getPriceChangeType(),
|
||||
isFlickering = value.isFlickering(),
|
||||
)
|
||||
} else {
|
||||
TokenItemState.SubtitleState.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.getFormattedCryptoPrice(appCurrency: AppCurrency): String {
|
||||
return format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.getPriceChangeType(): PriceChangeType {
|
||||
return PriceChangeConverter.fromBigDecimal(value = this)
|
||||
}
|
||||
|
||||
fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.common.ui.tokens
|
||||
|
||||
import androidx.compose.animation.Animatable
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Text view for token price.
|
||||
*
|
||||
* @param price Price of the token.
|
||||
* @param priceChangeType Type of the price change.
|
||||
*/
|
||||
@Composable
|
||||
fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
|
||||
val growColor = TangemTheme.colors.text.accent
|
||||
val fallColor = TangemTheme.colors.text.warning
|
||||
val generalColor = TangemTheme.colors.text.primary1
|
||||
|
||||
val color = remember(generalColor) { Animatable(generalColor) }
|
||||
var animationSkipped by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(price) {
|
||||
if (animationSkipped.not()) {
|
||||
animationSkipped = true
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (priceChangeType != null) {
|
||||
val nextColor = when (priceChangeType) {
|
||||
PriceChangeType.UP,
|
||||
-> growColor
|
||||
PriceChangeType.DOWN -> fallColor
|
||||
PriceChangeType.NEUTRAL -> return@LaunchedEffect
|
||||
}
|
||||
|
||||
color.animateTo(nextColor, snap())
|
||||
color.animateTo(generalColor, tween(durationMillis = 500))
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = price,
|
||||
color = color.value,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
package com.tangem.common.ui.userwallet
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
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.PreviewParameterProvider
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.coil.RotationTransformation
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.text.applyBladeBrush
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.StringsSigns.DOT
|
||||
import com.tangem.utils.StringsSigns.THREE_STARS
|
||||
|
||||
@Composable
|
||||
fun UserWalletItem(
|
||||
state: UserWalletItemUM,
|
||||
modifier: Modifier = Modifier,
|
||||
blockColors: CardColors = TangemBlockCardColors,
|
||||
) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = blockColors,
|
||||
onClick = state.onClick,
|
||||
enabled = state.isEnabled,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size68)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
CardImage(imageUrl = state.imageUrl)
|
||||
NameAndInfo(
|
||||
modifier = Modifier.weight(1f),
|
||||
name = state.name,
|
||||
information = state.information,
|
||||
balance = state.balance,
|
||||
)
|
||||
|
||||
when (state.endIcon) {
|
||||
UserWalletItemUM.EndIcon.None -> {}
|
||||
UserWalletItemUM.EndIcon.Arrow -> {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
UserWalletItemUM.EndIcon.Checkmark -> {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_check_24),
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NameAndInfo(
|
||||
name: TextReference,
|
||||
information: TextReference,
|
||||
balance: UserWalletItemUM.Balance,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.heightIn(min = TangemTheme.dimens.size40),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Text(
|
||||
text = name.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = information.resolveReference() + " $DOT ",
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = balance,
|
||||
label = "Balance content",
|
||||
) { balance ->
|
||||
val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance)
|
||||
|
||||
if (balanceValue == null) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.caption2,
|
||||
text = "aaaaa",
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = balanceValue,
|
||||
style = TangemTheme.typography.caption2.applyBladeBrush(
|
||||
isEnabled = isFlickering,
|
||||
textColor = TangemTheme.colors.text.tertiary,
|
||||
),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) {
|
||||
val imageModifier = modifier
|
||||
.width(TangemTheme.dimens.size24)
|
||||
.height(TangemTheme.dimens.size36)
|
||||
.clip(TangemTheme.shapes.roundedCornersSmall)
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
modifier = imageModifier,
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.transformations(RotationTransformation(angle = 90f))
|
||||
.size(
|
||||
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
|
||||
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
|
||||
)
|
||||
.data(imageUrl)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(enable = false)
|
||||
.build(),
|
||||
loading = {
|
||||
RectangleShimmer(
|
||||
modifier = imageModifier,
|
||||
radius = TangemTheme.dimens.size2,
|
||||
)
|
||||
},
|
||||
error = {
|
||||
Image(
|
||||
modifier = imageModifier,
|
||||
imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getBalanceValueAndFlickerState(balance: UserWalletItemUM.Balance): Pair<String?, Boolean> {
|
||||
return when (balance) {
|
||||
is UserWalletItemUM.Balance.Failed -> DASH_SIGN to false
|
||||
is UserWalletItemUM.Balance.Hidden -> THREE_STARS to false
|
||||
is UserWalletItemUM.Balance.Loading -> null to false
|
||||
is UserWalletItemUM.Balance.Locked -> stringResource(R.string.common_locked) to false
|
||||
is UserWalletItemUM.Balance.Loaded -> balance.value to balance.isFlickering
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_UserWalletItem(
|
||||
@PreviewParameter(UserWalletItemUMPreviewProvider::class) params: UserWalletItemUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
UserWalletItem(
|
||||
state = params,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWalletItemUM> {
|
||||
override val values: Sequence<UserWalletItemUM>
|
||||
get() = sequenceOf(
|
||||
UserWalletItemUM(
|
||||
id = UserWalletId("user_wallet_1".encodeToByteArray()),
|
||||
name = stringReference("My Wallet"),
|
||||
information = getInformation(cardCount = 1),
|
||||
balance = UserWalletItemUM.Balance.Locked,
|
||||
imageUrl = "",
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = UserWalletId("user_wallet_2".encodeToByteArray()),
|
||||
name = stringReference("Old wallet"),
|
||||
information = getInformation(cardCount = 2),
|
||||
balance = UserWalletItemUM.Balance.Hidden,
|
||||
imageUrl = "",
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
endIcon = UserWalletItemUM.EndIcon.Arrow,
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = UserWalletId("user_wallet_3".encodeToByteArray()),
|
||||
name = stringReference("Multi Card"),
|
||||
information = getInformation(cardCount = 3),
|
||||
balance = UserWalletItemUM.Balance.Failed,
|
||||
imageUrl = "",
|
||||
isEnabled = false,
|
||||
endIcon = UserWalletItemUM.EndIcon.Checkmark,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = UserWalletId("user_wallet_3".encodeToByteArray()),
|
||||
name = stringReference("Multi Card"),
|
||||
information = getInformation(cardCount = 3),
|
||||
balance = UserWalletItemUM.Balance.Loading,
|
||||
imageUrl = "",
|
||||
isEnabled = false,
|
||||
endIcon = UserWalletItemUM.EndIcon.Checkmark,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = UserWalletId("user_wallet_3".encodeToByteArray()),
|
||||
name = stringReference("Multi Card"),
|
||||
information = getInformation(cardCount = 3),
|
||||
balance = UserWalletItemUM.Balance.Loaded(
|
||||
value = "1.2345 BTC",
|
||||
isFlickering = false,
|
||||
),
|
||||
imageUrl = "",
|
||||
isEnabled = false,
|
||||
endIcon = UserWalletItemUM.EndIcon.Checkmark,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = UserWalletId("user_wallet_3".encodeToByteArray()),
|
||||
name = stringReference("Multi Card"),
|
||||
information = getInformation(cardCount = 3),
|
||||
balance = UserWalletItemUM.Balance.Loaded(
|
||||
value = "1.2345 BTC",
|
||||
isFlickering = true,
|
||||
),
|
||||
imageUrl = "",
|
||||
isEnabled = false,
|
||||
endIcon = UserWalletItemUM.EndIcon.Checkmark,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
private fun getInformation(cardCount: Int): TextReference {
|
||||
return TextReference.PluralRes(
|
||||
id = R.plurals.card_label_card_count,
|
||||
count = cardCount,
|
||||
formatArgs = wrappedList(cardCount),
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.tangem.common.ui.userwallet.converter
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.common.util.getCardsCount
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.tokens.model.TotalFiatBalance
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [UserWallet] to [UserWalletItemUM]
|
||||
*
|
||||
* @property onClick lambda be invoked when item is clicked
|
||||
* @property appCurrency selected app currency
|
||||
* @property balance wallet balance
|
||||
* @property isBalanceHidden wallet balance is hidden
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class UserWalletItemUMConverter(
|
||||
private val onClick: (UserWalletId) -> Unit,
|
||||
private val appCurrency: AppCurrency? = null,
|
||||
private val balance: TotalFiatBalance? = null,
|
||||
private val isBalanceHidden: Boolean = false,
|
||||
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
|
||||
) : Converter<UserWallet, UserWalletItemUM> {
|
||||
|
||||
override fun convert(value: UserWallet): UserWalletItemUM {
|
||||
return with(value) {
|
||||
UserWalletItemUM(
|
||||
id = walletId,
|
||||
name = stringReference(name),
|
||||
information = getInfo(userWallet = this),
|
||||
balance = getBalanceInfo(userWallet = this),
|
||||
imageUrl = artworkUrl,
|
||||
isEnabled = !isLocked,
|
||||
endIcon = endIcon,
|
||||
onClick = { onClick(value.walletId) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInfo(userWallet: UserWallet): TextReference {
|
||||
val cardCount = userWallet.getCardsCount() ?: 1
|
||||
return TextReference.PluralRes(
|
||||
id = R.plurals.card_label_card_count,
|
||||
count = cardCount,
|
||||
formatArgs = wrappedList(cardCount),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getBalanceInfo(userWallet: UserWallet): UserWalletItemUM.Balance {
|
||||
return when {
|
||||
isBalanceHidden -> UserWalletItemUM.Balance.Hidden
|
||||
userWallet.isLocked -> UserWalletItemUM.Balance.Locked
|
||||
balance == null -> UserWalletItemUM.Balance.Loading
|
||||
else -> {
|
||||
when (balance) {
|
||||
is TotalFiatBalance.Loading -> UserWalletItemUM.Balance.Loading
|
||||
is TotalFiatBalance.Failed -> UserWalletItemUM.Balance.Failed
|
||||
is TotalFiatBalance.Loaded -> {
|
||||
if (appCurrency != null) {
|
||||
val formattedAmount = balance.amount.format {
|
||||
fiat(appCurrency.code, appCurrency.symbol)
|
||||
}
|
||||
|
||||
UserWalletItemUM.Balance.Loaded(
|
||||
value = formattedAmount,
|
||||
isFlickering = balance.source == StatusSource.CACHE,
|
||||
)
|
||||
} else {
|
||||
UserWalletItemUM.Balance.Failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.common.ui.userwallet.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import javax.annotation.concurrent.Immutable
|
||||
|
||||
@Immutable
|
||||
data class UserWalletItemUM(
|
||||
val id: UserWalletId,
|
||||
val name: TextReference,
|
||||
val information: TextReference,
|
||||
val balance: Balance,
|
||||
val imageUrl: String,
|
||||
val isEnabled: Boolean,
|
||||
val endIcon: EndIcon = EndIcon.None,
|
||||
val onClick: () -> Unit,
|
||||
) {
|
||||
enum class EndIcon {
|
||||
None,
|
||||
Arrow,
|
||||
Checkmark,
|
||||
}
|
||||
|
||||
sealed class Balance {
|
||||
|
||||
data object Hidden : Balance()
|
||||
|
||||
data object Locked : Balance()
|
||||
|
||||
data object Failed : Balance()
|
||||
|
||||
data object Loading : Balance()
|
||||
|
||||
data class Loaded(
|
||||
val value: String,
|
||||
val isFlickering: Boolean,
|
||||
) : Balance()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue