Updated on 2026-08-14
This commit is contained in:
commit
652159dbce
316 changed files with 6242 additions and 3084 deletions
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@ package com.tangem.datasource.api.stakekit
|
|||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.stakekit.models.request.*
|
||||
import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.ActionDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.GetActionsResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO
|
||||
|
|
@ -35,14 +36,23 @@ interface StakeKitApi {
|
|||
@Body body: YieldBalanceRequestBody,
|
||||
): ApiResponse<List<BalanceDTO>>
|
||||
|
||||
@GET("actions")
|
||||
suspend fun getActions(
|
||||
@Query("walletAddress") walletAddress: String,
|
||||
@Query("network") network: String,
|
||||
@Query("status") status: String,
|
||||
@Query("sort") sort: String = "createdAtDesc",
|
||||
@Query("limit") limit: Int = 50,
|
||||
): ApiResponse<GetActionsResponse>
|
||||
|
||||
@POST("actions/enter")
|
||||
suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse<EnterActionResponse>
|
||||
suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse<ActionDTO>
|
||||
|
||||
@POST("actions/exit")
|
||||
suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse<EnterActionResponse>
|
||||
suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse<ActionDTO>
|
||||
|
||||
@POST("actions/pending")
|
||||
suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse<EnterActionResponse>
|
||||
suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse<ActionDTO>
|
||||
|
||||
@POST("actions/enter/estimate-gas")
|
||||
suspend fun estimateGasOnEnter(@Body body: ActionRequestBody): ApiResponse<StakingGasEstimateDTO>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import org.joda.time.DateTime
|
|||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EnterActionResponse(
|
||||
data class ActionDTO(
|
||||
@Json(name = "id")
|
||||
val id: String,
|
||||
@Json(name = "integrationId")
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.datasource.api.stakekit.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetActionsResponse(
|
||||
@Json(name = "data")
|
||||
val data: List<ActionDTO>,
|
||||
@Json(name = "hasNextPage")
|
||||
val hasNextPage: Boolean,
|
||||
@Json(name = "limit")
|
||||
val limit: Int,
|
||||
@Json(name = "page")
|
||||
val page: Int,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.token.*
|
||||
import com.tangem.datasource.local.token.DefaultStakingBalanceStore
|
||||
import com.tangem.datasource.local.token.StakingBalanceStore
|
||||
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -11,11 +12,23 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object StakingBalanceStoreModule {
|
||||
internal object StakingStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingTokensStore(): StakingYieldsStore {
|
||||
return DefaultStakingYieldsStore()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingBalanceStore(): StakingBalanceStore {
|
||||
return DefaultStakingBalanceStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingActionsStore(): StakingActionsStore {
|
||||
return DefaultStakingActionsStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
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 StakingTokensStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingTokensStore(): StakingYieldsStore {
|
||||
return DefaultStakingYieldsStore()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import com.tangem.datasource.asset.loader.AssetLoader
|
|||
import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage
|
||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage
|
||||
import com.tangem.datasource.local.config.providers.DefaultBlockchainProvidersStorage
|
||||
import com.tangem.datasource.local.config.testnet.DefaultTestnetTokensStorage
|
||||
|
|
@ -30,7 +32,7 @@ internal object ConfigModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage {
|
||||
fun provideTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage {
|
||||
return DefaultTestnetTokensStorage(assetLoader)
|
||||
}
|
||||
|
||||
|
|
@ -42,4 +44,13 @@ internal object ConfigModule {
|
|||
runtimeStateStore = RuntimeStateStore(defaultValue = emptyMap()),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIssuersConfigStorage(assetLoader: AssetLoader): IssuersConfigStorage {
|
||||
return DefaultIssuersConfigStorage(
|
||||
assetLoader = assetLoader,
|
||||
runtimeStateStore = RuntimeStateStore(defaultValue = emptyList()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.datasource.local.config.issuers
|
||||
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.config.issuers.models.Issuer
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
|
||||
internal class DefaultIssuersConfigStorage(
|
||||
private val assetLoader: AssetLoader,
|
||||
private val runtimeStateStore: RuntimeStateStore<List<Issuer>>,
|
||||
) : IssuersConfigStorage {
|
||||
|
||||
override suspend fun getConfig(): List<Issuer> {
|
||||
val cachedData = runtimeStateStore.get().value
|
||||
|
||||
if (cachedData.isNotEmpty()) return cachedData
|
||||
|
||||
val issuers = assetLoader.loadList<Issuer>(fileName = ISSUERS_FILE_NAME)
|
||||
|
||||
runtimeStateStore.store(value = issuers)
|
||||
|
||||
return issuers
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ISSUERS_FILE_NAME = "tangem-app-config/issuers"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.local.config.issuers
|
||||
|
||||
import com.tangem.datasource.local.config.issuers.models.Issuer
|
||||
|
||||
/**
|
||||
* Storage for list of Twins [Issuer]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface IssuersConfigStorage {
|
||||
|
||||
/** Get config */
|
||||
suspend fun getConfig(): List<Issuer>
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.local.config.issuers.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Issuer(
|
||||
@Json(name = "privateKey") val privateKey: String,
|
||||
@Json(name = "publicKey") val publicKey: String,
|
||||
)
|
||||
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
internal class DefaultStakingActionsStore(
|
||||
private val dataStore: StringKeyDataStore<List<StakingAction>>,
|
||||
) : StakingActionsStore {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
override fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<StakingAction>> {
|
||||
return dataStore.get(composeKey(userWalletId, cryptoCurrencyId))
|
||||
}
|
||||
|
||||
override suspend fun store(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
items: List<StakingAction>,
|
||||
) {
|
||||
mutex.withLock {
|
||||
dataStore.store(composeKey(userWalletId, cryptoCurrencyId), items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun composeKey(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): String {
|
||||
return userWalletId.stringValue + cryptoCurrencyId.value
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,13 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
|||
|
||||
internal class DefaultStakingYieldsStore : StakingYieldsStore {
|
||||
|
||||
private var yields = mutableListOf<YieldDTO>()
|
||||
private var yields = listOf<YieldDTO>()
|
||||
|
||||
override fun get(): List<YieldDTO> {
|
||||
return yields
|
||||
}
|
||||
|
||||
override fun store(items: List<YieldDTO>) {
|
||||
yields = items.toMutableList()
|
||||
yields = items
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface StakingActionsStore {
|
||||
|
||||
fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<StakingAction>>
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, items: List<StakingAction>)
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
},
|
||||
{
|
||||
"name": "WC_SOLANA_TX_SIGN_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.18.0"
|
||||
},
|
||||
{
|
||||
"name": "STAKING_ENABLED",
|
||||
|
|
@ -22,5 +22,17 @@
|
|||
{
|
||||
"name": "MIGRATE_USER_COUNTRY_CODE_ENABLED",
|
||||
"version": "5.17.0"
|
||||
},
|
||||
{
|
||||
"name": "ONRAMP_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "MAIN_ACTION_BUTTONS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ONBOARDING_CODE_REFACTORING_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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.core.ui.components.tokenlist
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.tokenlist.internal.NetworkTitleItem
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
||||
/**
|
||||
* Multi-currency content item
|
||||
*
|
||||
* @param state component UI model
|
||||
* @param isBalanceHidden flag that shows/hides balance
|
||||
* @param modifier modifier
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TokensListItemUM.NetworkGroupTitle -> {
|
||||
NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifier)
|
||||
}
|
||||
is TokensListItemUM.Token -> {
|
||||
TokenItem(
|
||||
state = state.state,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
is TokensListItemUM.SearchBar -> {
|
||||
SearchBar(state = state.searchBarUM, modifier = modifier.padding(all = 12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package com.tangem.core.ui.components.tokenlist.internal
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.rows.NetworkTitle
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import org.burnoutcrew.reorderable.ReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.detectReorder
|
||||
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||
|
||||
/**
|
||||
* Network title item
|
||||
*
|
||||
* @param networkName network name
|
||||
* @param modifier modifier
|
||||
*/
|
||||
@Composable
|
||||
internal fun NetworkTitleItem(networkName: String, modifier: Modifier = Modifier) {
|
||||
BaseNetworkTitleItem(networkName = networkName, modifier = modifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draggable network title item
|
||||
*
|
||||
* @param networkName network name
|
||||
* @param reorderableTokenListState reorderable token list state
|
||||
* @param modifier modifier
|
||||
*/
|
||||
@Composable
|
||||
fun DraggableNetworkTitleItem(
|
||||
networkName: String,
|
||||
reorderableTokenListState: ReorderableLazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BaseNetworkTitleItem(
|
||||
networkName = networkName,
|
||||
modifier = modifier,
|
||||
action = { DraggableIcon(reorderableTokenListState = reorderableTokenListState) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BaseNetworkTitleItem(
|
||||
networkName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
action: (@Composable BoxScope.() -> Unit)? = null,
|
||||
) {
|
||||
NetworkTitle(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_network_group_title, networkName),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DraggableIcon(reorderableTokenListState: ReorderableLazyListState) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size32)
|
||||
.detectReorder(reorderableTokenListState),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(id = R.drawable.ic_group_drop_24),
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun NetworkTitleItemPreview(@PreviewParameter(NetworkTitleItemProvider::class) isDraggable: Boolean) {
|
||||
TangemThemePreview {
|
||||
if (isDraggable) {
|
||||
DraggableNetworkTitleItem(
|
||||
networkName = "Ethereum",
|
||||
reorderableTokenListState = rememberReorderableLazyListState(onMove = { _, _ -> }),
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.primary),
|
||||
)
|
||||
} else {
|
||||
NetworkTitleItem(
|
||||
networkName = "Ethereum",
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object NetworkTitleItemProvider : CollectionPreviewParameterProvider<Boolean>(collection = listOf(true, false))
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.core.ui.components.tokenlist.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/** Tokens list item state */
|
||||
@Immutable
|
||||
sealed interface TokensListItemUM {
|
||||
|
||||
/** Unique ID */
|
||||
val id: Any
|
||||
|
||||
/**
|
||||
* Search bar item
|
||||
*
|
||||
* @property id id
|
||||
* @property searchBarUM search bar UI model
|
||||
*/
|
||||
data class SearchBar(
|
||||
override val id: Any = "search_bar",
|
||||
val searchBarUM: SearchBarUM,
|
||||
) : TokensListItemUM
|
||||
|
||||
/**
|
||||
* Network group title
|
||||
*
|
||||
* @property id id
|
||||
* @property name network group name
|
||||
*/
|
||||
data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemUM
|
||||
|
||||
/**
|
||||
* Token item
|
||||
*
|
||||
* @property state token state
|
||||
*/
|
||||
data class Token(val state: TokenItemState) : TokensListItemUM {
|
||||
override val id: String = state.id
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.core.ui.decorations
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
|
|
@ -15,46 +17,49 @@ fun Modifier.roundedShapeItemDecoration(
|
|||
lastIndex: Int,
|
||||
addDefaultPadding: Boolean = true,
|
||||
radius: Dp = TangemTheme.dimens.radius16,
|
||||
backgroundColor: Color? = null,
|
||||
): Modifier = composed {
|
||||
val modifier = if (addDefaultPadding) this.padding(horizontal = TangemTheme.dimens.spacing16) else this
|
||||
|
||||
val applyTopPadding: @Composable Modifier.() -> Modifier = {
|
||||
if (addDefaultPadding) {
|
||||
padding(top = TangemTheme.dimens.spacing12)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
val applyShape: Modifier.(shape: RoundedCornerShape?) -> Modifier = { shape ->
|
||||
if (backgroundColor != null) {
|
||||
if (shape != null) {
|
||||
background(color = backgroundColor, shape = shape)
|
||||
} else {
|
||||
background(color = backgroundColor)
|
||||
}
|
||||
} else {
|
||||
if (shape != null) {
|
||||
clip(shape = shape)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isSingleItem = currentIndex == 0 && lastIndex == 0
|
||||
when {
|
||||
isSingleItem -> {
|
||||
modifier
|
||||
.then(
|
||||
if (addDefaultPadding) {
|
||||
Modifier.padding(top = TangemTheme.dimens.spacing12)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.clip(shape = RoundedCornerShape(radius))
|
||||
.applyTopPadding()
|
||||
.applyShape(RoundedCornerShape(radius))
|
||||
}
|
||||
currentIndex == 0 -> {
|
||||
modifier
|
||||
.then(
|
||||
if (addDefaultPadding) {
|
||||
Modifier.padding(top = TangemTheme.dimens.spacing12)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.clip(
|
||||
shape = RoundedCornerShape(
|
||||
topStart = radius,
|
||||
topEnd = radius,
|
||||
),
|
||||
)
|
||||
.applyTopPadding()
|
||||
.applyShape(RoundedCornerShape(topStart = radius, topEnd = radius))
|
||||
}
|
||||
currentIndex == lastIndex -> {
|
||||
modifier
|
||||
.clip(
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = radius,
|
||||
bottomEnd = radius,
|
||||
),
|
||||
)
|
||||
modifier.applyShape(RoundedCornerShape(bottomStart = radius, bottomEnd = radius))
|
||||
}
|
||||
else -> modifier
|
||||
else -> modifier.applyShape(null)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.tangem.utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Job holder. It is automatically finished old job if new one is started
|
||||
|
|
@ -26,4 +29,13 @@ class JobHolder {
|
|||
|
||||
fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this)
|
||||
|
||||
suspend fun Job.saveInAndJoin(jobHolder: JobHolder) = saveIn(jobHolder).join()
|
||||
suspend fun Job.saveInAndJoin(jobHolder: JobHolder) = saveIn(jobHolder).join()
|
||||
|
||||
fun CoroutineScope.withDebounce(jobHolder: JobHolder, timeMillis: Long = 800L, function: () -> Unit) {
|
||||
launch {
|
||||
delay(timeMillis = timeMillis)
|
||||
|
||||
function()
|
||||
}
|
||||
.saveIn(jobHolder)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue