Updated on 2026-08-14
This commit is contained in:
commit
cab30e9431
186 changed files with 3729 additions and 2030 deletions
|
|
@ -22,6 +22,9 @@ android {
|
|||
resources.excludes.add("META-INF/LICENSE.md")
|
||||
resources.excludes.add("META-INF/NOTICE.md")
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
|
|
@ -143,6 +146,8 @@ dependencies {
|
|||
implementation(projects.features.walletSettings.impl)
|
||||
implementation(projects.features.markets.api)
|
||||
implementation(projects.features.markets.impl)
|
||||
implementation(projects.features.onramp.api)
|
||||
implementation(projects.features.onramp.impl)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
package com.tangem.common
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import com.kaspersky.components.composesupport.config.withComposeSupport
|
||||
import com.kaspersky.kaspresso.kaspresso.Kaspresso
|
||||
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.tap.MainActivity
|
||||
import com.tangem.tap.domain.sdk.TangemSdkManager
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Rule
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -24,6 +30,9 @@ abstract class BaseTestCase : TestCase(
|
|||
@Inject
|
||||
lateinit var tangemSdkManager: TangemSdkManager
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferencesStore: AppPreferencesStore
|
||||
|
||||
@get:Rule
|
||||
open val composeTestRule = createAndroidComposeRule<MainActivity>()
|
||||
|
||||
|
|
@ -48,9 +57,22 @@ abstract class BaseTestCase : TestCase(
|
|||
hiltRule.inject()
|
||||
Intents.init()
|
||||
additionalBeforeSection()
|
||||
|
||||
runBlocking {
|
||||
delay(INIT_DELAY)
|
||||
}
|
||||
|
||||
}.after {
|
||||
runBlocking {
|
||||
appPreferencesStore.editData { prefs -> prefs.clear() }
|
||||
}
|
||||
|
||||
additionalAfterSection()
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INIT_DELAY = 1000L
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -143,9 +143,6 @@ class DetailsScreenTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
ComposeScreen.onComposeScreen<WalletSettingsTestScreen>(composeTestRule) {
|
||||
step("Assert Link more cards button does not exist") {
|
||||
linkMoreCardsButton.assertIsNotDisplayed()
|
||||
}
|
||||
step("Assert Card Settings button is visible") {
|
||||
cardSettingsButton.assertIsDisplayed()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tests
|
||||
|
||||
import android.content.Intent.ACTION_VIEW
|
||||
import androidx.test.espresso.intent.matcher.UriMatchers
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.screens.DisclaimerTestScreen
|
||||
|
|
@ -9,6 +10,7 @@ import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
|
|||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.kakao.intent.KIntent
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -29,7 +31,7 @@ class StoriesTest : BaseTestCase() {
|
|||
step("Assert: browser opened") {
|
||||
val expectedIntent = KIntent {
|
||||
hasAction(ACTION_VIEW)
|
||||
hasData(NEW_BUY_WALLET_URL)
|
||||
hasData { toString().startsWith(NEW_BUY_WALLET_URL) }
|
||||
}
|
||||
expectedIntent.intended()
|
||||
device.uiDevice.pressBack()
|
||||
|
|
|
|||
|
|
@ -748,6 +748,16 @@
|
|||
"networkId": "core/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "casper-network",
|
||||
"name": "Casper",
|
||||
"symbol": "CSPR",
|
||||
"networks": [
|
||||
{
|
||||
"networkId": "casper-network/test"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
|
||||
|
|
@ -119,4 +120,6 @@ interface ApplicationEntryPoint {
|
|||
fun getHomeFeatureToggles(): HomeFeatureToggles
|
||||
|
||||
fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase
|
||||
|
||||
fun getOnrampFeatureToggles(): OnrampFeatureToggles
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.tap.common.analytics.AnalyticsFactory
|
||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
|
||||
|
|
@ -189,6 +190,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase
|
||||
get() = entryPoint.getGetUserCountryCodeUseCase()
|
||||
|
||||
private val onrampFeatureToggles: OnrampFeatureToggles
|
||||
get() = entryPoint.getOnrampFeatureToggles()
|
||||
// endregion
|
||||
|
||||
override fun onCreate() {
|
||||
|
|
@ -272,6 +276,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
transactionSignerFactory = transactionSignerFactory,
|
||||
homeFeatureToggles = homeFeatureToggles,
|
||||
getUserCountryUseCase = getUserCountryUseCase,
|
||||
onrampFeatureToggles = onrampFeatureToggles,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.staking.api.navigation.StakingRouter
|
||||
|
|
@ -87,4 +88,5 @@ data class DaggerGraphState(
|
|||
val transactionSignerFactory: TransactionSignerFactory? = null,
|
||||
val homeFeatureToggles: HomeFeatureToggles? = null,
|
||||
val getUserCountryUseCase: GetUserCountryUseCase? = null,
|
||||
val onrampFeatureToggles: OnrampFeatureToggles? = null,
|
||||
) : StateType
|
||||
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
unqualifiedResLocale=en-US
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.kotlin.android) apply false
|
||||
alias(deps.plugins.kotlin.jvm) apply false
|
||||
|
|
@ -20,6 +22,16 @@ interface Injected {
|
|||
val fs: FileSystemOperations
|
||||
}
|
||||
|
||||
// Test Logging
|
||||
subprojects {
|
||||
tasks.withType<Test> {
|
||||
testLogging {
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val assembleInternalQA by tasks.registering {
|
||||
group = "build"
|
||||
description = "Builds internal APK to 'build/outputs' directory"
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ 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.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
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.utils.BigDecimalFormatter.formatCryptoAmount
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
|
@ -49,7 +50,7 @@ class AmountStateConverter(
|
|||
val appCurrency = appCurrencyProvider()
|
||||
val status = cryptoCurrencyStatusProvider()
|
||||
val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||
val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
|
||||
val crypto = status.value.amount.format { crypto(status.currency) }
|
||||
val noFeeRate = status.value.fiatRate.isNullOrZero()
|
||||
|
||||
return AmountState.Data(
|
||||
|
|
|
|||
|
|
@ -19,16 +19,20 @@ 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.res.TangemThemePreview
|
||||
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 = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol)
|
||||
val cryptoAmount = formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol)
|
||||
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = amount.fiatAmount.value,
|
||||
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
|
||||
|
|
@ -77,6 +81,9 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ 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
|
||||
|
|
@ -92,11 +94,7 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
|
|||
),
|
||||
) {
|
||||
val text = if (amountField.isFiatValue) {
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = secondaryAmount.value,
|
||||
cryptoCurrency = secondaryAmount.currencySymbol,
|
||||
decimals = secondaryAmount.decimals,
|
||||
)
|
||||
secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) }
|
||||
} else {
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = secondaryAmount.value,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ 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.utils.BigDecimalFormatter
|
||||
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 java.math.BigDecimal
|
||||
|
||||
sealed class NotificationUM(val config: NotificationConfig) {
|
||||
|
|
@ -267,8 +269,8 @@ sealed class NotificationUM(val config: NotificationConfig) {
|
|||
subtitle = resourceReference(
|
||||
R.string.koinos_insufficient_mana_to_send_koin_description,
|
||||
formatArgs = wrappedList(
|
||||
BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()),
|
||||
BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()),
|
||||
mana.format { crypto("", Blockchain.Koinos.decimals()).shorted() },
|
||||
maxMana.format { crypto("", Blockchain.Koinos.decimals()).shorted() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -286,11 +288,9 @@ sealed class NotificationUM(val config: NotificationConfig) {
|
|||
subtitle = resourceReference(
|
||||
R.string.koinos_mana_exceeds_koin_balance_description,
|
||||
formatArgs = wrappedList(
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
availableKoinForTransfer,
|
||||
Blockchain.Koinos.currency,
|
||||
Blockchain.Koinos.decimals(),
|
||||
),
|
||||
availableKoinForTransfer.format {
|
||||
crypto(Blockchain.Koinos.currency, Blockchain.Koinos.decimals())
|
||||
},
|
||||
),
|
||||
),
|
||||
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ 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.utils.BigDecimalFormatter
|
||||
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
|
||||
|
|
@ -65,10 +67,9 @@ object NotificationsFactory {
|
|||
if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) {
|
||||
add(
|
||||
NotificationUM.Error.ReserveAmount(
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = sendingAmount,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
sendingAmount.format {
|
||||
crypto(cryptoCurrency)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -87,10 +88,7 @@ object NotificationsFactory {
|
|||
NotificationUM.Error.TransactionLimitError(
|
||||
cryptoCurrency = cryptoCurrency.name,
|
||||
utxoLimit = utxoLimit.maxLimit.toPlainString(),
|
||||
amountLimit = BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = utxoLimit.maxAmount,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
amountLimit = utxoLimit.maxAmount.format { crypto(cryptoCurrency) },
|
||||
onConfirmClick = {
|
||||
onReduceClick(
|
||||
utxoLimit.maxAmount,
|
||||
|
|
@ -124,10 +122,7 @@ object NotificationsFactory {
|
|||
if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) {
|
||||
add(
|
||||
NotificationUM.Error.ExistentialDeposit(
|
||||
deposit = BigDecimalFormatter.formatCryptoAmountUncapped(
|
||||
cryptoAmount = existentialDeposit,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
deposit = existentialDeposit.format { crypto(cryptoCurrency).uncapped() },
|
||||
onConfirmClick = {
|
||||
onReduceClick(
|
||||
existentialDeposit,
|
||||
|
|
@ -155,10 +150,7 @@ object NotificationsFactory {
|
|||
if (isFeeCoverage) {
|
||||
add(
|
||||
NotificationUM.Warning.FeeCoverageNotification(
|
||||
cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped(
|
||||
cryptoAmount = cryptoDiff,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
cryptoAmount = cryptoDiff.format { crypto(cryptoCurrency).uncapped() },
|
||||
fiatAmount = getFiatString(
|
||||
value = cryptoDiff,
|
||||
rate = fiatRate,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco
|
|||
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.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
|
|
@ -80,7 +83,7 @@ class TokenItemStateConverter(
|
|||
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
|
||||
val amount = value.amount?.plus(getStakedBalance()) ?: return DASH_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
|
||||
return amount.format { crypto(currency) }
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String {
|
||||
|
|
@ -167,10 +170,7 @@ class TokenItemStateConverter(
|
|||
return if (fiatRate != null && priceChange != null) {
|
||||
TokenItemState.SubtitleState.CryptoPriceContent(
|
||||
price = fiatRate.getFormattedCryptoPrice(appCurrency),
|
||||
priceChangePercent = BigDecimalFormatter.formatPercent(
|
||||
percent = priceChange,
|
||||
useAbsoluteValue = true,
|
||||
),
|
||||
priceChangePercent = priceChange.format { percent() },
|
||||
type = priceChange.getPriceChangeType(),
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
package com.tangem.datasource.api.onramp
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO
|
||||
import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
|
||||
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
|
||||
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
|
||||
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
|
||||
|
||||
internal class MockedOnrampApi : OnrampApi {
|
||||
override suspend fun getCurrencies(): ApiResponse<List<OnrampCurrencyDTO>> = ApiResponse.Success(
|
||||
COUNTRIES.map(OnrampCountryDTO::defaultCurrency),
|
||||
)
|
||||
|
||||
override suspend fun getCountries(): ApiResponse<List<OnrampCountryDTO>> = ApiResponse.Success(COUNTRIES + RUSSIA)
|
||||
|
||||
override suspend fun getCountryByIp(): ApiResponse<OnrampCountryDTO> = ApiResponse.Success(RUSSIA)
|
||||
|
||||
override suspend fun getPaymentMethods(): ApiResponse<List<PaymentMethodDTO>> = ApiResponse.Success(
|
||||
listOf(
|
||||
PaymentMethodDTO(id = "google", name = "Google Play", image = ""),
|
||||
PaymentMethodDTO(id = "apple", name = "Apple Pay", image = ""),
|
||||
PaymentMethodDTO(id = "card", name = "Card", image = ""),
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun getPairs(body: OnrampPairsRequest): ApiResponse<List<OnrampPairDTO>> = ApiResponse.Success(
|
||||
listOf(
|
||||
OnrampPairDTO(
|
||||
fromCurrencyCode = "USD",
|
||||
to = OnrampDestinationDTO(contractAddress = "0xcontract_address", network = "ethereum"),
|
||||
providers = listOf(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun getQuote(
|
||||
fromCurrencyCode: String,
|
||||
toContractAddress: String,
|
||||
toNetwork: String,
|
||||
paymentMethod: String,
|
||||
countryCode: String,
|
||||
fromAmount: String,
|
||||
toDecimals: Int,
|
||||
providerId: String,
|
||||
): ApiResponse<OnrampQuoteResponse> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getData(
|
||||
fromCurrencyCode: String,
|
||||
toContractAddress: String,
|
||||
toNetwork: String,
|
||||
paymentMethod: String,
|
||||
countryCode: String,
|
||||
fromAmount: String,
|
||||
toDecimals: Int,
|
||||
providerId: String,
|
||||
toAddress: String,
|
||||
redirectUrl: String,
|
||||
language: String?,
|
||||
theme: String?,
|
||||
requestId: String,
|
||||
): ApiResponse<OnrampDataResponse> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getStatus(txId: String): ApiResponse<OnrampStatusResponse> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val RUSSIA = OnrampCountryDTO(
|
||||
name = "Russia",
|
||||
code = "RU",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/ru.svg",
|
||||
alpha3 = "RUS",
|
||||
continent = "",
|
||||
defaultCurrency = OnrampCurrencyDTO(
|
||||
name = "Russian ruble",
|
||||
code = "RUB",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/ru.svg",
|
||||
precision = 2,
|
||||
),
|
||||
onrampAvailable = false,
|
||||
)
|
||||
private val COUNTRIES = listOf(
|
||||
OnrampCountryDTO(
|
||||
name = "United States of America",
|
||||
code = "USA",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/us.svg",
|
||||
alpha3 = "USA",
|
||||
continent = "",
|
||||
defaultCurrency = OnrampCurrencyDTO(
|
||||
name = "US Dollar",
|
||||
code = "USD",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/us.svg",
|
||||
precision = 2,
|
||||
),
|
||||
onrampAvailable = true,
|
||||
),
|
||||
OnrampCountryDTO(
|
||||
name = "Europe Union",
|
||||
code = "EU",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/eu.svg",
|
||||
alpha3 = "EUR",
|
||||
continent = "",
|
||||
defaultCurrency = OnrampCurrencyDTO(
|
||||
name = "Euro",
|
||||
code = "EUR",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/eu.svg",
|
||||
precision = 2,
|
||||
),
|
||||
onrampAvailable = true,
|
||||
),
|
||||
OnrampCountryDTO(
|
||||
name = "Great Britain",
|
||||
code = "GB",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/gb.svg",
|
||||
alpha3 = "GB",
|
||||
continent = "",
|
||||
defaultCurrency = OnrampCurrencyDTO(
|
||||
name = "British Pound Sterling",
|
||||
code = "GBP",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/gb.svg",
|
||||
precision = 2,
|
||||
),
|
||||
onrampAvailable = true,
|
||||
),
|
||||
OnrampCountryDTO(
|
||||
name = "CANADA",
|
||||
code = "CA",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/ca.svg",
|
||||
alpha3 = "CA",
|
||||
continent = "",
|
||||
defaultCurrency = OnrampCurrencyDTO(
|
||||
name = "Canadian Dollar",
|
||||
code = "CAD",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/ca.svg",
|
||||
precision = 2,
|
||||
),
|
||||
onrampAvailable = true,
|
||||
),
|
||||
OnrampCountryDTO(
|
||||
name = "Hon Kong",
|
||||
code = "HK",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/hk.svg",
|
||||
alpha3 = "HK",
|
||||
continent = "",
|
||||
defaultCurrency = OnrampCurrencyDTO(
|
||||
name = "Hon Kong Dollar",
|
||||
code = "HKD",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/hk.svg",
|
||||
precision = 2,
|
||||
),
|
||||
onrampAvailable = true,
|
||||
),
|
||||
OnrampCountryDTO(
|
||||
name = "Australia",
|
||||
code = "AU",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/au.svg",
|
||||
alpha3 = "AU",
|
||||
continent = "",
|
||||
defaultCurrency = OnrampCurrencyDTO(
|
||||
name = "Australian Dollar",
|
||||
code = "AUD",
|
||||
image = "https://hatscripts.github.io/circle-flags/flags/au.svg",
|
||||
precision = 2,
|
||||
),
|
||||
onrampAvailable = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,31 +9,31 @@ data class YieldDTO(
|
|||
@Json(name = "id")
|
||||
val id: String,
|
||||
@Json(name = "token")
|
||||
val token: TokenDTO,
|
||||
val token: TokenDTO?,
|
||||
@Json(name = "tokens")
|
||||
val tokens: List<TokenDTO>,
|
||||
val tokens: List<TokenDTO>?,
|
||||
@Json(name = "args")
|
||||
val args: ArgsDTO,
|
||||
val args: ArgsDTO?,
|
||||
@Json(name = "status")
|
||||
val status: StatusDTO,
|
||||
val status: StatusDTO?,
|
||||
@Json(name = "apy")
|
||||
val apy: BigDecimal,
|
||||
val apy: BigDecimal?,
|
||||
@Json(name = "rewardRate")
|
||||
val rewardRate: Double,
|
||||
val rewardRate: Double?,
|
||||
@Json(name = "rewardType")
|
||||
val rewardType: RewardTypeDTO,
|
||||
val rewardType: RewardTypeDTO?,
|
||||
@Json(name = "metadata")
|
||||
val metadata: MetadataDTO,
|
||||
val metadata: MetadataDTO?,
|
||||
@Json(name = "validators")
|
||||
val validators: List<ValidatorDTO>,
|
||||
val validators: List<ValidatorDTO>?,
|
||||
@Json(name = "isAvailable")
|
||||
val isAvailable: Boolean,
|
||||
val isAvailable: Boolean?,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StatusDTO(
|
||||
@Json(name = "enter")
|
||||
val enter: Boolean,
|
||||
val enter: Boolean?,
|
||||
@Json(name = "exit")
|
||||
val exit: Boolean?,
|
||||
)
|
||||
|
|
@ -41,21 +41,21 @@ data class YieldDTO(
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class ArgsDTO(
|
||||
@Json(name = "enter")
|
||||
val enter: Enter,
|
||||
val enter: Enter?,
|
||||
@Json(name = "exit")
|
||||
val exit: Enter?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Enter(
|
||||
@Json(name = "addresses")
|
||||
val addresses: Addresses,
|
||||
val addresses: Addresses?,
|
||||
@Json(name = "args")
|
||||
val args: Map<String, AddressArgumentDTO>,
|
||||
val args: Map<String, AddressArgumentDTO>?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Addresses(
|
||||
@Json(name = "address")
|
||||
val address: AddressArgumentDTO,
|
||||
val address: AddressArgumentDTO?,
|
||||
@Json(name = "additionalAddresses")
|
||||
val additionalAddresses: Map<String, AddressArgumentDTO>? = null,
|
||||
)
|
||||
|
|
@ -65,7 +65,7 @@ data class YieldDTO(
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class ValidatorDTO(
|
||||
@Json(name = "address")
|
||||
val address: String,
|
||||
val address: String?,
|
||||
@Json(name = "status")
|
||||
val status: ValidatorStatusDTO,
|
||||
@Json(name = "name")
|
||||
|
|
@ -106,51 +106,51 @@ data class YieldDTO(
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class MetadataDTO(
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
val name: String?,
|
||||
@Json(name = "logoURI")
|
||||
val logoUri: String,
|
||||
val logoUri: String?,
|
||||
@Json(name = "description")
|
||||
val description: String,
|
||||
val description: String?,
|
||||
@Json(name = "documentation")
|
||||
val documentation: String?,
|
||||
@Json(name = "gasFeeToken")
|
||||
val gasFeeTokenDTO: TokenDTO,
|
||||
val gasFeeTokenDTO: TokenDTO?,
|
||||
@Json(name = "token")
|
||||
val tokenDTO: TokenDTO,
|
||||
val tokenDTO: TokenDTO?,
|
||||
@Json(name = "tokens")
|
||||
val tokensDTO: List<TokenDTO>,
|
||||
val tokensDTO: List<TokenDTO>?,
|
||||
@Json(name = "type")
|
||||
val type: String,
|
||||
val type: String?,
|
||||
@Json(name = "rewardSchedule")
|
||||
val rewardSchedule: RewardScheduleDTO,
|
||||
val rewardSchedule: RewardScheduleDTO?,
|
||||
@Json(name = "cooldownPeriod")
|
||||
val cooldownPeriod: PeriodDTO?,
|
||||
@Json(name = "warmupPeriod")
|
||||
val warmupPeriod: PeriodDTO,
|
||||
val warmupPeriod: PeriodDTO?,
|
||||
@Json(name = "rewardClaiming")
|
||||
val rewardClaiming: RewardClaimingDTO,
|
||||
val rewardClaiming: RewardClaimingDTO?,
|
||||
@Json(name = "defaultValidator")
|
||||
val defaultValidator: String?,
|
||||
@Json(name = "minimumStake")
|
||||
val minimumStake: Int?,
|
||||
@Json(name = "supportsMultipleValidators")
|
||||
val supportsMultipleValidators: Boolean,
|
||||
val supportsMultipleValidators: Boolean?,
|
||||
@Json(name = "revshare")
|
||||
val revshare: EnabledDTO,
|
||||
val revshare: EnabledDTO?,
|
||||
@Json(name = "fee")
|
||||
val fee: EnabledDTO,
|
||||
val fee: EnabledDTO?,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PeriodDTO(
|
||||
@Json(name = "days")
|
||||
val days: Int,
|
||||
val days: Int?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EnabledDTO(
|
||||
@Json(name = "enabled")
|
||||
val enabled: Boolean,
|
||||
val enabled: Boolean?,
|
||||
)
|
||||
|
||||
enum class RewardScheduleDTO {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
|
|||
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.onramp.MockedOnrampApi
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
|
|
@ -98,22 +99,24 @@ internal object NetworkModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideOnrampApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
appLogsStore: AppLogsStore,
|
||||
// @NetworkMoshi moshi: Moshi,
|
||||
// @ApplicationContext context: Context,
|
||||
// apiConfigsManager: ApiConfigsManager,
|
||||
// appLogsStore: AppLogsStore,
|
||||
): OnrampApi {
|
||||
return createApi(
|
||||
id = ApiConfig.ID.Express,
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
clientBuilder = {
|
||||
addInterceptor(
|
||||
NetworkLogsSaveInterceptor(appLogsStore),
|
||||
)
|
||||
},
|
||||
)
|
||||
// TODO: Remove when backend will be ready - [REDACTED_TASK_KEY]
|
||||
return MockedOnrampApi()
|
||||
// return createApi(
|
||||
// id = ApiConfig.ID.Express,
|
||||
// moshi = moshi,
|
||||
// context = context,
|
||||
// apiConfigsManager = apiConfigsManager,
|
||||
// clientBuilder = {
|
||||
// addInterceptor(
|
||||
// NetworkLogsSaveInterceptor(appLogsStore),
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
|||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import com.tangem.utils.extensions.replaceBy
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
|
|
@ -27,4 +28,23 @@ internal class DefaultNetworksStatusesStore(
|
|||
store(key, newValues)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeAll(key: UserWalletId, values: Collection<NetworkStatus>) {
|
||||
mutex.withLock {
|
||||
val currentValues = getSyncOrNull(key) ?: emptySet()
|
||||
val updatedValues = currentValues.toMutableSet()
|
||||
|
||||
values.forEach { newValue ->
|
||||
val isReplaced = updatedValues.replaceBy(newValue) {
|
||||
it.network == newValue.network
|
||||
}
|
||||
|
||||
if (!isReplaced) {
|
||||
updatedValues.add(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
store(key, updatedValues)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,4 +11,6 @@ interface NetworksStatusesStore {
|
|||
suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>?
|
||||
|
||||
suspend fun store(key: UserWalletId, value: NetworkStatus)
|
||||
|
||||
suspend fun storeAll(key: UserWalletId, values: Collection<NetworkStatus>)
|
||||
}
|
||||
|
|
@ -22,5 +22,9 @@
|
|||
{
|
||||
"name": "MIGRATE_USER_COUNTRY_CODE_ENABLED",
|
||||
"version": "5.17.0"
|
||||
},
|
||||
{
|
||||
"name": "ONRAMP_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -52,4 +52,9 @@ dependencies {
|
|||
api(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.markdown)
|
||||
|
||||
/** Tests */
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
}
|
||||
|
|
@ -4,10 +4,15 @@ import androidx.compose.ui.text.AnnotatedString
|
|||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
|
||||
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
|
||||
import com.tangem.core.ui.utils.defaultFormat
|
||||
import com.tangem.core.ui.utils.formatWithThousands
|
||||
import timber.log.Timber
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class AmountVisualTransformation(
|
||||
private val decimals: Int,
|
||||
|
|
@ -25,13 +30,13 @@ class AmountVisualTransformation(
|
|||
val formattedText = if (formattedAmount.isNotEmpty() && symbol != null) {
|
||||
AnnotatedString(
|
||||
if (currencyCode != null) {
|
||||
BigDecimalFormatter.formatFiatEditableAmount(
|
||||
formatFiatEditableAmount(
|
||||
fiatAmount = formattedAmount,
|
||||
fiatCurrencyCode = currencyCode,
|
||||
fiatCurrencySymbol = symbol,
|
||||
)
|
||||
} else {
|
||||
BigDecimalFormatter.formatWithSymbol(formattedAmount, symbol)
|
||||
formatWithSymbol(formattedAmount, symbol)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
|
|
@ -45,6 +50,28 @@ class AmountVisualTransformation(
|
|||
)
|
||||
}
|
||||
|
||||
private fun formatFiatEditableAmount(
|
||||
fiatAmount: String?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (fiatAmount == null) return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
}
|
||||
val formatter = requireNotNull(numberFormatter as? DecimalFormat) {
|
||||
Timber.e("NumberFormat is null")
|
||||
return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
}
|
||||
return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}"
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
private fun formatWithSymbol(amount: String, symbol: String) = "$amount$CURRENCY_SPACE$symbol"
|
||||
|
||||
private class OffsetMappingImpl(
|
||||
private val text: String,
|
||||
private val formattedText: AnnotatedString,
|
||||
|
|
|
|||
|
|
@ -24,9 +24,10 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.annotatedReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
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
|
||||
|
||||
/**
|
||||
|
|
@ -98,7 +99,7 @@ private fun InputRowImageSelectorPreview(
|
|||
append(" ")
|
||||
withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) {
|
||||
append(
|
||||
BigDecimalFormatter.formatPercent(BigDecimal.ZERO, true),
|
||||
BigDecimal.ZERO.format { percent() },
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,30 +2,57 @@
|
|||
|
||||
package com.tangem.core.ui.components.sheetscaffold
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BlurMaskFilter
|
||||
import android.renderscript.Allocation
|
||||
import android.renderscript.Element
|
||||
import android.renderscript.RenderScript
|
||||
import android.renderscript.ScriptIntrinsicBlur
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.geometry.center
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.onPlaced
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import androidx.compose.ui.util.fastMaxOfOrNull
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.graphics.withSave
|
||||
import androidx.core.graphics.withTranslation
|
||||
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.*
|
||||
import com.tangem.core.ui.extensions.softLayerShadow
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.toPx
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
|
|
@ -51,13 +78,6 @@ import kotlin.math.roundToInt
|
|||
* [Dp.Unspecified] for a sheet that spans the entire screen width.
|
||||
* @param sheetShape the shape of the bottom sheet
|
||||
* @param sheetContainerColor the background color of the bottom sheet
|
||||
* @param sheetContentColor the preferred content color provided by the bottom sheet to its
|
||||
* children. Defaults to the matching content color for [sheetContainerColor], or if that is not a
|
||||
* color from the theme, this will keep the same content color set above the bottom sheet.
|
||||
* @param sheetTonalElevation when [sheetContainerColor] is [ColorScheme.surface], a translucent
|
||||
* primary color overlay is applied on top of the container. A higher tonal elevation value will
|
||||
* result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
|
||||
* @param sheetShadowElevation the shadow elevation of the bottom sheet
|
||||
* @param sheetSwipeEnabled whether the sheet swiping is enabled and should react to the user's
|
||||
* input
|
||||
* @param topBar top app bar of the screen, typically a [SmallTopAppBar]
|
||||
|
|
@ -81,10 +101,7 @@ fun TangemBottomSheetScaffold(
|
|||
sheetPeekHeight: Dp,
|
||||
sheetMaxWidth: Dp = 640.dp,
|
||||
sheetShape: Shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
sheetContainerColor: Color = Color.White, // FIXME
|
||||
sheetContentColor: Color = contentColorFor(sheetContainerColor),
|
||||
sheetTonalElevation: Dp = 0.dp,
|
||||
sheetShadowElevation: Dp = 1.dp,
|
||||
sheetContainerColor: Color = Color.White,
|
||||
sheetSwipeEnabled: Boolean = true,
|
||||
topBar: @Composable (() -> Unit)? = null,
|
||||
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
|
||||
|
|
@ -109,9 +126,6 @@ fun TangemBottomSheetScaffold(
|
|||
sheetSwipeEnabled = sheetSwipeEnabled,
|
||||
shape = sheetShape,
|
||||
containerColor = sheetContainerColor,
|
||||
contentColor = sheetContentColor,
|
||||
tonalElevation = sheetTonalElevation,
|
||||
shadowElevation = sheetShadowElevation,
|
||||
content = sheetContent,
|
||||
)
|
||||
},
|
||||
|
|
@ -178,9 +192,6 @@ private fun StandardBottomSheet(
|
|||
sheetSwipeEnabled: Boolean,
|
||||
shape: Shape,
|
||||
containerColor: Color,
|
||||
contentColor: Color,
|
||||
tonalElevation: Dp,
|
||||
shadowElevation: Dp,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -202,7 +213,7 @@ private fun StandardBottomSheet(
|
|||
Modifier
|
||||
}
|
||||
|
||||
Surface(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = sheetMaxWidth)
|
||||
.fillMaxWidth()
|
||||
|
|
@ -251,16 +262,20 @@ private fun StandardBottomSheet(
|
|||
state = state.anchoredDraggableState,
|
||||
orientation = orientation,
|
||||
enabled = sheetSwipeEnabled,
|
||||
),
|
||||
shape = shape,
|
||||
color = containerColor,
|
||||
contentColor = contentColor,
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation,
|
||||
)
|
||||
.softLayerShadow(
|
||||
radius = 8.dp,
|
||||
color = Color.Black.copy(
|
||||
alpha = if (isSystemInDarkTheme()) .16f else .08f
|
||||
),
|
||||
shape = shape,
|
||||
offset = DpOffset(x = 0.dp, y = (-4).dp),
|
||||
isAlphaContentClip = true
|
||||
)
|
||||
.background(containerColor, shape)
|
||||
.clip(shape),
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
content()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +308,7 @@ private fun BottomSheetScaffoldLayout(
|
|||
),
|
||||
) {
|
||||
(topBarMeasurables, bodyMeasurables, bottomSheetMeasurables, snackbarHostMeasurables),
|
||||
constraints,
|
||||
constraints,
|
||||
->
|
||||
val layoutWidth = constraints.maxWidth
|
||||
val layoutHeight = constraints.maxHeight
|
||||
|
|
@ -321,7 +336,7 @@ private fun BottomSheetScaffoldLayout(
|
|||
PartiallyExpanded -> sheetOffset().roundToInt() - snackbarHeight
|
||||
Expanded,
|
||||
Hidden,
|
||||
-> layoutHeight - snackbarHeight
|
||||
-> layoutHeight - snackbarHeight
|
||||
}
|
||||
|
||||
// Placement order is important for elevation
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ fun getActiveIconRes(blockchainId: String): Int {
|
|||
"energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22
|
||||
"energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22
|
||||
"core", "core/test" -> R.drawable.img_core_22
|
||||
"casper-network", "casper-network/test" -> R.drawable.img_casper_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -159,6 +160,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
|
|||
"energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22
|
||||
"energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22
|
||||
"core", "core/test" -> R.drawable.img_core_22
|
||||
"casper-network", "casper-network/test" -> R.drawable.img_casper_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -236,6 +238,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
|
|||
"energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22
|
||||
"energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22
|
||||
"core", "core/test" -> R.drawable.img_core_22
|
||||
"casper-network" -> R.drawable.img_casper_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -316,6 +319,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
|
|||
"energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22
|
||||
"energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_22
|
||||
"core", "core/test" -> R.drawable.ic_core_22
|
||||
"casper-network", "casper-network/test" -> R.drawable.ic_casper_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -396,6 +400,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
|
|||
"energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22
|
||||
"energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_22
|
||||
"core", "core/test" -> R.drawable.ic_core_22
|
||||
"casper-network", "casper-network/test" -> R.drawable.ic_casper_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.core.ui.extensions
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun Modifier.softLayerShadow(
|
||||
radius: Dp = 8.dp,
|
||||
color: Color = Color.Black.copy(alpha = .23f),
|
||||
shape: Shape = RectangleShape,
|
||||
spread: Dp = 0.dp,
|
||||
offset: DpOffset = DpOffset(x = 0.dp, y = 2.dp),
|
||||
isAlphaContentClip: Boolean = false,
|
||||
): Modifier = this.drawWithCache {
|
||||
val radiusPx = radius.toPx()
|
||||
require(radiusPx > 0.0F)
|
||||
val paint = Paint().apply {
|
||||
this.color = color
|
||||
|
||||
asFrameworkPaint().apply {
|
||||
isDither = true
|
||||
isAntiAlias = true
|
||||
|
||||
setShadowLayer(
|
||||
radiusPx,
|
||||
offset.x.toPx(),
|
||||
offset.y.toPx(),
|
||||
color.toArgb(),
|
||||
)
|
||||
}
|
||||
}
|
||||
val shapeOutline = shape.createOutline(
|
||||
size = size,
|
||||
layoutDirection = LayoutDirection.Rtl,
|
||||
density = this,
|
||||
)
|
||||
val shapePath = Path().apply {
|
||||
addOutline(outline = shapeOutline)
|
||||
}
|
||||
|
||||
val drawShadowBlock: DrawScope.() -> Unit = {
|
||||
drawIntoCanvas { canvas ->
|
||||
canvas.withSave {
|
||||
if (spread.value != 0.0F) {
|
||||
canvas.scale(
|
||||
sx = spreadScale(
|
||||
spread = spread.toPx(),
|
||||
size = size.width,
|
||||
),
|
||||
sy = spreadScale(
|
||||
spread = spread.toPx(),
|
||||
size = size.height,
|
||||
),
|
||||
pivotX = center.x,
|
||||
pivotY = center.y,
|
||||
)
|
||||
}
|
||||
|
||||
canvas.drawOutline(
|
||||
outline = shapeOutline,
|
||||
paint = paint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDrawBehind {
|
||||
if (isAlphaContentClip) {
|
||||
clipShadowByPath(
|
||||
path = shapePath,
|
||||
block = drawShadowBlock,
|
||||
)
|
||||
} else {
|
||||
drawShadowBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnnecessaryParentheses")
|
||||
private fun spreadScale(spread: Float, size: Float): Float = 1.0F + ((spread / size) * 2.0F)
|
||||
|
||||
private fun DrawScope.clipShadowByPath(path: Path, block: DrawScope.() -> Unit) {
|
||||
clipPath(
|
||||
path = path,
|
||||
clipOp = ClipOp.Difference,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import android.icu.text.CompactDecimalFormat
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
/**
|
||||
* Formats the amount in compact format.
|
||||
* "123456.6" -> "$123.457K"
|
||||
* "12345.6" -> "$123.046K"
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
*/
|
||||
fun BigDecimalFiatFormat.compact(threeDigitsMethod: Boolean = false): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
if (value < BigDecimal.ONE) {
|
||||
return@BigDecimalFormat defaultAmount()(value)
|
||||
}
|
||||
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = value,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
)
|
||||
|
||||
addFiatCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the amount in compact format.
|
||||
* "123456.6" -> "ETH 123.457K"
|
||||
* "12345.6" -> "123.046K ETH"
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
*/
|
||||
fun BigDecimalCryptoFormat.compact(threeDigitsMethod: Boolean = false): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
if (value < BigDecimal.ONE) {
|
||||
return@BigDecimalFormat defaultAmount()(value)
|
||||
}
|
||||
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = value,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
)
|
||||
|
||||
addFiatCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
fiatCurrencyCode = BigDecimalFormatConstants.usdCurrency.currencyCode,
|
||||
fiatCurrencySymbol = BigDecimalFormatConstants.usdCurrency.symbol,
|
||||
locale = locale,
|
||||
).replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = BigDecimalFormatConstants.usdCurrency.symbol,
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the amount in compact format.
|
||||
* ex. "123456.6" -> "123.46K", "12345.6" -> "123.05K"
|
||||
* Negative amount is not supported!
|
||||
*/
|
||||
fun BigDecimalFormatScope.rawCompact(locale: Locale = Locale.getDefault()) = BigDecimalFormat { value ->
|
||||
if (value < BigDecimal.ZERO) {
|
||||
return@BigDecimalFormat value.toPlainString()
|
||||
}
|
||||
|
||||
formatCompactAmount(
|
||||
amount = value,
|
||||
locale = locale,
|
||||
threeDigitsMethod = false,
|
||||
)
|
||||
}
|
||||
|
||||
// == Helpers ==
|
||||
|
||||
/**
|
||||
* "123456.6" -> "123.457K"
|
||||
* "12345.6" -> "123.046K"
|
||||
* Negative amount is not supported
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
* @param scale the number of digits to the right of the decimal point
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun formatCompactAmount(
|
||||
amount: BigDecimal,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
threeDigitsMethod: Boolean = false,
|
||||
): String {
|
||||
if (threeDigitsMethod) {
|
||||
val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.toString().count()
|
||||
val digitsToFormat = 6 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 4
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
return formatter.format(scaledAmount)
|
||||
} else {
|
||||
val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.toString().count()
|
||||
val digitsToFormat = 5 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 2
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
return formatter.format(scaledAmount)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a proper currency symbol for the provided formatted [amount]
|
||||
* ex. '10.0k" -> "$10.0k", "string" -> "$string"
|
||||
*/
|
||||
private fun addFiatCurrencySymbolToStringAmount(
|
||||
amount: String,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val sampleAmount = BigDecimal.TEN
|
||||
val currency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
maximumFractionDigits = 0
|
||||
minimumFractionDigits = 0
|
||||
this.currency = currency
|
||||
}
|
||||
|
||||
val formatted = formatter.format(sampleAmount)
|
||||
.replace(currency.getSymbol(locale), fiatCurrencySymbol)
|
||||
.replace(sampleAmount.toString(), amount)
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.FORMAT_THRESHOLD
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.utils.extensions.isNotWhitespace
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
open class BigDecimalCryptoFormat(
|
||||
val symbol: String,
|
||||
val decimals: Int,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalFormat {
|
||||
|
||||
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
||||
}
|
||||
|
||||
class BigDecimalCryptoFormatFull(
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalCryptoFormat(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
locale = locale,
|
||||
) {
|
||||
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
|
||||
fun BigDecimalFormatScope.crypto(
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalCryptoFormat {
|
||||
return BigDecimalCryptoFormat(
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
fun BigDecimalFormatScope.crypto(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalCryptoFormat {
|
||||
return BigDecimalCryptoFormat(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
}
|
||||
|
||||
fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value ->
|
||||
val formatter = if (value.isMoreThanThreshold()) {
|
||||
NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = 2
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
} else {
|
||||
NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format for displaying crypto amounts with their original decimals.
|
||||
*/
|
||||
fun BigDecimalCryptoFormat.uncapped() = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format for displaying crypto amounts with a fixed number of decimals.
|
||||
*/
|
||||
fun BigDecimalCryptoFormat.anyDecimals(maxDecimals: Int = decimals, minDecimals: Int = decimals) =
|
||||
BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = maxDecimals
|
||||
minimumFractionDigits = minDecimals
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format for displaying fees.
|
||||
* If the fee is less than the threshold, it will be displayed as a fixed value "<0.000001 BTC", "<BTC 0.000001".
|
||||
*/
|
||||
fun BigDecimalCryptoFormat.fee(canBeLower: Boolean = false) = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
if (value.lessThanFeeCryptoThreshold()) {
|
||||
buildString {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
append(
|
||||
formatter
|
||||
.format(CRYPTO_FEE_FORMAT_THRESHOLD)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
addStartSpace = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
buildString {
|
||||
if (canBeLower) {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
}
|
||||
append(
|
||||
formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
addStartSpace = canBeLower,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// == Helpers ==
|
||||
|
||||
private fun BigDecimal.isMoreThanThreshold() = this > FORMAT_THRESHOLD
|
||||
|
||||
private fun BigDecimal.lessThanFeeCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD
|
||||
|
||||
private val usdCurrency = Currency.getInstance(Locale.US)
|
||||
|
||||
// Replaces fiat currency symbol with crypto currency symbol
|
||||
// with respect to the position of the symbol and whitespace
|
||||
internal fun String.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol: String,
|
||||
cryptoCurrencySymbol: String,
|
||||
addStartSpace: Boolean = false,
|
||||
): String {
|
||||
val str = this
|
||||
if (str.isEmpty()) return str
|
||||
|
||||
return buildString {
|
||||
when {
|
||||
str.endsWith(fiatCurrencySymbol) -> {
|
||||
val withoutSymbol = str.dropLast(fiatCurrencySymbol.length)
|
||||
|
||||
if (cryptoCurrencySymbol.isBlank()) {
|
||||
return withoutSymbol
|
||||
}
|
||||
|
||||
val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol
|
||||
|
||||
append(withoutSymbol)
|
||||
|
||||
if (last.isNotWhitespace()) {
|
||||
append(CURRENCY_SPACE)
|
||||
}
|
||||
|
||||
append(cryptoCurrencySymbol)
|
||||
}
|
||||
str.startsWith(fiatCurrencySymbol) -> {
|
||||
if (addStartSpace) {
|
||||
append(CURRENCY_SPACE)
|
||||
}
|
||||
|
||||
val withoutSymbol = str.drop(fiatCurrencySymbol.length)
|
||||
val first = withoutSymbol.firstOrNull()
|
||||
?: return cryptoCurrencySymbol
|
||||
|
||||
if (cryptoCurrencySymbol.isBlank()) {
|
||||
return withoutSymbol
|
||||
}
|
||||
|
||||
append(cryptoCurrencySymbol)
|
||||
|
||||
if (first.isNotWhitespace()) {
|
||||
append(CURRENCY_SPACE)
|
||||
}
|
||||
|
||||
append(withoutSymbol)
|
||||
}
|
||||
else -> append(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
||||
import com.tangem.utils.StringsSigns.TILDE_SIGN
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
open class BigDecimalFiatFormat(
|
||||
val fiatCurrencyCode: String,
|
||||
val fiatCurrencySymbol: String,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalFormat {
|
||||
override fun invoke(p1: BigDecimal): String = error("")
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
|
||||
fun BigDecimalFormatScope.fiat(
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalFiatFormat {
|
||||
return BigDecimalFiatFormat(
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
/**
|
||||
* Formats fiat amount with default precision.
|
||||
*/
|
||||
fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
if (value.isLessThanThreshold()) {
|
||||
buildString {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
append(
|
||||
formatter.format(FIAT_FORMAT_THRESHOLD)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
formatter.format(value)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats fiat amount with default precision and adds tilde sign
|
||||
*/
|
||||
fun BigDecimalFiatFormat.approximateAmount(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formattedAmount = defaultAmount()(value)
|
||||
|
||||
if (value.isLessThanThreshold()) {
|
||||
formattedAmount
|
||||
} else {
|
||||
buildString {
|
||||
append(TILDE_SIGN)
|
||||
append(formattedAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats fiat amount with extended precision.
|
||||
*/
|
||||
fun BigDecimalFiatFormat.uncapped(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
|
||||
val digits = if (value.isLessThanThreshold()) {
|
||||
FIAT_MARKET_EXTENDED_DIGITS
|
||||
} else {
|
||||
FIAT_MARKET_DEFAULT_DIGITS
|
||||
}
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = digits
|
||||
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats fiat price with precision calculated based on the value.
|
||||
* @see getFiatPriceAmountWithScale
|
||||
*/
|
||||
fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
|
||||
val (priceAmount, finalScale) = getFiatPriceAmountWithScale(value = value)
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = finalScale
|
||||
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(priceAmount)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
// == Helpers ==
|
||||
|
||||
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
|
||||
|
||||
private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair<BigDecimal, Int> {
|
||||
return if (value < BigDecimal.ONE) {
|
||||
val leadingZeroes = value.scale() - value.precision()
|
||||
val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES
|
||||
|
||||
val amount = value
|
||||
.setScale(scale, RoundingMode.HALF_UP)
|
||||
.stripTrailingZeros()
|
||||
|
||||
amount to amount.scale()
|
||||
} else {
|
||||
value to FIAT_MARKET_DEFAULT_DIGITS
|
||||
}
|
||||
}
|
||||
|
||||
// == Constants ==
|
||||
|
||||
private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
private const val FIAT_MARKET_DEFAULT_DIGITS = 2
|
||||
private const val FIAT_MARKET_EXTENDED_DIGITS = 6
|
||||
private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface BigDecimalFormatScope {
|
||||
companion object { val Empty = object : BigDecimalFormatScope {} }
|
||||
}
|
||||
|
||||
fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope
|
||||
|
||||
inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String {
|
||||
return BigDecimalFormatScope.Empty.block()(this)
|
||||
}
|
||||
|
||||
inline fun BigDecimal?.format(
|
||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||
block: BigDecimalFormatScope.() -> BigDecimalFormat,
|
||||
): String {
|
||||
if (this == null) return fallbackString
|
||||
return BigDecimalFormatScope.Empty.block()(this)
|
||||
}
|
||||
|
||||
fun BigDecimal?.format(
|
||||
format: BigDecimalFormat,
|
||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||
): String {
|
||||
if (this == null) return fallbackString
|
||||
return format(this)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.StringsSigns.LOWER_SIGN
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
object BigDecimalFormatConstants {
|
||||
|
||||
const val EMPTY_BALANCE_SIGN = DASH_SIGN
|
||||
const val CAN_BE_LOWER_SIGN = LOWER_SIGN
|
||||
val FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
|
||||
const val CURRENCY_SPACE = '\u00a0'
|
||||
|
||||
val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001")
|
||||
|
||||
val usdCurrency: Currency by lazy { Currency.getInstance(Locale.US) }
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class BigDecimalPercentFormat(
|
||||
val withoutSign: Boolean = true,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalFormat {
|
||||
override fun invoke(value: BigDecimal): String = default()(value)
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
|
||||
fun BigDecimalFormatScope.percent(
|
||||
withoutSign: Boolean = true,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalPercentFormat {
|
||||
return BigDecimalPercentFormat(
|
||||
withoutSign = withoutSign,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
private fun BigDecimalPercentFormat.default(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getPercentInstance(locale).apply {
|
||||
maximumFractionDigits = 2
|
||||
minimumFractionDigits = 2
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
val valueToFormat = if (withoutSign) value.abs() else value
|
||||
|
||||
formatter.format(valueToFormat)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
open class BigDecimalSimpleFormat(
|
||||
val decimals: Int,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalFormat {
|
||||
|
||||
override fun invoke(value: BigDecimal): String = default()(value)
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
|
||||
fun BigDecimalFormatScope.simple(decimals: Int, locale: Locale = Locale.getDefault()) = BigDecimalSimpleFormat(
|
||||
decimals = decimals,
|
||||
locale = locale,
|
||||
)
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
fun BigDecimalSimpleFormat.default() = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getInstance(locale).apply {
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = 0
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import java.util.Currency
|
||||
|
||||
fun getJavaCurrencyByCode(code: String): Currency {
|
||||
return runCatching { Currency.getInstance(code) }
|
||||
.getOrElse { e ->
|
||||
// Currency code is not valid ISO 4217 code
|
||||
if (e is IllegalArgumentException) {
|
||||
BigDecimalFormatConstants.usdCurrency
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,22 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import android.icu.text.CompactDecimalFormat
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.StringsSigns.LOWER_SIGN
|
||||
import com.tangem.utils.StringsSigns.TILDE_SIGN
|
||||
import com.tangem.utils.extensions.isNotWhitespace
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
object BigDecimalFormatter {
|
||||
|
||||
const val EMPTY_BALANCE_SIGN = DASH_SIGN
|
||||
private const val CAN_BE_LOWER_SIGN = LOWER_SIGN
|
||||
private val FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
|
||||
private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
private val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001")
|
||||
|
||||
private const val FIAT_MARKET_DEFAULT_DIGITS = 2
|
||||
private const val FIAT_MARKET_EXTENDED_DIGITS = 6
|
||||
|
|
@ -30,161 +24,7 @@ object BigDecimalFormatter {
|
|||
|
||||
private val usdCurrency = Currency.getInstance("USD")
|
||||
|
||||
@Deprecated(
|
||||
"Use formatCryptoAmount2",
|
||||
replaceWith = ReplaceWith("formatCryptoAmount2"),
|
||||
)
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoCurrency.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009$cryptoCurrency"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate to this method from formatCryptoAmount ([REDACTED_TASK_KEY])
|
||||
fun formatCryptoAmount2(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.symbol,
|
||||
cryptoCurrencySymbol = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
fun formatCryptoAmountShorted(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = if (cryptoAmount.isMoreThanThreshold()) {
|
||||
NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = 2
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
} else {
|
||||
NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoCurrency.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009$cryptoCurrency"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmountUncapped(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = cryptoCurrency.decimals
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoCurrency.symbol.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009${cryptoCurrency.symbol}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatCryptoFeeAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
canBeLower: Boolean = false,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
val amountFormatted = if (cryptoAmount.checkCryptoThreshold()) {
|
||||
buildString {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
append(
|
||||
formatter.format(CRYPTO_FEE_FORMAT_THRESHOLD),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
buildString {
|
||||
if (canBeLower) {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
}
|
||||
append(formatter.format(cryptoAmount))
|
||||
}
|
||||
}
|
||||
|
||||
return if (cryptoCurrency.isEmpty()) {
|
||||
amountFormatted
|
||||
} else {
|
||||
amountFormatted + "\u2009$cryptoCurrency"
|
||||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals, locale)
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatFiatAmount(
|
||||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
|
|
@ -226,6 +66,7 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatFiatAmountUncapped(
|
||||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
|
|
@ -251,6 +92,7 @@ object BigDecimalFormatter {
|
|||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatFiatPriceUncapped(
|
||||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
|
|
@ -273,6 +115,7 @@ object BigDecimalFormatter {
|
|||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair<BigDecimal, Int> {
|
||||
return if (value < BigDecimal.ONE) {
|
||||
val leadingZeroes = value.scale() - value.precision()
|
||||
|
|
@ -288,47 +131,6 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
fun formatFiatEditableAmount(
|
||||
fiatAmount: String?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatterCurrency = getCurrency(fiatCurrencyCode)
|
||||
val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
}
|
||||
val formatter = requireNotNull(numberFormatter as? DecimalFormat) {
|
||||
Timber.e("NumberFormat is null")
|
||||
return EMPTY_BALANCE_SIGN
|
||||
}
|
||||
return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}"
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
fun formatPercent(
|
||||
percent: BigDecimal,
|
||||
useAbsoluteValue: Boolean,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
maxFractionDigits: Int = 2,
|
||||
minFractionDigits: Int = 2,
|
||||
): String {
|
||||
val formatter = NumberFormat.getPercentInstance(locale).apply {
|
||||
maximumFractionDigits = maxFractionDigits
|
||||
minimumFractionDigits = minFractionDigits
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
val value = if (useAbsoluteValue) percent.abs() else percent
|
||||
|
||||
return formatter.format(value)
|
||||
}
|
||||
|
||||
fun formatWithSymbol(amount: String, symbol: String) = "$amount\u2009$symbol"
|
||||
|
||||
private fun BigDecimal.isMoreThanThreshold() = this > FORMAT_THRESHOLD
|
||||
|
||||
private fun getCurrency(code: String): Currency {
|
||||
return runCatching { Currency.getInstance(code) }
|
||||
.getOrElse { e ->
|
||||
|
|
@ -341,231 +143,5 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a proper currency sign for the provided formatted [amount]
|
||||
* ex. '10.0k" -> "$10.0k", "string" -> "$string"
|
||||
*/
|
||||
private fun addCurrencySymbolToStringAmount(
|
||||
amount: String,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val sampleAmount = BigDecimal.TEN
|
||||
val currency = getCurrency(fiatCurrencyCode)
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
maximumFractionDigits = 0
|
||||
minimumFractionDigits = 0
|
||||
this.currency = currency
|
||||
}
|
||||
|
||||
val formatted = formatter.format(sampleAmount)
|
||||
.replace(currency.getSymbol(locale), fiatCurrencySymbol)
|
||||
.replace(sampleAmount.toString(), amount)
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a proper currency sign for the provided formatted [amount]
|
||||
* ex. '10.0k" -> "ETH 10.0k", "string" -> "ETH string"
|
||||
*/
|
||||
private fun addCryptoCurrencySymbolToStringAmount(
|
||||
amount: String,
|
||||
cryptoCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val sampleAmount = BigDecimal.TEN
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
maximumFractionDigits = 0
|
||||
minimumFractionDigits = 0
|
||||
currency = usdCurrency
|
||||
}
|
||||
|
||||
val formatted = formatter.format(sampleAmount)
|
||||
.replace(sampleAmount.toString(), amount)
|
||||
|
||||
return formatted.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.symbol,
|
||||
cryptoCurrencySymbol = cryptoCurrencySymbol,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "123456.6" -> "$123.457K"
|
||||
* "12345.6" -> "$123.046K"
|
||||
* Negative amount is not supported
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
* @param scale the number of digits to the right of the decimal point
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun formatCompactFiatAmount(
|
||||
amount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
threeDigitsMethod: Boolean = false,
|
||||
scale: Int = 0,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (amount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
if (amount < BigDecimal.ONE) {
|
||||
return formatFiatPriceUncapped(
|
||||
fiatAmount = amount,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = amount,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
scale = scale,
|
||||
)
|
||||
|
||||
return addCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "123456.6" -> "ETH 123.457K"
|
||||
* "12345.6" -> "123.046K ETH"
|
||||
* Negative amount is not supported
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
* @param scale the number of digits to the right of the decimal point
|
||||
*/
|
||||
fun formatCompactCryptoAmount(
|
||||
amount: BigDecimal?,
|
||||
cryptoCurrencySymbol: String,
|
||||
threeDigitsMethod: Boolean = false,
|
||||
decimals: Int = 0,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (amount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
if (amount < BigDecimal.ONE) {
|
||||
return formatCryptoAmount2(
|
||||
cryptoAmount = amount,
|
||||
cryptoCurrency = cryptoCurrencySymbol,
|
||||
decimals = decimals,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = amount,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
scale = decimals,
|
||||
)
|
||||
|
||||
return addCryptoCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
cryptoCurrencySymbol = cryptoCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "123456.6" -> "123.457K"
|
||||
* "12345.6" -> "123.046K"
|
||||
* Negative amount is not supported
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
* @param scale the number of digits to the right of the decimal point
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun formatCompactAmount(
|
||||
amount: BigDecimal,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
threeDigitsMethod: Boolean = false,
|
||||
scale: Int = 0,
|
||||
): String {
|
||||
if (threeDigitsMethod) {
|
||||
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.toString().count()
|
||||
val digitsToFormat = 6 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 4
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
|
||||
} else {
|
||||
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.toString().count()
|
||||
val digitsToFormat = 5 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 2
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces fiat currency symbol with crypto currency symbol
|
||||
// with respect to the position of the symbol and whitespace
|
||||
private fun String.replaceFiatSymbolWithCrypto(fiatCurrencySymbol: String, cryptoCurrencySymbol: String): String {
|
||||
val str = this
|
||||
if (str.isEmpty()) return str
|
||||
|
||||
return buildString {
|
||||
when {
|
||||
str.endsWith(fiatCurrencySymbol) -> {
|
||||
val withoutSymbol = str.dropLast(fiatCurrencySymbol.length)
|
||||
val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol
|
||||
|
||||
append(withoutSymbol)
|
||||
|
||||
if (last.isNotWhitespace()) {
|
||||
append("\u2009")
|
||||
}
|
||||
|
||||
append(cryptoCurrencySymbol)
|
||||
}
|
||||
str.startsWith(fiatCurrencySymbol) -> {
|
||||
append(cryptoCurrencySymbol)
|
||||
|
||||
val withoutSymbol = str.drop(fiatCurrencySymbol.length)
|
||||
val first = withoutSymbol.firstOrNull()
|
||||
?: return cryptoCurrencySymbol
|
||||
|
||||
if (first.isNotWhitespace()) {
|
||||
append("\u2009")
|
||||
}
|
||||
|
||||
append(withoutSymbol)
|
||||
}
|
||||
else -> append(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
|
||||
|
||||
private fun BigDecimal.checkCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD
|
||||
}
|
||||
9
core/ui/src/main/res/drawable/ic_casper_22.xml
Normal file
9
core/ui/src/main/res/drawable/ic_casper_22.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:pathData="M7.746,13.948V8.129C7.746,7.991 7.797,7.854 7.901,7.763C7.983,7.69 8.081,7.653 8.195,7.653H15.5V5.4H15.396C14.708,5.4 14.149,5.961 14.149,6.652V6.673C14.149,7.015 13.886,7.314 13.546,7.323C13.205,7.332 12.91,7.051 12.91,6.702V6.643C12.91,5.957 12.355,5.401 11.672,5.401H7.789C7.351,5.401 6.958,5.5 6.611,5.697C6.264,5.895 5.992,6.168 5.795,6.515C5.599,6.863 5.5,7.257 5.5,7.696V14.354C5.5,14.794 5.599,15.188 5.795,15.535C5.993,15.883 6.264,16.156 6.611,16.354C6.958,16.552 7.351,16.65 7.789,16.65H15.498V14.397H8.22C8.082,14.397 7.946,14.346 7.855,14.242C7.782,14.159 7.745,14.061 7.745,13.946L7.746,13.948Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
19
core/ui/src/main/res/drawable/img_casper_22.xml
Normal file
19
core/ui/src/main/res/drawable/img_casper_22.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
|
||||
<path
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M0,0h22v22h-22z"
|
||||
android:fillColor="#FF2D2E"/>
|
||||
<path
|
||||
android:pathData="M7.746,13.948V8.129C7.746,7.991 7.797,7.854 7.901,7.763C7.983,7.69 8.081,7.653 8.195,7.653H15.5V5.4H15.396C14.708,5.4 14.149,5.961 14.149,6.652V6.673C14.149,7.015 13.886,7.314 13.546,7.323C13.205,7.332 12.91,7.051 12.91,6.702V6.643C12.91,5.957 12.355,5.401 11.672,5.401H7.789C7.351,5.401 6.958,5.5 6.611,5.697C6.264,5.895 5.992,6.168 5.795,6.515C5.599,6.863 5.5,7.257 5.5,7.696V14.354C5.5,14.794 5.599,15.188 5.795,15.535C5.993,15.883 6.264,16.156 6.611,16.354C6.958,16.552 7.351,16.65 7.789,16.65H15.498V14.397H8.22C8.082,14.397 7.946,14.346 7.855,14.242C7.782,14.159 7.745,14.061 7.745,13.946L7.746,13.948Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,407 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
internal class BigDecimalCryptoFormatTest {
|
||||
|
||||
private val testLocale = Locale.US
|
||||
private val testLocale2 = Locale.GERMANY
|
||||
private val symbol = "BTC"
|
||||
|
||||
// === defaultAmount() ===
|
||||
|
||||
@Test
|
||||
fun `defaultAmount (usually used as a user balance)`() {
|
||||
val testValue = BigDecimal("0.123456789999")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12345679".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount (usually used as a user balance) alter locale`() {
|
||||
val testValue = BigDecimal("0.123456789999")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale2,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0,12345679".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount decimals more than 8`() {
|
||||
val testValue = BigDecimal("0.123456789999")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12345679".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount decimals more than 8 (short value)`() {
|
||||
val testValue = BigDecimal("0.12345")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12345".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount decimals minimal (short value)`() {
|
||||
val testValue = BigDecimal("0.12345")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 2,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount less than 2 decimals`() {
|
||||
val testValue = BigDecimal("0.12345")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 0,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount grouping`() {
|
||||
val testValue = BigDecimal("12345678.11")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 0,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("12,345,678.11".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
// === shorted() ===
|
||||
|
||||
@Test
|
||||
fun `shorted amount smoke`() {
|
||||
val testValue = BigDecimal("50000.126123")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50,000.13".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shorted amount decimals less than 2 grouping`() {
|
||||
val testValue = BigDecimal("50000.126123")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 1,
|
||||
locale = testLocale,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50,000.13".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shorted amount less than threshold`() {
|
||||
val testValue = BigDecimal("0.0034567899")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 4,
|
||||
locale = testLocale,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.0034".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shorted amount less than threshold, more decimals`() {
|
||||
val testValue = BigDecimal("0.00345678")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.003456".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shorted amount diff locale half up`() {
|
||||
val testValue = BigDecimal("50000.126123")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale2,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50.000,13".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
// === uncapped() ===
|
||||
|
||||
@Test
|
||||
fun `uncapped amount`() {
|
||||
val testValue = BigDecimal("50000.123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50,000.1234123412".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped amount diff locale`() {
|
||||
val testValue = BigDecimal("50000.123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale2,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50.000,1234123412".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped amount half up`() {
|
||||
val testValue = BigDecimal("50000.12341234125")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale2,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50.000,1234123413".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped amount min decimals`() {
|
||||
val testValue = BigDecimal("50000.12341234125")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 1,
|
||||
locale = testLocale2,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50.000,12".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
// === fee ===
|
||||
|
||||
@Test
|
||||
fun `fee amount`() {
|
||||
val testValue = BigDecimal("0.000123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).fee()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.000123".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount diff locale`() {
|
||||
val testValue = BigDecimal("0.000123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale2,
|
||||
).fee()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0,000123".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount canBeLower true`() {
|
||||
val testValue = BigDecimal("0.000123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).fee(canBeLower = true)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + CURRENCY_SPACE_FOR_TESTS + "0.000123".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount canBeLower true (diff locale)`() {
|
||||
val testValue = BigDecimal("0.000123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale2,
|
||||
).fee(canBeLower = true)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + "0,000123".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount lee than threshold`() {
|
||||
val testValue = BigDecimal("0.0000001234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).fee()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + CURRENCY_SPACE_FOR_TESTS + "0.000001".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount min decimals half up`() {
|
||||
val testValue = BigDecimal("0.125412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 1,
|
||||
locale = testLocale,
|
||||
).fee()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.13".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
// === anyDecimals() ===
|
||||
|
||||
@Test
|
||||
fun `anyDecimals smoke`() {
|
||||
val testValue = BigDecimal("0.123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 5,
|
||||
locale = testLocale,
|
||||
).anyDecimals()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12341".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `anyDecimals zero`() {
|
||||
val testValue = BigDecimal("0.123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 0,
|
||||
locale = testLocale,
|
||||
).anyDecimals()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
internal class BigDecimalFiatFormatTest {
|
||||
|
||||
val testLocale = Locale.US
|
||||
val testLocale2 = Locale.GERMANY
|
||||
|
||||
val usdCurrencyCode = "USD"
|
||||
val usdSymbol = "$"
|
||||
|
||||
private fun String.addUsdSymbolLeft() = usdSymbol + this
|
||||
|
||||
// === defaultAmount() ===
|
||||
|
||||
@Test
|
||||
fun `defaultAmount smoke`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1,234.12".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount half up`() {
|
||||
val testValue = BigDecimal("1234.125")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1,234.13".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount diff locale`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale2,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1.234,12".addSymbolWithSpaceRight(usdSymbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount less threshold`() {
|
||||
val testValue = BigDecimal("0.002234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + "0.01".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount less threshold diff locale`() {
|
||||
val testValue = BigDecimal("0.002234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale2,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + "0,01".addSymbolWithSpaceRight(usdSymbol))
|
||||
}
|
||||
|
||||
// === approximateAmount() ===
|
||||
|
||||
@Test
|
||||
fun `approximateAmount smoke`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).approximateAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("~" + "1,234.12".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approximateAmount half up`() {
|
||||
val testValue = BigDecimal("1234.125")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).approximateAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("~" + "1,234.13".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approximateAmount diff locale`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale2,
|
||||
).approximateAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("~" + "1.234,12".addSymbolWithSpaceRight(usdSymbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approximateAmount less threshold`() {
|
||||
val testValue = BigDecimal("0.002234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).approximateAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + "0.01".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
// === uncapped() ===
|
||||
|
||||
@Test
|
||||
fun `uncapped smoke`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1,234.12".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped less threshold`() {
|
||||
val testValue = BigDecimal("0.00121")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.00121".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped decimals overflow`() {
|
||||
val testValue = BigDecimal("0.00123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.001234".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
// === price() ===
|
||||
|
||||
@Test
|
||||
fun `price smoke`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1,234.12".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price diff locale`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale2,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1.234,12".addSymbolWithSpaceRight(usdSymbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price less threshold`() {
|
||||
val testValue = BigDecimal("0.99987")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.9999".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price less threshold more decimals strip zeros`() {
|
||||
val testValue = BigDecimal("0.0000123000")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.0000123".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price less threshold too much decimals strip zeros`() {
|
||||
val testValue = BigDecimal("0.000000000000000000001230001234000")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.00000000000000000000123".addUsdSymbolLeft())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class BigDecimalFormatTest {
|
||||
|
||||
@Test
|
||||
fun smoke() {
|
||||
val value = BigDecimal("1234")
|
||||
val bgformat = BigDecimalFormat { bg ->
|
||||
bg.toPlainString() + "!"
|
||||
}
|
||||
val expected = "1234!"
|
||||
|
||||
Truth.assertThat(
|
||||
value.format(bgformat),
|
||||
).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(
|
||||
value.format { bgformat },
|
||||
).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(
|
||||
null.format(fallbackString = "!") { bgformat },
|
||||
).isEqualTo("!")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
internal class BigDecimalPercentFormatTest {
|
||||
|
||||
val testLocale = Locale.US
|
||||
val testLocale2 = Locale.GERMANY
|
||||
|
||||
@Test
|
||||
fun smoke() {
|
||||
val value = BigDecimal("00.34")
|
||||
|
||||
val formatted = value.format {
|
||||
percent(locale = testLocale)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("34.00%")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun negative() {
|
||||
val value = BigDecimal("00.34").negate()
|
||||
|
||||
val formatted = value.format {
|
||||
percent(locale = testLocale)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("34.00%")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `negative with sign`() {
|
||||
val value = BigDecimal("00.34").negate()
|
||||
|
||||
val formatted = value.format {
|
||||
percent(
|
||||
withoutSign = false,
|
||||
locale = testLocale,
|
||||
)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("-34.00%")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default more decimals half up`() {
|
||||
val value = BigDecimal("00.345678").negate()
|
||||
|
||||
val formatted = value.format {
|
||||
percent(locale = testLocale)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("34.57%")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default diff locale`() {
|
||||
val value = BigDecimal("00.345678").negate()
|
||||
|
||||
val formatted = value.format {
|
||||
percent(locale = testLocale2)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("34,57".addSymbolWithSpaceRight("%"))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
internal const val CURRENCY_SPACE_FOR_TESTS = '\u00a0'
|
||||
|
||||
internal fun String.addSymbolWithSpaceRight(symbol: String): String = "$this$CURRENCY_SPACE_FOR_TESTS$symbol"
|
||||
|
||||
internal fun String.addSymbolWithSpaceLeft(symbol: String): String = "$symbol$CURRENCY_SPACE_FOR_TESTS$this"
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.tangem.utils
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
// todo determine where to place this extensions
|
||||
fun BigDecimal.toFormattedString(
|
||||
decimals: Int,
|
||||
roundingMode: RoundingMode = RoundingMode.DOWN,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val formatter = NumberFormat.getInstance(locale) as? DecimalFormat
|
||||
val df = formatter?.apply {
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = 0
|
||||
isGroupingUsed = true
|
||||
this.roundingMode = roundingMode
|
||||
}
|
||||
return df?.format(this) ?: this.toPlainString()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun BigDecimal.toFormattedCurrencyString(
|
||||
decimals: Int,
|
||||
currency: String? = null,
|
||||
roundingMode: RoundingMode = RoundingMode.DOWN,
|
||||
limitNumberOfDecimals: Boolean = true,
|
||||
): String {
|
||||
val decimalsForRounding = if (limitNumberOfDecimals) {
|
||||
if (decimals > 8) 8 else decimals
|
||||
} else {
|
||||
decimals
|
||||
}
|
||||
val formattedAmount = this.toFormattedString(
|
||||
decimals = decimalsForRounding,
|
||||
roundingMode = roundingMode,
|
||||
)
|
||||
val formattedCurrency = currency?.let { " $it" } ?: ""
|
||||
return "$formattedAmount$formattedCurrency"
|
||||
}
|
||||
|
|
@ -11,4 +11,19 @@ interface Converter<I : Any, O : Any?> {
|
|||
fun convertSet(input: Collection<I>): Set<O> {
|
||||
return input.mapTo(hashSetOf(), ::convert)
|
||||
}
|
||||
|
||||
fun convertListIgnoreErrors(input: Collection<I>, onError: ((Throwable) -> Unit)? = null): List<O> {
|
||||
return input.mapNotNull {
|
||||
try {
|
||||
convert(it)
|
||||
} catch (throwable: Throwable) {
|
||||
onError?.invoke(throwable)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> T?.asMandatory(name: String): T {
|
||||
return this ?: error("$name must not be null")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.data.staking
|
|||
|
||||
import android.util.Base64
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.catch
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
|
@ -30,8 +29,6 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction
|
||||
import com.tangem.datasource.local.token.StakingBalanceStore
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.lce.lceFlow
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
|
|
@ -57,6 +54,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -103,10 +101,6 @@ internal class DefaultStakingRepository(
|
|||
private val yieldBalanceConverter = YieldBalanceConverter()
|
||||
private val yieldBalanceListConverter = YieldBalanceListConverter(yieldBalanceConverter)
|
||||
|
||||
private val isYieldBalanceFetching = MutableStateFlow(
|
||||
value = emptyMap<UserWalletId, Boolean>(),
|
||||
)
|
||||
|
||||
private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) }
|
||||
|
||||
override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) {
|
||||
|
|
@ -126,7 +120,7 @@ internal class DefaultStakingRepository(
|
|||
val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)
|
||||
.getOrThrow()
|
||||
|
||||
stakingYieldsStore.store(stakingTokensWithYields.data.filter { it.isAvailable })
|
||||
stakingYieldsStore.store(stakingTokensWithYields.data.filter { it.isAvailable ?: false })
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -151,7 +145,7 @@ internal class DefaultStakingRepository(
|
|||
val yield = getYield(cryptoCurrencyId, symbol)
|
||||
|
||||
StakingEntryInfo(
|
||||
apr = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr),
|
||||
apr = requireNotNull(yield.preferredValidators.maxByOrNull { it.apr.orZero() }?.apr),
|
||||
rewardSchedule = yield.metadata.rewardSchedule,
|
||||
tokenSymbol = yield.token.symbol,
|
||||
)
|
||||
|
|
@ -207,23 +201,21 @@ internal class DefaultStakingRepository(
|
|||
): StakingAction {
|
||||
return withContext(dispatchers.io) {
|
||||
val response = when (params.actionCommonType) {
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(
|
||||
StakingActionCommonType.Enter -> stakeKitApi.createEnterAction(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(
|
||||
StakingActionCommonType.Exit -> stakeKitApi.createExitAction(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
StakingActionCommonType.PENDING_REWARDS,
|
||||
-> stakeKitApi.createPendingAction(
|
||||
is StakingActionCommonType.Pending -> stakeKitApi.createPendingAction(
|
||||
createPendingActionRequestBody(params),
|
||||
)
|
||||
}
|
||||
|
|
@ -239,23 +231,21 @@ internal class DefaultStakingRepository(
|
|||
): StakingGasEstimate {
|
||||
return withContext(dispatchers.io) {
|
||||
val gasEstimateDTO = when (params.actionCommonType) {
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(
|
||||
StakingActionCommonType.Enter -> stakeKitApi.estimateGasOnEnter(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(
|
||||
StakingActionCommonType.Exit -> stakeKitApi.estimateGasOnExit(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.PENDING_REWARDS,
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
-> stakeKitApi.estimateGasOnPending(
|
||||
is StakingActionCommonType.Pending -> stakeKitApi.estimateGasOnPending(
|
||||
createPendingActionRequestBody(params),
|
||||
)
|
||||
}
|
||||
|
|
@ -389,88 +379,66 @@ internal class DefaultStakingRepository(
|
|||
refresh: Boolean,
|
||||
) = withContext(dispatchers.io) {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
|
||||
try {
|
||||
isYieldBalanceFetching.update {
|
||||
it + (userWalletId to true)
|
||||
}
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getYieldBalancesKey(userWalletId),
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
val yields = getEnabledYields()
|
||||
val availableCurrencies = cryptoCurrencies
|
||||
.mapNotNull { currency ->
|
||||
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
|
||||
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
|
||||
|
||||
if (integrationId != null && yields.any { it.id == integrationId }) {
|
||||
addresses to integrationId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.flatMap { (addresses, integrationId) ->
|
||||
addresses.map { address -> address to integrationId }
|
||||
}
|
||||
.map { getBalanceRequestData(it.first.value, it.second) }
|
||||
.ifEmpty {
|
||||
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
|
||||
error("No addresses found")
|
||||
}
|
||||
val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow()
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getYieldBalancesKey(userWalletId),
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
val yields = getEnabledYields().ifEmpty {
|
||||
Timber.i("No enabled yields for $userWalletId")
|
||||
stakingBalanceStore.store(userWalletId, emptySet())
|
||||
|
||||
stakingBalanceStore.store(userWalletId, result)
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
isYieldBalanceFetching.update {
|
||||
it - userWalletId
|
||||
}
|
||||
}
|
||||
return@invokeOnExpire
|
||||
}
|
||||
val availableCurrencies = cryptoCurrencies
|
||||
.mapNotNull { currency ->
|
||||
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
|
||||
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
|
||||
|
||||
if (integrationId != null && yields.any { it.id == integrationId }) {
|
||||
addresses to integrationId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.flatMap { (addresses, integrationId) ->
|
||||
addresses.map { address -> address to integrationId }
|
||||
}
|
||||
.map { getBalanceRequestData(it.first.value, it.second) }
|
||||
.ifEmpty {
|
||||
Timber.i("No yield balances available for $userWalletId")
|
||||
stakingBalanceStore.store(userWalletId, emptySet())
|
||||
|
||||
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
|
||||
|
||||
return@invokeOnExpire
|
||||
}
|
||||
|
||||
val result = stakeKitApi
|
||||
.getMultipleYieldBalances(availableCurrencies)
|
||||
.getOrThrow()
|
||||
|
||||
stakingBalanceStore.store(userWalletId, result)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMultiYieldBalanceFlow(
|
||||
override fun getMultiYieldBalanceUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): Flow<YieldBalanceList> = channelFlow {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) {
|
||||
send(YieldBalanceList.Empty)
|
||||
} else {
|
||||
launch(dispatchers.io) {
|
||||
stakingBalanceStore.get(userWalletId)
|
||||
.collectLatest { send(yieldBalanceListConverter.convert(it)) }
|
||||
}
|
||||
stakingBalanceStore.get(userWalletId)
|
||||
.onEach {
|
||||
val balances = yieldBalanceListConverter.convert(it)
|
||||
send(balances)
|
||||
}
|
||||
.launchIn(scope = this + dispatchers.io)
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
fetchMultiYieldBalance(
|
||||
userWalletId,
|
||||
cryptoCurrencies,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.cancellable()
|
||||
|
||||
override fun getMultiYieldBalanceLce(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) {
|
||||
send(YieldBalanceList.Empty)
|
||||
} else {
|
||||
launch(dispatchers.io) {
|
||||
combine(
|
||||
stakingBalanceStore.get(userWalletId),
|
||||
isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } },
|
||||
) { result, isFetching ->
|
||||
val balances = yieldBalanceListConverter.convert(result)
|
||||
send(balances, isStillLoading = isFetching)
|
||||
}.collect()
|
||||
}
|
||||
withContext(dispatchers.io) {
|
||||
catch(
|
||||
block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) },
|
||||
catch = { raise(it) },
|
||||
)
|
||||
fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -583,9 +551,10 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
|
||||
private fun getEnabledYields(): List<Yield> {
|
||||
return stakingYieldsStore
|
||||
.get()
|
||||
.map { yieldConverter.convert(it) }
|
||||
return yieldConverter.convertListIgnoreErrors(
|
||||
input = stakingYieldsStore.get(),
|
||||
onError = { Timber.e("Error converting enabled yields list: $it") },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody {
|
||||
|
|
|
|||
|
|
@ -17,37 +17,37 @@ class YieldConverter(
|
|||
|
||||
override fun convert(value: YieldDTO): Yield {
|
||||
return Yield(
|
||||
id = value.id,
|
||||
token = tokenConverter.convert(value.token),
|
||||
tokens = value.tokens.map { tokenConverter.convert(it) },
|
||||
args = convertArgs(value.args),
|
||||
status = convertStatus(value.status),
|
||||
apy = value.apy,
|
||||
rewardRate = value.rewardRate,
|
||||
rewardType = convertRewardType(value.rewardType),
|
||||
metadata = convertMetadata(value.metadata),
|
||||
validators = value.validators
|
||||
id = value.id.asMandatory("id"),
|
||||
token = tokenConverter.convert(value.token.asMandatory("token")),
|
||||
tokens = value.tokens.asMandatory("tokens").map { tokenConverter.convert(it) },
|
||||
args = convertArgs(value.args.asMandatory("args")),
|
||||
status = convertStatus(value.status.asMandatory("status")),
|
||||
apy = value.apy.asMandatory("apy"),
|
||||
rewardRate = value.rewardRate.asMandatory("rewardRate"),
|
||||
rewardType = convertRewardType(value.rewardType.asMandatory("rewardType")),
|
||||
metadata = convertMetadata(value.metadata.asMandatory("metadata")),
|
||||
validators = value.validators.asMandatory("validators")
|
||||
.asSequence()
|
||||
.filter { it.status == ValidatorStatusDTO.ACTIVE }
|
||||
.map { convertValidator(it) }
|
||||
.sortedByDescending { it.isStrategicPartner }
|
||||
.sortedByDescending { it.apr }
|
||||
.toImmutableList(),
|
||||
isAvailable = value.isAvailable,
|
||||
isAvailable = value.isAvailable.asMandatory("isAvailable"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args {
|
||||
return Yield.Args(
|
||||
enter = convertEnter(argsDTO.enter),
|
||||
enter = convertEnter(argsDTO.enter.asMandatory("enter")),
|
||||
exit = argsDTO.exit?.let { convertEnter(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter {
|
||||
return Yield.Args.Enter(
|
||||
addresses = convertAddresses(enterDTO.addresses),
|
||||
args = enterDTO.args
|
||||
addresses = convertAddresses(enterDTO.addresses.asMandatory("addresses")),
|
||||
args = enterDTO.args.asMandatory("args")
|
||||
.mapKeys { convertArgType(it.key) }
|
||||
.mapValues { convertAddressArgument(it.value) },
|
||||
)
|
||||
|
|
@ -55,7 +55,7 @@ class YieldConverter(
|
|||
|
||||
private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses {
|
||||
return Yield.Args.Enter.Addresses(
|
||||
address = convertAddressArgument(addressesDTO.address),
|
||||
address = convertAddressArgument(addressesDTO.address.asMandatory("address")),
|
||||
additionalAddresses = addressesDTO.additionalAddresses
|
||||
?.mapKeys { convertArgType(it.key) }
|
||||
?.mapValues { convertAddressArgument(it.value) },
|
||||
|
|
@ -73,48 +73,51 @@ class YieldConverter(
|
|||
|
||||
private fun convertStatus(statusDTO: YieldDTO.StatusDTO): Yield.Status {
|
||||
return Yield.Status(
|
||||
enter = statusDTO.enter,
|
||||
enter = statusDTO.enter.asMandatory("enter"),
|
||||
exit = statusDTO.exit,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertMetadata(metadataDTO: YieldDTO.MetadataDTO): Yield.Metadata {
|
||||
return Yield.Metadata(
|
||||
name = metadataDTO.name,
|
||||
logoUri = metadataDTO.logoUri,
|
||||
description = metadataDTO.description,
|
||||
name = metadataDTO.name.asMandatory("name"),
|
||||
logoUri = metadataDTO.logoUri.asMandatory("logoUri"),
|
||||
description = metadataDTO.description.asMandatory("description"),
|
||||
documentation = metadataDTO.documentation,
|
||||
gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO),
|
||||
token = tokenConverter.convert(metadataDTO.tokenDTO),
|
||||
tokens = metadataDTO.tokensDTO.map { tokenConverter.convert(it) },
|
||||
type = metadataDTO.type,
|
||||
rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule),
|
||||
gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")),
|
||||
token = tokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")),
|
||||
tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map { tokenConverter.convert(it) },
|
||||
type = metadataDTO.type.asMandatory("type"),
|
||||
rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule.asMandatory("rewardSchedule")),
|
||||
cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) },
|
||||
warmupPeriod = convertPeriod(metadataDTO.warmupPeriod),
|
||||
rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming),
|
||||
warmupPeriod = convertPeriod(metadataDTO.warmupPeriod.asMandatory("warmupPeriod")),
|
||||
rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming.asMandatory("rewardClaiming")),
|
||||
defaultValidator = metadataDTO.defaultValidator,
|
||||
minimumStake = metadataDTO.minimumStake,
|
||||
supportsMultipleValidators = metadataDTO.supportsMultipleValidators,
|
||||
revshare = convertEnabled(metadataDTO.revshare),
|
||||
fee = convertEnabled(metadataDTO.fee),
|
||||
supportsMultipleValidators = metadataDTO.supportsMultipleValidators.asMandatory(
|
||||
"supportsMultipleValidators",
|
||||
),
|
||||
revshare = convertEnabled(metadataDTO.revshare.asMandatory("revshare")),
|
||||
fee = convertEnabled(metadataDTO.fee.asMandatory("fee")),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period {
|
||||
return Yield.Metadata.Period(
|
||||
days = periodDTO.days,
|
||||
days = periodDTO.days.asMandatory("days"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertEnabled(enabledDTO: YieldDTO.MetadataDTO.EnabledDTO): Yield.Metadata.Enabled {
|
||||
return Yield.Metadata.Enabled(
|
||||
enabled = enabledDTO.enabled,
|
||||
enabled = enabledDTO.enabled.asMandatory("enabled"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator {
|
||||
val address = validatorDTO.address.asMandatory("address")
|
||||
return Yield.Validator(
|
||||
address = validatorDTO.address,
|
||||
address = address,
|
||||
status = convertValidatorStatus(validatorDTO.status),
|
||||
name = validatorDTO.name,
|
||||
image = validatorDTO.image,
|
||||
|
|
@ -124,7 +127,7 @@ class YieldConverter(
|
|||
stakedBalance = validatorDTO.stakedBalance,
|
||||
votingPower = validatorDTO.votingPower,
|
||||
preferred = validatorDTO.preferred,
|
||||
isStrategicPartner = isStrategicPartner(validatorDTO.address),
|
||||
isStrategicPartner = isStrategicPartner(address),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import arrow.core.raise.catch
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
|
|
@ -27,8 +26,6 @@ import com.tangem.datasource.local.preferences.utils.storeObject
|
|||
import com.tangem.datasource.local.token.ExpressAssetsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.lce.lceFlow
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
|
@ -42,6 +39,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
|
||||
|
|
@ -66,10 +64,6 @@ internal class DefaultCurrenciesRepository(
|
|||
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
|
||||
private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers)
|
||||
|
||||
private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow(
|
||||
value = emptyMap<UserWalletId, Boolean>(),
|
||||
)
|
||||
|
||||
override suspend fun saveTokens(
|
||||
userWalletId: UserWalletId,
|
||||
currencies: List<CryptoCurrency>,
|
||||
|
|
@ -206,18 +200,14 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow<Throwable, List<CryptoCurrency>> {
|
||||
return lceFlow {
|
||||
val userWallet = catch({ getUserWallet(userWalletId) }) {
|
||||
raise(it)
|
||||
}
|
||||
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
|
||||
return channelFlow {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
|
||||
if (userWallet.isMultiCurrency) {
|
||||
getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId).collect(::send)
|
||||
getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send)
|
||||
} else {
|
||||
val currency = catch({ getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) {
|
||||
raise(it)
|
||||
}
|
||||
val currency = getSingleCurrencyWalletPrimaryCurrency(userWalletId)
|
||||
send(listOf(currency))
|
||||
}
|
||||
}
|
||||
|
|
@ -260,40 +250,14 @@ internal class DefaultCurrenciesRepository(
|
|||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
launch(dispatchers.io) {
|
||||
getMultiCurrencyWalletCurrencies(userWallet)
|
||||
.collectLatest(::send)
|
||||
}
|
||||
getMultiCurrencyWalletCurrencies(userWallet)
|
||||
.onEach { send(it) }
|
||||
.launchIn(scope = this + dispatchers.io)
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
fetchTokensIfCacheExpired(userWallet, refresh = false)
|
||||
}
|
||||
}
|
||||
.cancellable()
|
||||
}
|
||||
|
||||
override fun getMultiCurrencyWalletCurrenciesUpdatesLce(
|
||||
userWalletId: UserWalletId,
|
||||
): LceFlow<Throwable, List<CryptoCurrency>> = lceFlow {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
catch({ ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) }) {
|
||||
raise(it)
|
||||
}
|
||||
|
||||
launch(dispatchers.io) {
|
||||
combine(
|
||||
getMultiCurrencyWalletCurrencies(userWallet),
|
||||
isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } },
|
||||
) { currencies, isFetching ->
|
||||
send(currencies, isStillLoading = isFetching)
|
||||
}.collect()
|
||||
}
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
catch({ fetchTokensIfCacheExpired(userWallet, refresh = false) }) {
|
||||
raise(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrenciesSync(
|
||||
|
|
@ -545,19 +509,7 @@ internal class DefaultCurrenciesRepository(
|
|||
cacheRegistry.invokeOnExpire(
|
||||
key = getTokensCacheKey(userWallet.walletId),
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
isMultiCurrencyWalletCurrenciesFetching.update {
|
||||
it + (userWallet.walletId to true)
|
||||
}
|
||||
|
||||
try {
|
||||
fetchTokens(userWallet)
|
||||
} finally {
|
||||
isMultiCurrencyWalletCurrenciesFetching.update {
|
||||
it - userWallet.walletId
|
||||
}
|
||||
}
|
||||
},
|
||||
block = { fetchTokens(userWallet) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import arrow.core.raise.catch
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
|
|
@ -15,8 +14,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
|
|||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.lce.lceFlow
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
|
||||
|
|
@ -28,7 +25,10 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
|||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -46,42 +46,16 @@ internal class DefaultNetworksRepository(
|
|||
private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory() }
|
||||
private val networkStatusFactory by lazy { NetworkStatusFactory() }
|
||||
|
||||
private val isNetworkStatusesFetching = MutableStateFlow(
|
||||
value = emptyMap<UserWalletId, Boolean>(),
|
||||
)
|
||||
|
||||
override fun getNetworkStatusesUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
): Flow<Set<NetworkStatus>> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
networksStatusesStore.get(userWalletId)
|
||||
.collectLatest(::send)
|
||||
}
|
||||
networksStatusesStore.get(userWalletId)
|
||||
.onEach(::send)
|
||||
.launchIn(scope = this + dispatchers.io)
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, false)
|
||||
}
|
||||
}
|
||||
.cancellable()
|
||||
|
||||
override fun getNetworkStatusesUpdatesLce(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
): LceFlow<Throwable, Set<NetworkStatus>> = lceFlow {
|
||||
launch(dispatchers.io) {
|
||||
combine(
|
||||
networksStatusesStore.get(userWalletId),
|
||||
isNetworkStatusesFetching.map { it.getOrElse(userWalletId) { false } },
|
||||
) { statuses, isFetching ->
|
||||
send(statuses, isStillLoading = isFetching)
|
||||
}.collect()
|
||||
}
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
catch({ fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) }) {
|
||||
raise(it)
|
||||
}
|
||||
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,83 +101,36 @@ internal class DefaultNetworksRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkAddress(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): CryptoCurrencyAddress = withContext(dispatchers.io) {
|
||||
CryptoCurrencyAddress(
|
||||
cryptoCurrency = currency,
|
||||
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
|
||||
.firstOrNull { it.type == AddressType.Default }
|
||||
?.value.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getNetworkAddressFlow(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): Flow<CryptoCurrencyAddress> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
send(getNetworkAddress(userWalletId, currency))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress> =
|
||||
withContext(dispatchers.io) {
|
||||
// Get list of currencies matching [network]
|
||||
val currencies = getCurrencies(userWalletId)
|
||||
|
||||
// There is no currencies matching given [networks] in [userWalletId]
|
||||
if (currencies.toList().isEmpty()) return@withContext emptyList()
|
||||
|
||||
currencies.toList().map { currency ->
|
||||
CryptoCurrencyAddress(
|
||||
cryptoCurrency = currency,
|
||||
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
|
||||
.firstOrNull { it.type == AddressType.Default }
|
||||
?.value.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNetworkAddressesFlow(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): Flow<List<CryptoCurrencyAddress>> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
send(getNetworkAddresses(userWalletId, network))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
send(getNetworkAddresses(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworksStatusesIfCacheExpired(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
refresh: Boolean,
|
||||
) {
|
||||
val currencies = getCurrencies(userWalletId, networks)
|
||||
val networksDeferred = networks.mapNotNull { network ->
|
||||
fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh)
|
||||
) = coroutineScope {
|
||||
if (refresh) {
|
||||
val statusesToRefresh = networks.map { NetworkStatus(it, NetworkStatus.Refreshing) }
|
||||
networksStatusesStore.storeAll(userWalletId, statusesToRefresh)
|
||||
}
|
||||
|
||||
if (networksDeferred.isNotEmpty()) {
|
||||
try {
|
||||
isNetworkStatusesFetching.update {
|
||||
it + (userWalletId to true)
|
||||
}
|
||||
val currencies = getCurrencies(userWalletId, networks)
|
||||
val networksDeferred = networks.mapNotNull { network ->
|
||||
coroutineScope {
|
||||
val key = getNetworksStatusesCacheKey(userWalletId, network)
|
||||
|
||||
networksDeferred.awaitAll()
|
||||
} finally {
|
||||
isNetworkStatusesFetching.update {
|
||||
it - userWalletId
|
||||
if (refresh || cacheRegistry.isExpired(key)) {
|
||||
async {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = key,
|
||||
skipCache = refresh,
|
||||
block = { fetchNetworkStatus(userWalletId, network, currencies) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
networksDeferred.awaitAll()
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworksPendingTransactions(
|
||||
|
|
@ -222,26 +149,6 @@ internal class DefaultNetworksRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkStatusIfCacheExpired(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
currencies: Sequence<CryptoCurrency>,
|
||||
refresh: Boolean,
|
||||
): Deferred<Unit>? = coroutineScope {
|
||||
val key = getNetworksStatusesCacheKey(userWalletId, network)
|
||||
if (refresh || cacheRegistry.isExpired(key)) {
|
||||
async {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = key,
|
||||
skipCache = refresh,
|
||||
block = { fetchNetworkStatus(userWalletId, network, currencies) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkStatus(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.core.lce
|
||||
|
||||
import arrow.atomic.AtomicBoolean
|
||||
import arrow.core.raise.Raise
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
|
|
@ -34,6 +35,8 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
|||
private val ifLoading: suspend LceFlowScope<E, C>.(C?) -> Unit,
|
||||
) : Raise<E>, CoroutineScope by producerScope {
|
||||
|
||||
val isLoading: AtomicBoolean = AtomicBoolean(value = true)
|
||||
|
||||
/**
|
||||
* Sends a error of type [E] within the [ProducerScope] and then closes it for send.
|
||||
* All subsequent sends will be ignored.
|
||||
|
|
@ -46,6 +49,8 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
|||
* @param r Error to raise.
|
||||
*/
|
||||
override fun raise(r: E): Nothing {
|
||||
isLoading.set(false)
|
||||
|
||||
producerScope.trySendBlocking(r.lceError())
|
||||
producerScope.close()
|
||||
|
||||
|
|
@ -66,6 +71,8 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
|||
* @param isStillLoading A flag indicating whether the content is still loading.
|
||||
*/
|
||||
suspend fun send(content: C, isStillLoading: Boolean = false) {
|
||||
isLoading.set(isStillLoading)
|
||||
|
||||
val value = if (isStillLoading) {
|
||||
ifLoading(content)
|
||||
return
|
||||
|
|
@ -89,6 +96,8 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
|||
suspend fun send(value: Lce<E, C>) {
|
||||
if (producerScope.isClosedForSend) return
|
||||
|
||||
isLoading.set(value.isLoading())
|
||||
|
||||
producerScope.send(value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.domain.core.lce
|
||||
|
||||
import arrow.atomic.Atomic
|
||||
import arrow.core.Either
|
||||
import arrow.core.identity
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.RaiseDSL
|
||||
import arrow.core.raise.recover
|
||||
|
|
@ -97,6 +99,12 @@ class LceRaise<E : Any> @PublishedApi internal constructor(
|
|||
is Lce.Content -> content
|
||||
is Lce.Error -> raise(r = this)
|
||||
}
|
||||
|
||||
@RaiseDSL
|
||||
fun <C : Any> Either<E, C>.bindEither(): C = fold(
|
||||
ifLeft = { raise(it) },
|
||||
ifRight = ::identity,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ data class Yield(
|
|||
val isAvailable: Boolean,
|
||||
) {
|
||||
|
||||
val preferredValidators: List<Validator>
|
||||
get() = validators.filter { it.preferred }
|
||||
|
||||
fun getCurrentToken(rawCurrencyId: String?) = tokens.firstOrNull { rawCurrencyId == it.coinGeckoId } ?: token
|
||||
|
||||
@Serializable
|
||||
data class Status(
|
||||
val enter: Boolean,
|
||||
|
|
@ -130,8 +135,6 @@ data class Yield(
|
|||
APR, // simple rate
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
fun getCurrentToken(rawCurrencyId: String?) = tokens.firstOrNull { rawCurrencyId == it.coinGeckoId } ?: token
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
package com.tangem.domain.staking.model.stakekit.action
|
||||
|
||||
enum class StakingActionCommonType {
|
||||
ENTER,
|
||||
EXIT,
|
||||
PENDING_REWARDS,
|
||||
PENDING_OTHER,
|
||||
sealed class StakingActionCommonType {
|
||||
|
||||
data object Enter : StakingActionCommonType()
|
||||
data object Exit : StakingActionCommonType()
|
||||
sealed class Pending : StakingActionCommonType() {
|
||||
data object Restake : Pending()
|
||||
data object Rewards : Pending()
|
||||
data object Other : Pending()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.domain.staking.repositories
|
|||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
|
|
@ -50,16 +49,11 @@ interface StakingRepository {
|
|||
refresh: Boolean = false,
|
||||
)
|
||||
|
||||
fun getMultiYieldBalanceFlow(
|
||||
fun getMultiYieldBalanceUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): Flow<YieldBalanceList>
|
||||
|
||||
fun getMultiYieldBalanceLce(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): LceFlow<Throwable, YieldBalanceList>
|
||||
|
||||
suspend fun getMultiYieldBalanceSync(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ data class NetworkStatus(
|
|||
*/
|
||||
sealed class Value
|
||||
|
||||
/**
|
||||
* Represents the state where the network is refreshing.
|
||||
*/
|
||||
data object Refreshing : Value()
|
||||
|
||||
/**
|
||||
* Represents the state where the network is unreachable.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -40,38 +40,39 @@ class FetchTokenListUseCase(
|
|||
* network statuses, and quotes for associated tokens.
|
||||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param refresh Indicates whether to force a refresh of the token list data.
|
||||
* @param mode The refresh mode to control the fetching process.
|
||||
* @return An [Either] representing success (Right) or an error (Left) in fetching the token list.
|
||||
*/
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either<TokenListError, Unit> {
|
||||
return either {
|
||||
val currencies = fetchCurrencies(userWalletId, refresh)
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
mode: RefreshMode = RefreshMode.NONE,
|
||||
): Either<TokenListError, Unit> = either {
|
||||
val currencies = fetchCurrencies(userWalletId, refresh = mode.refreshCurrencies)
|
||||
|
||||
coroutineScope {
|
||||
val fetchStatuses = async {
|
||||
fetchNetworksStatuses(
|
||||
userWalletId,
|
||||
currencies.mapTo(hashSetOf()) { it.network },
|
||||
refresh,
|
||||
)
|
||||
}
|
||||
val fetchQuotes = async {
|
||||
fetchQuotes(
|
||||
currencies.mapTo(hashSetOf()) { it.id },
|
||||
refresh,
|
||||
)
|
||||
}
|
||||
|
||||
val yieldBalances = async {
|
||||
fetchYieldBalances(
|
||||
userWalletId = userWalletId,
|
||||
currencies = currencies,
|
||||
refresh = refresh,
|
||||
)
|
||||
}
|
||||
|
||||
awaitAll(fetchStatuses, fetchQuotes, yieldBalances)
|
||||
coroutineScope {
|
||||
val fetchStatuses = async {
|
||||
fetchNetworksStatuses(
|
||||
userWalletId,
|
||||
currencies.mapTo(hashSetOf()) { it.network },
|
||||
refresh = mode.refreshNetworksStatuses,
|
||||
)
|
||||
}
|
||||
val fetchQuotes = async {
|
||||
fetchQuotes(
|
||||
currencies.mapTo(hashSetOf()) { it.id },
|
||||
refresh = mode.refreshQuotes,
|
||||
)
|
||||
}
|
||||
|
||||
val yieldBalances = async {
|
||||
fetchYieldBalances(
|
||||
userWalletId = userWalletId,
|
||||
currencies = currencies,
|
||||
refresh = mode.refreshYieldBalances,
|
||||
)
|
||||
}
|
||||
|
||||
awaitAll(fetchStatuses, fetchQuotes, yieldBalances)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,4 +121,33 @@ class FetchTokenListUseCase(
|
|||
catch = { /* Ignore error */ },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the refresh modes available for fetching token list information.
|
||||
*/
|
||||
enum class RefreshMode(
|
||||
internal val refreshCurrencies: Boolean,
|
||||
internal val refreshNetworksStatuses: Boolean,
|
||||
internal val refreshQuotes: Boolean,
|
||||
internal val refreshYieldBalances: Boolean,
|
||||
) {
|
||||
NONE(
|
||||
refreshCurrencies = false,
|
||||
refreshNetworksStatuses = false,
|
||||
refreshQuotes = false,
|
||||
refreshYieldBalances = false,
|
||||
),
|
||||
FULL(
|
||||
refreshCurrencies = true,
|
||||
refreshNetworksStatuses = true,
|
||||
refreshQuotes = true,
|
||||
refreshYieldBalances = true,
|
||||
),
|
||||
SKIP_CURRENCIES(
|
||||
refreshCurrencies = false,
|
||||
refreshNetworksStatuses = true,
|
||||
refreshQuotes = true,
|
||||
refreshYieldBalances = true,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -85,9 +85,6 @@ class GetWalletTotalBalanceUseCase(
|
|||
stakingRepository = stakingRepository,
|
||||
)
|
||||
|
||||
return operations.getCurrenciesStatuses(
|
||||
userWalletId = userWalletId,
|
||||
isSingleCurrencyWalletsAllowed = true,
|
||||
)
|
||||
return operations.getCurrenciesStatuses(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -54,4 +54,13 @@ sealed class TokenList {
|
|||
enum class SortType {
|
||||
NONE, BALANCE,
|
||||
}
|
||||
|
||||
/** Get flatten list of cryptocurrency status [CryptoCurrencyStatus] */
|
||||
fun flattenCurrencies(): List<CryptoCurrencyStatus> {
|
||||
return when (this) {
|
||||
is GroupedByNetwork -> groups.flatMap(NetworkGroup::currencies)
|
||||
is Ungrouped -> currencies
|
||||
is Empty -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.domain.tokens.operations
|
||||
|
||||
import arrow.core.*
|
||||
import arrow.core.raise.ensureNotNull
|
||||
import arrow.core.raise.recover
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.lce.lce
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.core.lce.lceFlow
|
||||
import com.tangem.domain.core.utils.EitherFlow
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
|
|
@ -26,147 +27,126 @@ internal class CurrenciesStatusesLceOperations(
|
|||
private val stakingRepository: StakingRepository,
|
||||
) {
|
||||
|
||||
fun getCurrenciesStatuses(
|
||||
userWalletId: UserWalletId,
|
||||
isSingleCurrencyWalletsAllowed: Boolean = false,
|
||||
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> {
|
||||
fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrencyStatus>> {
|
||||
return transformToCurrenciesStatuses(
|
||||
userWalletId = userWalletId,
|
||||
flow = if (isSingleCurrencyWalletsAllowed) {
|
||||
getWalletCurrencies(userWalletId)
|
||||
} else {
|
||||
getMultiCurrencyWalletCurrencies(userWalletId)
|
||||
},
|
||||
currenciesFlow = getWalletCurrencies(userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun transformToCurrenciesStatuses(
|
||||
userWalletId: UserWalletId,
|
||||
flow: LceFlow<TokenListError, List<CryptoCurrency>>,
|
||||
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> {
|
||||
return flow.transformLatest transform@{ maybeCurrencies ->
|
||||
val nonEmptyCurrencies = maybeCurrencies.fold(
|
||||
ifLoading = { maybeContent ->
|
||||
emit(createLoadingCurrenciesStatuses(maybeContent))
|
||||
return@transform
|
||||
},
|
||||
ifContent = { content ->
|
||||
val nonEmptyCurrencies = content.toNonEmptyListOrNull()
|
||||
currenciesFlow: EitherFlow<TokenListError, List<CryptoCurrency>>,
|
||||
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> = lceFlow {
|
||||
currenciesFlow.collectLatest { maybeCurrencies ->
|
||||
val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull()
|
||||
ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens }
|
||||
|
||||
if (nonEmptyCurrencies == null) {
|
||||
emit(TokenListError.EmptyTokens.lceError())
|
||||
return@transform
|
||||
} else {
|
||||
nonEmptyCurrencies
|
||||
}
|
||||
},
|
||||
ifError = { error ->
|
||||
emit(error.lceError())
|
||||
return@transform
|
||||
},
|
||||
)
|
||||
// This is only 'true' when the flow here is empty, such as during initial loading
|
||||
if (isLoading.get()) {
|
||||
val loadingCurrencies = createCurrenciesStatuses(
|
||||
currencies = nonEmptyCurrencies,
|
||||
maybeNetworkStatuses = null,
|
||||
maybeQuotes = null,
|
||||
maybeYieldBalances = null,
|
||||
|
||||
)
|
||||
send(loadingCurrencies)
|
||||
}
|
||||
|
||||
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
|
||||
|
||||
fun createCurrenciesStatuses(
|
||||
maybeQuotes: Either<TokenListError, Set<Quote>>?,
|
||||
maybeNetworkStatuses: Either<TokenListError, Set<NetworkStatus>>?,
|
||||
maybeYieldBalances: Either<TokenListError, YieldBalanceList>?,
|
||||
): Lce<TokenListError, List<CryptoCurrencyStatus>> = createCurrenciesStatuses(
|
||||
currencies = nonEmptyCurrencies,
|
||||
maybeQuotes = maybeQuotes,
|
||||
maybeNetworkStatuses = maybeNetworkStatuses,
|
||||
maybeYieldBalances = maybeYieldBalances,
|
||||
)
|
||||
|
||||
combine(
|
||||
getQuotes(currenciesIds),
|
||||
getNetworksStatuses(userWalletId, networks),
|
||||
getYieldBalances(userWalletId, nonEmptyCurrencies),
|
||||
|
||||
) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances ->
|
||||
val statuses = createCurrenciesStatuses(
|
||||
currencies = nonEmptyCurrencies,
|
||||
maybeQuotes = maybeQuotes,
|
||||
maybeNetworkStatuses = maybeNetworksStatuses,
|
||||
maybeYieldBalances = maybeYieldBalances,
|
||||
)
|
||||
emit(statuses)
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLoadingCurrenciesStatuses(
|
||||
maybeCurrencies: List<CryptoCurrency>?,
|
||||
): Lce<TokenListError, List<CryptoCurrencyStatus>> {
|
||||
val nonEmptyCurrencies = maybeCurrencies?.toNonEmptyListOrNull()
|
||||
|
||||
val statuses = if (nonEmptyCurrencies == null) {
|
||||
lceLoading()
|
||||
} else {
|
||||
createCurrenciesStatuses(
|
||||
currencies = nonEmptyCurrencies,
|
||||
maybeNetworkStatuses = null,
|
||||
maybeQuotes = null,
|
||||
maybeYieldBalances = null,
|
||||
::createCurrenciesStatuses,
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { maybeCurrenciesStatuses ->
|
||||
send(maybeCurrenciesStatuses)
|
||||
}
|
||||
.launchIn(scope = this)
|
||||
}
|
||||
|
||||
return statuses
|
||||
}
|
||||
|
||||
private fun getWalletCurrencies(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrency>> {
|
||||
private fun getWalletCurrencies(userWalletId: UserWalletId): EitherFlow<TokenListError, List<CryptoCurrency>> {
|
||||
return currenciesRepository.getWalletCurrenciesUpdates(userWalletId)
|
||||
.map { maybeCurrencies ->
|
||||
maybeCurrencies.mapError { TokenListError.DataError(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMultiCurrencyWalletCurrencies(
|
||||
userWalletId: UserWalletId,
|
||||
): LceFlow<TokenListError, List<CryptoCurrency>> {
|
||||
return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId)
|
||||
.map<List<CryptoCurrency>, Either<TokenListError, List<CryptoCurrency>>> { it.right() }
|
||||
.catch { emit(TokenListError.DataError(it).left()) }
|
||||
.distinctUntilChanged()
|
||||
.map { maybeCurrencies ->
|
||||
maybeCurrencies.mapError { TokenListError.DataError(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCurrenciesStatuses(
|
||||
currencies: NonEmptyList<CryptoCurrency>,
|
||||
maybeQuotes: Either<TokenListError, Set<Quote>>?,
|
||||
maybeNetworkStatuses: Lce<TokenListError, Set<NetworkStatus>>?,
|
||||
maybeYieldBalances: Lce<TokenListError, YieldBalanceList>?,
|
||||
maybeNetworkStatuses: Either<TokenListError, Set<NetworkStatus>>?,
|
||||
maybeYieldBalances: Either<TokenListError, YieldBalanceList>?,
|
||||
): Lce<TokenListError, List<CryptoCurrencyStatus>> = lce {
|
||||
isLoading.set(maybeNetworkStatuses == null)
|
||||
isLoading.set(maybeNetworkStatuses == null || maybeYieldBalances == null)
|
||||
|
||||
var quotesRetrievingFailed = false
|
||||
|
||||
val networksStatuses = maybeNetworkStatuses?.bindOrNull()?.toNonEmptySetOrNull()
|
||||
val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull()
|
||||
val yieldBalances = maybeYieldBalances?.bindEither()
|
||||
val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) {
|
||||
quotesRetrievingFailed = true
|
||||
null
|
||||
}?.ifEmpty {
|
||||
quotesRetrievingFailed = true
|
||||
null
|
||||
}
|
||||
|
||||
val yieldBalances = maybeYieldBalances?.getOrNull()
|
||||
if (quotes == null) {
|
||||
quotesRetrievingFailed = true
|
||||
}
|
||||
|
||||
currencies.map { currency ->
|
||||
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
|
||||
val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network }
|
||||
val address = extractAddress(networkStatus)
|
||||
val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id)
|
||||
val yieldBalance = if (supportedIntegration.isNullOrBlank().not()) {
|
||||
(yieldBalances as? YieldBalanceList.Data)?.getBalance(
|
||||
address = address,
|
||||
integrationId = supportedIntegration,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus)
|
||||
|
||||
createCurrencyStatus(
|
||||
val currencyStatus = createCurrencyStatus(
|
||||
currency = currency,
|
||||
quote = quote,
|
||||
networkStatus = networkStatus,
|
||||
yieldBalance = yieldBalance,
|
||||
ignoreQuote = quotesRetrievingFailed,
|
||||
)
|
||||
|
||||
if (currencyStatus.value is CryptoCurrencyStatus.Loading) {
|
||||
isLoading.set(true)
|
||||
}
|
||||
|
||||
currencyStatus
|
||||
}
|
||||
}
|
||||
|
||||
private fun findYieldBalanceOrNull(
|
||||
yieldBalances: YieldBalanceList?,
|
||||
currency: CryptoCurrency,
|
||||
networkStatus: NetworkStatus?,
|
||||
): YieldBalance? {
|
||||
if (yieldBalances !is YieldBalanceList.Data) return null
|
||||
|
||||
val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id)
|
||||
|
||||
if (supportedIntegration.isNullOrBlank()) return null
|
||||
|
||||
return yieldBalances.getBalance(
|
||||
address = extractAddress(networkStatus),
|
||||
integrationId = supportedIntegration,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCurrencyStatus(
|
||||
currency: CryptoCurrency,
|
||||
quote: Quote?,
|
||||
|
|
@ -189,28 +169,27 @@ internal class CurrenciesStatusesLceOperations(
|
|||
return quotesRepository.getQuotesUpdates(tokensIds)
|
||||
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
|
||||
.catch { emit(TokenListError.DataError(it).left()) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun getNetworksStatuses(
|
||||
userWalletId: UserWalletId,
|
||||
networks: NonEmptySet<Network>,
|
||||
): LceFlow<TokenListError, Set<NetworkStatus>> {
|
||||
return networksRepository.getNetworkStatusesUpdatesLce(userWalletId, networks)
|
||||
.map { maybeStatuses ->
|
||||
maybeStatuses.mapError { TokenListError.DataError(it) }
|
||||
}
|
||||
): EitherFlow<TokenListError, Set<NetworkStatus>> {
|
||||
return networksRepository.getNetworkStatusesUpdates(userWalletId, networks)
|
||||
.map<Set<NetworkStatus>, Either<TokenListError, Set<NetworkStatus>>> { it.right() }
|
||||
.catch { emit(TokenListError.DataError(it).left()) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun getYieldBalances(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): LceFlow<TokenListError, YieldBalanceList> {
|
||||
return stakingRepository.getMultiYieldBalanceLce(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencies = cryptoCurrencies,
|
||||
).map { maybeBalances ->
|
||||
maybeBalances.mapError { TokenListError.DataError(it) }
|
||||
}
|
||||
): EitherFlow<TokenListError, YieldBalanceList> {
|
||||
return stakingRepository.getMultiYieldBalanceUpdates(userWalletId, cryptoCurrencies)
|
||||
.map<YieldBalanceList, Either<TokenListError, YieldBalanceList>> { it.right() }
|
||||
.catch { emit(TokenListError.DataError(it).left()) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun getIds(currencies: List<CryptoCurrency>): Pair<NonEmptySet<Network>, NonEmptySet<CryptoCurrency.ID>> {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ internal class CurrencyStatusOperations(
|
|||
|
||||
private fun createStatus(): CryptoCurrencyStatus.Value {
|
||||
return when (val status = networkStatus?.value) {
|
||||
null -> CryptoCurrencyStatus.Loading
|
||||
null,
|
||||
is NetworkStatus.Refreshing,
|
||||
-> CryptoCurrencyStatus.Loading
|
||||
is NetworkStatus.MissedDerivation -> createMissedDerivationStatus()
|
||||
is NetworkStatus.Unreachable -> createUnreachableStatus(status)
|
||||
is NetworkStatus.NoAccount -> createNoAccountStatus(status)
|
||||
|
|
|
|||
|
|
@ -23,11 +23,7 @@ internal class TokenListSortingOperations(
|
|||
sortByBalance: Boolean = tokenList.sortedBy == TokenList.SortType.BALANCE,
|
||||
isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TotalFiatBalance.Loading,
|
||||
) : this(
|
||||
currencies = when (tokenList) {
|
||||
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies }
|
||||
is TokenList.Ungrouped -> tokenList.currencies
|
||||
is TokenList.Empty -> emptyList()
|
||||
},
|
||||
currencies = tokenList.flattenCurrencies(),
|
||||
isAnyTokenLoading = isAnyTokenLoading,
|
||||
sortByBalance = sortByBalance,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||
|
|
@ -81,7 +80,7 @@ interface CurrenciesRepository {
|
|||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @return A list of [CryptoCurrency].
|
||||
*/
|
||||
fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow<Throwable, List<CryptoCurrency>>
|
||||
fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>>
|
||||
|
||||
/**
|
||||
* Retrieves the primary cryptocurrency for a specific single-currency user wallet.
|
||||
|
|
@ -130,17 +129,6 @@ interface CurrenciesRepository {
|
|||
*/
|
||||
fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>>
|
||||
|
||||
/**
|
||||
* Retrieves updates of the list of cryptocurrencies within a multi-currency wallet.
|
||||
*
|
||||
* Loads remote cryptocurrencies if they have expired.
|
||||
*
|
||||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @return A [LceFlow] emitting the set of cryptocurrencies associated with the user wallet. May emit an
|
||||
* [DataError.UserWalletError.WrongUserWallet] if single-currency user wallet ID provided.
|
||||
*/
|
||||
fun getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId: UserWalletId): LceFlow<Throwable, List<CryptoCurrency>>
|
||||
|
||||
/**
|
||||
* Retrieves the list of cryptocurrencies within a multi-currency wallet.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
|
|
@ -23,20 +21,6 @@ interface NetworksRepository {
|
|||
*/
|
||||
fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>>
|
||||
|
||||
/**
|
||||
* Retrieves updates of network statuses of specified blockchain networks for a specific user wallet.
|
||||
*
|
||||
* Loads remote network statuses if they have expired.
|
||||
*
|
||||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @param networks A set of network which statuses are to be retrieved.
|
||||
* @return A [LceFlow] emitting a set of [NetworkStatus] objects corresponding to the specified networks.
|
||||
*/
|
||||
fun getNetworkStatusesUpdatesLce(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
): LceFlow<Throwable, Set<NetworkStatus>>
|
||||
|
||||
/**
|
||||
* Fetches pending transactions for given network
|
||||
*
|
||||
|
|
@ -63,33 +47,8 @@ interface NetworksRepository {
|
|||
|
||||
fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean
|
||||
|
||||
/**
|
||||
* Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId]
|
||||
*/
|
||||
fun getNetworkAddressesFlow(userWalletId: UserWalletId, network: Network): Flow<List<CryptoCurrencyAddress>>
|
||||
|
||||
/**
|
||||
* Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId]
|
||||
*/
|
||||
suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List<CryptoCurrencyAddress>
|
||||
|
||||
/**
|
||||
* Returns address of [cryptoCurrency] in selected wallet [userWalletId]
|
||||
*/
|
||||
suspend fun getNetworkAddress(userWalletId: UserWalletId, currency: CryptoCurrency): CryptoCurrencyAddress
|
||||
|
||||
/**
|
||||
* Returns address of [cryptoCurrency] in selected wallet [userWalletId]
|
||||
*/
|
||||
fun getNetworkAddressFlow(userWalletId: UserWalletId, currency: CryptoCurrency): Flow<CryptoCurrencyAddress>
|
||||
|
||||
/**
|
||||
* Returns list of addresses and crypto currency info in selected wallet [userWalletId]
|
||||
*/
|
||||
fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>>
|
||||
|
||||
/**
|
||||
* Returns list of addresses and crypto currency info in selected wallet [userWalletId]
|
||||
*/
|
||||
suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress>
|
||||
}
|
||||
|
|
@ -3,8 +3,6 @@ package com.tangem.domain.tokens.repository
|
|||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.utils.toLce
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||
|
|
@ -57,7 +55,7 @@ internal class MockCurrenciesRepository(
|
|||
|
||||
override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
|
||||
|
||||
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow<Throwable, List<CryptoCurrency>> {
|
||||
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
|
||||
return emptyFlow()
|
||||
}
|
||||
|
||||
|
|
@ -91,12 +89,6 @@ internal class MockCurrenciesRepository(
|
|||
return tokens.map { it.getOrElse { e -> throw e } }
|
||||
}
|
||||
|
||||
override fun getMultiCurrencyWalletCurrenciesUpdatesLce(
|
||||
userWalletId: UserWalletId,
|
||||
): LceFlow<Throwable, List<CryptoCurrency>> {
|
||||
return tokens.map { it.toLce() }
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@ package com.tangem.domain.tokens.repository
|
|||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.utils.toLce
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
|
|
@ -26,13 +22,6 @@ internal class MockNetworksRepository(
|
|||
return statuses.map { it.getOrElse { e -> throw e } }
|
||||
}
|
||||
|
||||
override fun getNetworkStatusesUpdatesLce(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
): LceFlow<Throwable, Set<NetworkStatus>> {
|
||||
return statuses.map { it.toLce() }
|
||||
}
|
||||
|
||||
override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
|
||||
// no-op
|
||||
}
|
||||
|
|
@ -47,37 +36,10 @@ internal class MockNetworksRepository(
|
|||
|
||||
override fun isNeedToCreateAccountWithoutReserve(network: Network) = false
|
||||
|
||||
override fun getNetworkAddressesFlow(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): Flow<List<CryptoCurrencyAddress>> = channelFlow {
|
||||
send(emptyList())
|
||||
}
|
||||
|
||||
override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>> = channelFlow {
|
||||
send(emptyList())
|
||||
}
|
||||
|
||||
override suspend fun getNetworkAddresses(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): List<CryptoCurrencyAddress> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun getNetworkAddress(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): CryptoCurrencyAddress = CryptoCurrencyAddress(currency, "")
|
||||
|
||||
override fun getNetworkAddressFlow(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): Flow<CryptoCurrencyAddress> = channelFlow {
|
||||
send(CryptoCurrencyAddress(currency, ""))
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ import com.tangem.blockchain.common.Amount
|
|||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionStatus
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.lce.lceFlow
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
|
|
@ -20,6 +18,7 @@ import com.tangem.domain.tokens.model.Network
|
|||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -146,27 +145,10 @@ class MockStakingRepository : StakingRepository {
|
|||
/* no-op */
|
||||
}
|
||||
|
||||
override fun getMultiYieldBalanceFlow(
|
||||
override fun getMultiYieldBalanceUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): Flow<YieldBalanceList> = channelFlow {
|
||||
send(
|
||||
YieldBalanceList.Data(
|
||||
balances = listOf(YieldBalance.Error),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMultiYieldBalanceLce(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
|
||||
send(
|
||||
YieldBalanceList.Data(
|
||||
balances = listOf(YieldBalance.Error),
|
||||
),
|
||||
)
|
||||
}
|
||||
): Flow<YieldBalanceList> = flowOf()
|
||||
|
||||
override suspend fun getMultiYieldBalanceSync(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import com.tangem.blockchain.extensions.Result
|
|||
import com.tangem.blockchain.network.ResultChecker
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.simple
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
|
|
@ -24,7 +26,6 @@ import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_C
|
|||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.sdk.extensions.localizedDescriptionRes
|
||||
import com.tangem.utils.toFormattedString
|
||||
|
||||
class SendTransactionUseCase(
|
||||
private val demoConfig: DemoConfig,
|
||||
|
|
@ -120,7 +121,7 @@ class SendTransactionUseCase(
|
|||
is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error)
|
||||
is BlockchainSdkError.CreateAccountUnderfunded -> {
|
||||
val minAmount = error.minReserve
|
||||
val minValue = minAmount.value?.toFormattedString(minAmount.decimals).orEmpty()
|
||||
val minValue = minAmount.value?.format { simple(minAmount.decimals) }.orEmpty()
|
||||
SendTransactionError.CreateAccountUnderfunded(minValue)
|
||||
}
|
||||
else -> {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.update
|
|||
|
||||
internal class PreviewManageTokensComponent(
|
||||
private val isLoading: Boolean,
|
||||
private val showTangemIcon: Boolean,
|
||||
params: ManageTokensComponent.Params,
|
||||
) : ManageTokensComponent {
|
||||
|
||||
|
|
@ -65,6 +66,7 @@ internal class PreviewManageTokensComponent(
|
|||
loadMore = { false },
|
||||
saveChanges = {},
|
||||
isSavingInProgress = false,
|
||||
needToAddDerivations = showTangemIcon,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ internal data class CustomTokenFormUM(
|
|||
val notifications: PersistentList<NotificationUM> = persistentListOf(),
|
||||
val canAddToken: Boolean = false,
|
||||
val isValidating: Boolean = false,
|
||||
val needToAddDerivation: Boolean = false,
|
||||
val saveToken: () -> Unit,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ internal sealed class ManageTokensUM {
|
|||
val saveChanges: () -> Unit,
|
||||
val hasChanges: Boolean,
|
||||
val isSavingInProgress: Boolean,
|
||||
val needToAddDerivations: Boolean,
|
||||
) : ManageTokensUM()
|
||||
|
||||
fun copySealed(
|
||||
|
|
@ -52,6 +53,7 @@ internal sealed class ManageTokensUM {
|
|||
isNextBatchLoading: Boolean = this.isNextBatchLoading,
|
||||
isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress,
|
||||
scrollToTop: StateEvent<Unit> = this.scrollToTop,
|
||||
needToAddDerivations: Boolean = this is ManageContent && this.needToAddDerivations,
|
||||
): ManageTokensUM {
|
||||
return when (this) {
|
||||
is ManageContent -> copy(
|
||||
|
|
@ -62,6 +64,7 @@ internal sealed class ManageTokensUM {
|
|||
isNextBatchLoading = isNextBatchLoading,
|
||||
isSavingInProgress = isSavingInProgress,
|
||||
scrollToTop = scrollToTop,
|
||||
needToAddDerivations = needToAddDerivations,
|
||||
)
|
||||
is ReadContent -> copy(
|
||||
search = search,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.ContentMessage
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.card.HasMissedDerivationsUseCase
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -43,6 +44,7 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
private val customCurrencyValidator: CustomCurrencyValidator,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val customTokenFormManager: CustomCurrencyFormBuilder,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
|
|
@ -159,7 +161,12 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
fillForm: Boolean,
|
||||
isAlreadyAdded: Boolean,
|
||||
isCustom: Boolean,
|
||||
) {
|
||||
) = modelScope.launch {
|
||||
val needToAddDerivation = hasMissedDerivationsUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value),
|
||||
)
|
||||
|
||||
state.update { state ->
|
||||
var updatedState = state
|
||||
.updateWithProgress(
|
||||
|
|
@ -169,6 +176,7 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
clearNotifications = true,
|
||||
clearFieldErrors = true,
|
||||
disableSecondaryFields = !isCustom,
|
||||
needToAddDerivation = needToAddDerivation,
|
||||
)
|
||||
|
||||
if (fillForm) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.tangem.core.ui.event.triggeredEvent
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.card.HasMissedDerivationsUseCase
|
||||
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
|
||||
|
|
@ -47,6 +48,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val manageTokensListManager: ManageTokensListManager,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
|
||||
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
paramsContainer: ParamsContainer,
|
||||
|
|
@ -140,6 +142,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
hasChanges = false,
|
||||
saveChanges = ::saveChanges,
|
||||
loadMore = ::loadMoreItems,
|
||||
needToAddDerivations = false,
|
||||
isSavingInProgress = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -255,10 +258,22 @@ internal class ManageTokensModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) {
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(),
|
||||
)
|
||||
modelScope.launch {
|
||||
val hasMissedDerivations = params.userWalletId?.let { walletId ->
|
||||
val networks = currenciesToAdd.values
|
||||
.flatten()
|
||||
.toSet()
|
||||
.associate { it.backendId to null }
|
||||
|
||||
hasMissedDerivationsUseCase(walletId, networks)
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(),
|
||||
needToAddDerivations = hasMissedDerivations ?: false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.bottomFade
|
||||
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.fields.SimpleTextField
|
||||
import com.tangem.core.ui.components.isOpened
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
|
|
@ -82,14 +84,22 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier
|
|||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = TangemTheme.dimens.spacing16 + bottomBarHeight)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.custom_token_add_token),
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
enabled = model.canAddToken,
|
||||
showProgress = model.isValidating,
|
||||
animateContentChange = true,
|
||||
icon = if (model.needToAddDerivation) {
|
||||
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
},
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
onClick = model.saveToken,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,11 +33,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
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.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
|
|
@ -122,6 +128,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
|
|||
.fillMaxWidth(),
|
||||
isVisible = state.hasChanges,
|
||||
showProgress = state.isSavingInProgress,
|
||||
showIcon = state.needToAddDerivations,
|
||||
onClick = state.saveChanges,
|
||||
)
|
||||
}
|
||||
|
|
@ -158,6 +165,7 @@ private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarU
|
|||
private fun SaveChangesButton(
|
||||
isVisible: Boolean,
|
||||
showProgress: Boolean,
|
||||
showIcon: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -168,10 +176,18 @@ private fun SaveChangesButton(
|
|||
exit = fadeOut(),
|
||||
label = "save_button_visibility",
|
||||
) {
|
||||
PrimaryButtonIconEnd(
|
||||
TangemButton(
|
||||
text = stringResource(id = R.string.common_save),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
icon = if (showIcon) {
|
||||
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
},
|
||||
showProgress = showProgress,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
enabled = true,
|
||||
animateContentChange = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -472,6 +488,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
|
|||
get() = sequenceOf(
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = true,
|
||||
showTangemIcon = true,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("wallet_id"),
|
||||
|
|
@ -479,10 +496,12 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
|
|||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
showTangemIcon = true,
|
||||
params = ManageTokensComponent.Params(source = ManageTokensSource.ONBOARDING, userWalletId = null),
|
||||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
showTangemIcon = false,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("wallet_id"),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal fun CustomTokenFormUM.updateWithProgress(
|
|||
showProgress: Boolean,
|
||||
isWasFilled: Boolean = this.tokenForm?.wasFilled ?: false,
|
||||
canAddToken: Boolean = this.canAddToken,
|
||||
needToAddDerivation: Boolean = false,
|
||||
clearNotifications: Boolean = false,
|
||||
clearFieldErrors: Boolean = false,
|
||||
disableSecondaryFields: Boolean = false,
|
||||
|
|
@ -34,6 +35,7 @@ internal fun CustomTokenFormUM.updateWithProgress(
|
|||
return copy(
|
||||
isValidating = showProgress,
|
||||
canAddToken = canAddToken,
|
||||
needToAddDerivation = needToAddDerivation,
|
||||
notifications = if (clearNotifications) persistentListOf() else notifications,
|
||||
).updateTokenForm {
|
||||
val updatedFields = fields.mapValues { (key, field) ->
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
|
|||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
|
@ -172,12 +174,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
),
|
||||
dateTimeText = resourceReference(R.string.common_today),
|
||||
priceChangePercentText = params.token.tokenQuotes.h24Percent?.let {
|
||||
BigDecimalFormatter.formatPercent(
|
||||
percent = it,
|
||||
useAbsoluteValue = true,
|
||||
)
|
||||
},
|
||||
priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() },
|
||||
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
|
||||
iconUrl = params.token.imageUrl,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
|
|
@ -472,12 +469,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
)
|
||||
} ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval)
|
||||
|
||||
val percentText = percent?.let {
|
||||
BigDecimalFormatter.formatPercent(
|
||||
percent = it,
|
||||
useAbsoluteValue = true,
|
||||
)
|
||||
} ?: ""
|
||||
val percentText = percent?.format { percent() } ?: ""
|
||||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ package com.tangem.features.markets.details.impl.model.converters
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.rawCompact
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
|
|
@ -142,14 +145,17 @@ internal class InsightsConverter(
|
|||
|
||||
private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String {
|
||||
val value = if (isFiatValue) {
|
||||
val currency = appCurrency()
|
||||
BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this.abs(),
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
this.abs().format {
|
||||
val currency = appCurrency()
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).compact()
|
||||
}
|
||||
} else {
|
||||
BigDecimalFormatter.formatCompactAmount(amount = this.abs())
|
||||
this.abs().format {
|
||||
rawCompact()
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package com.tangem.features.markets.details.impl.model.converters
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
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.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
|
|
@ -129,18 +132,21 @@ internal class MetricsConverter(
|
|||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
|
||||
return if (crypto) {
|
||||
BigDecimalFormatter.formatCompactCryptoAmount(
|
||||
amount = this,
|
||||
cryptoCurrencySymbol = tokenSymbol,
|
||||
)
|
||||
format {
|
||||
crypto(
|
||||
symbol = tokenSymbol,
|
||||
decimals = 2,
|
||||
).compact()
|
||||
}
|
||||
} else {
|
||||
val currency = appCurrency()
|
||||
|
||||
BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).compact()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package com.tangem.features.markets.details.impl.model.formatter
|
|||
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
|
|
@ -28,12 +30,7 @@ internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInte
|
|||
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
|
||||
}
|
||||
|
||||
return percent?.let {
|
||||
BigDecimalFormatter.formatPercent(
|
||||
percent = it,
|
||||
useAbsoluteValue = true,
|
||||
)
|
||||
} ?: ""
|
||||
return percent?.format { percent() } ?: ""
|
||||
}
|
||||
|
||||
internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SmallButtonShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
|
|
@ -76,10 +78,26 @@ private fun Title() {
|
|||
private fun AddButton(state: AddButtonState, onClick: () -> Unit) {
|
||||
when (state) {
|
||||
AddButtonState.Loading -> {
|
||||
SmallButtonShimmer(
|
||||
modifier = Modifier.size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18),
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius3),
|
||||
)
|
||||
Box {
|
||||
SmallButtonShimmer(
|
||||
modifier = Modifier.width(width = TangemTheme.dimens.size63),
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius3),
|
||||
withIcon = true,
|
||||
)
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
AddButtonState.Available,
|
||||
AddButtonState.Unavailable,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
|
@ -91,10 +93,7 @@ internal class TokenMarketBlockModel @Inject constructor(
|
|||
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
),
|
||||
h24Percent = BigDecimalFormatter.formatPercent(
|
||||
percent = res.priceChange,
|
||||
useAbsoluteValue = true,
|
||||
),
|
||||
h24Percent = res.priceChange.format { percent() },
|
||||
priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
|
|||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
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.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
|
|
@ -71,12 +75,14 @@ internal class MarketsTokenItemConverter(
|
|||
private fun TokenMarket.getMarketCap(): String? {
|
||||
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
|
||||
|
||||
return BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = value,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
return value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).compact(
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
|
|
@ -144,9 +150,6 @@ internal class MarketsTokenItemConverter(
|
|||
TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
|
||||
}
|
||||
|
||||
return BigDecimalFormatter.formatPercent(
|
||||
percent = percent,
|
||||
useAbsoluteValue = true,
|
||||
)
|
||||
return percent.format { percent() }
|
||||
}
|
||||
}
|
||||
1
features/onramp/api/.gitignore
vendored
Normal file
1
features/onramp/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
16
features/onramp/api/build.gradle.kts
Normal file
16
features/onramp/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("kotlin-parcelize")
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.onramp.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.onramp
|
||||
|
||||
interface OnrampFeatureToggles {
|
||||
|
||||
val isFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.onramp.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface OnrampComponent : ComposableContentComponent {
|
||||
|
||||
interface Factory : ComponentFactory<Unit, OnrampComponent>
|
||||
}
|
||||
1
features/onramp/impl/.gitignore
vendored
Normal file
1
features/onramp/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
38
features/onramp/impl/build.gradle.kts
Normal file
38
features/onramp/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.onramp.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - API */
|
||||
implementation(projects.features.onramp.api)
|
||||
|
||||
/** Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.featuretoggles)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/* AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.lifecycle.compose)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.shimmer)
|
||||
implementation(deps.compose.coil)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.onramp
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
|
||||
internal class DefaultOnrampFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : OnrampFeatureToggles {
|
||||
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("ONRAMP_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.onramp.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.onramp.DefaultOnrampFeatureToggles
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeatureModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): OnrampFeatureToggles {
|
||||
return DefaultOnrampFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import com.tangem.common.extensions.isZero
|
|||
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.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
|
|
@ -17,7 +19,6 @@ import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_K
|
|||
import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
|
|
@ -76,10 +77,7 @@ internal class SendRecipientHistoryListConverter(
|
|||
}
|
||||
|
||||
private fun TxHistoryItem.getAmount(cryptoCurrency: CryptoCurrency): String {
|
||||
return amount.toFormattedCurrencyString(
|
||||
currency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
return amount.format { crypto(cryptoCurrency) }
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractTimestamp(): TextReference {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ 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.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
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.features.send.impl.presentation.state.SendStates
|
||||
|
|
@ -228,12 +231,14 @@ private fun SendStates.FeeState.getFiatValue() = if (isFeeConvertibleToFiat) {
|
|||
)
|
||||
} else {
|
||||
val amount = fee?.amount
|
||||
BigDecimalFormatter.formatCryptoFeeAmount(
|
||||
cryptoAmount = amount?.value,
|
||||
cryptoCurrency = amount?.currencySymbol.orEmpty(),
|
||||
decimals = amount?.decimals ?: 0,
|
||||
canBeLower = isFeeApproximate,
|
||||
)
|
||||
amount?.value.format {
|
||||
crypto(
|
||||
decimals = amount?.decimals ?: 0,
|
||||
symbol = amount?.currencySymbol.orEmpty(),
|
||||
).fee(
|
||||
canBeLower = isFeeApproximate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getButtonData(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import com.tangem.core.ui.components.RectangleShimmer
|
|||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.rows.SelectorRowItem
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
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.parseToBigDecimal
|
||||
|
|
@ -53,12 +56,12 @@ internal fun SendSpeedSelectorItem(
|
|||
onSelect = onSelect,
|
||||
modifier = modifier,
|
||||
preDot = stringReference(
|
||||
BigDecimalFormatter.formatCryptoFeeAmount(
|
||||
cryptoAmount = amount?.value,
|
||||
cryptoCurrency = amount?.currencySymbol.orEmpty(),
|
||||
decimals = amount?.decimals ?: 0,
|
||||
canBeLower = state.isFeeApproximate,
|
||||
),
|
||||
amount?.value.format {
|
||||
crypto(
|
||||
symbol = amount?.currencySymbol.orEmpty(),
|
||||
decimals = amount?.decimals ?: 0,
|
||||
).fee(canBeLower = state.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (state.isFeeConvertibleToFiat) {
|
||||
getFiatReference(amount?.value, state.rate, state.appCurrency)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
|||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.rows.SelectorRowItem
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
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
|
||||
|
|
@ -62,12 +65,12 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o
|
|||
titleRes = title,
|
||||
iconRes = icon,
|
||||
preDot = stringReference(
|
||||
BigDecimalFormatter.formatCryptoFeeAmount(
|
||||
cryptoAmount = feeAmount?.value,
|
||||
cryptoCurrency = feeAmount?.currencySymbol.orEmpty(),
|
||||
decimals = feeAmount?.decimals ?: 0,
|
||||
canBeLower = feeState.isFeeApproximate,
|
||||
),
|
||||
feeAmount?.value.format {
|
||||
crypto(
|
||||
symbol = feeAmount?.currencySymbol.orEmpty(),
|
||||
decimals = feeAmount?.decimals ?: 0,
|
||||
).fee(canBeLower = feeState.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (feeState.isFeeConvertibleToFiat) {
|
||||
getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency)
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ internal class StakingAnalyticSender(
|
|||
|
||||
fun confirmationScreen(value: StakingUiState) {
|
||||
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val validatorState = confirmationState?.validatorState as? ValidatorState.Content
|
||||
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val validatorName = validatorState?.chosenValidator?.name ?: return
|
||||
|
||||
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) return
|
||||
if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return
|
||||
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvents.ConfirmationScreenOpened(
|
||||
|
|
@ -55,6 +55,7 @@ internal class StakingAnalyticSender(
|
|||
StakingStep.Amount -> StakeScreenSource.Amount
|
||||
StakingStep.Confirmation -> StakeScreenSource.Confirmation
|
||||
StakingStep.Validators,
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.RewardsValidators,
|
||||
-> StakeScreenSource.Validators
|
||||
},
|
||||
|
|
@ -78,8 +79,7 @@ internal class StakingAnalyticSender(
|
|||
}
|
||||
|
||||
fun sendTransactionStakingAnalytics(value: StakingUiState) {
|
||||
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val validatorState = confirmationState?.validatorState as? ValidatorState.Content
|
||||
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val validatorName = validatorState?.chosenValidator?.name ?: return
|
||||
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -102,8 +102,7 @@ internal class StakingAnalyticSender(
|
|||
}
|
||||
|
||||
fun sendTransactionStakingClickedAnalytics(value: StakingUiState) {
|
||||
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val validatorState = confirmationState?.validatorState as? ValidatorState.Content
|
||||
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val validatorName = validatorState?.chosenValidator?.name ?: return
|
||||
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -119,11 +118,9 @@ internal class StakingAnalyticSender(
|
|||
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
|
||||
return when (value.actionType) {
|
||||
StakingActionCommonType.ENTER -> StakingActionType.STAKE
|
||||
StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE
|
||||
StakingActionCommonType.PENDING_REWARDS,
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
-> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN
|
||||
StakingActionCommonType.Enter -> StakingActionType.STAKE
|
||||
StakingActionCommonType.Exit -> StakingActionType.UNSTAKE
|
||||
is StakingActionCommonType.Pending -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,12 @@ internal class StakingStateController @Inject constructor(
|
|||
mutableUiState.update(function = titleTransformer::transform)
|
||||
}
|
||||
|
||||
fun updateAll(vararg transformer: Transformer<StakingUiState>) {
|
||||
transformer.forEach { mutableUiState.update(function = it::transform) }
|
||||
mutableUiState.update(function = buttonsTransformer::transform)
|
||||
mutableUiState.update(function = titleTransformer::transform)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
mutableUiState.update { getInitialState() }
|
||||
mutableUiState.update(function = buttonsTransformer::transform)
|
||||
|
|
@ -72,12 +78,13 @@ internal class StakingStateController @Inject constructor(
|
|||
currentStep = StakingStep.InitialInfo,
|
||||
initialInfoState = StakingStates.InitialInfoState.Empty(),
|
||||
amountState = AmountState.Empty(),
|
||||
validatorState = StakingStates.ValidatorState.Empty(),
|
||||
rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(),
|
||||
confirmationState = StakingStates.ConfirmationState.Empty(),
|
||||
isBalanceHidden = false,
|
||||
event = consumedEvent(),
|
||||
bottomSheetConfig = null,
|
||||
actionType = StakingActionCommonType.ENTER,
|
||||
actionType = StakingActionCommonType.Enter,
|
||||
buttonsState = NavigationButtonsState.Empty,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,12 +23,14 @@ internal class StakingStateRouter(
|
|||
fun onNextClick() {
|
||||
when (stateController.value.currentStep) {
|
||||
StakingStep.InitialInfo -> when (stateController.value.actionType) {
|
||||
StakingActionCommonType.ENTER -> showAmount()
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
StakingActionCommonType.EXIT,
|
||||
StakingActionCommonType.Enter -> showAmount()
|
||||
StakingActionCommonType.Pending.Other,
|
||||
StakingActionCommonType.Exit,
|
||||
StakingActionCommonType.Pending.Rewards,
|
||||
-> showConfirmation()
|
||||
StakingActionCommonType.PENDING_REWARDS -> showRewardsValidators()
|
||||
StakingActionCommonType.Pending.Restake -> showRestakeValidators()
|
||||
}
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.RewardsValidators,
|
||||
StakingStep.Validators,
|
||||
StakingStep.Amount,
|
||||
|
|
@ -41,25 +43,31 @@ internal class StakingStateRouter(
|
|||
val uiState = stateController.uiState.value
|
||||
when (uiState.currentStep) {
|
||||
StakingStep.InitialInfo -> onBackClick()
|
||||
StakingStep.Amount -> showInitial()
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.RewardsValidators,
|
||||
StakingStep.Amount,
|
||||
-> showInitial()
|
||||
StakingStep.Confirmation -> {
|
||||
if (uiState.actionType != StakingActionCommonType.ENTER) {
|
||||
if (uiState.actionType != StakingActionCommonType.Enter) {
|
||||
showInitial()
|
||||
} else {
|
||||
showAmount()
|
||||
}
|
||||
}
|
||||
StakingStep.Validators -> showConfirmation()
|
||||
StakingStep.RewardsValidators -> showInitial()
|
||||
}
|
||||
}
|
||||
|
||||
fun showValidators() {
|
||||
stateController.update { it.copy(currentStep = StakingStep.Validators) }
|
||||
}
|
||||
|
||||
private fun showInitial() {
|
||||
analyticSender.initialInfoScreen(stateController.value)
|
||||
stateController.update { it.copy(currentStep = StakingStep.InitialInfo) }
|
||||
}
|
||||
|
||||
private fun showRewardsValidators() {
|
||||
fun showRewardsValidators() {
|
||||
analyticsEventsHandler.send(
|
||||
StakingAnalyticsEvents.RewardScreenOpened(stateController.value.cryptoCurrencySymbol),
|
||||
)
|
||||
|
|
@ -73,8 +81,8 @@ internal class StakingStateRouter(
|
|||
stateController.update { it.copy(currentStep = StakingStep.Amount) }
|
||||
}
|
||||
|
||||
fun showValidators() {
|
||||
stateController.update { it.copy(currentStep = StakingStep.Validators) }
|
||||
private fun showRestakeValidators() {
|
||||
stateController.update { it.copy(currentStep = StakingStep.RestakeValidator) }
|
||||
}
|
||||
|
||||
private fun showConfirmation() {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.domain.staking.model.PendingTransaction
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
|
||||
|
|
@ -34,6 +35,7 @@ internal data class StakingUiState(
|
|||
val amountState: AmountState,
|
||||
val rewardsValidatorsState: StakingStates.RewardsValidatorsState,
|
||||
val confirmationState: StakingStates.ConfirmationState,
|
||||
val validatorState: StakingStates.ValidatorState,
|
||||
val isBalanceHidden: Boolean,
|
||||
val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
val actionType: StakingActionCommonType,
|
||||
|
|
@ -45,10 +47,12 @@ internal data class StakingUiState(
|
|||
initialInfoState: StakingStates.InitialInfoState = this.initialInfoState,
|
||||
amountState: AmountState = this.amountState,
|
||||
confirmationState: StakingStates.ConfirmationState = this.confirmationState,
|
||||
validatorState: StakingStates.ValidatorState = this.validatorState,
|
||||
): StakingUiState = copy(
|
||||
initialInfoState = initialInfoState,
|
||||
amountState = amountState,
|
||||
confirmationState = confirmationState,
|
||||
validatorState = validatorState,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -85,13 +89,30 @@ internal sealed class StakingStates {
|
|||
) : RewardsValidatorsState()
|
||||
}
|
||||
|
||||
sealed class ValidatorState : StakingStates() {
|
||||
abstract val isClickable: Boolean
|
||||
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
override val isClickable: Boolean,
|
||||
val isVisibleOnConfirmation: Boolean,
|
||||
val chosenValidator: Yield.Validator,
|
||||
val activeValidator: Yield.Validator?,
|
||||
val availableValidators: List<Yield.Validator>,
|
||||
) : ValidatorState()
|
||||
|
||||
data class Empty(
|
||||
override val isClickable: Boolean = false,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : ValidatorState()
|
||||
}
|
||||
|
||||
/** Confirmation state */
|
||||
sealed class ConfirmationState : StakingStates() {
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val innerState: InnerConfirmationStakingState,
|
||||
val feeState: FeeState,
|
||||
val validatorState: ValidatorState,
|
||||
val pendingAction: PendingAction?,
|
||||
val pendingActions: ImmutableList<PendingAction>?,
|
||||
val notifications: ImmutableList<NotificationUM>,
|
||||
|
|
@ -112,6 +133,7 @@ enum class StakingStep {
|
|||
InitialInfo,
|
||||
RewardsValidators,
|
||||
Amount,
|
||||
RestakeValidator,
|
||||
Confirmation,
|
||||
Validators,
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
|
||||
@Immutable
|
||||
internal sealed class ValidatorState {
|
||||
|
||||
abstract val isClickable: Boolean
|
||||
|
||||
data class Content(
|
||||
override val isClickable: Boolean,
|
||||
val chosenValidator: Yield.Validator,
|
||||
val availableValidators: List<Yield.Validator>,
|
||||
) : ValidatorState()
|
||||
|
||||
data object Loading : ValidatorState() {
|
||||
override val isClickable: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
data object Error : ValidatorState() {
|
||||
override val isClickable: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
fun copySealed(isClickable: Boolean): ValidatorState {
|
||||
return if (this is Content) {
|
||||
copy(isClickable = isClickable)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue