Skip to content

Commit

Permalink
Improve buy screen
Browse files Browse the repository at this point in the history
  • Loading branch information
furenster committed Jun 4, 2024
1 parent a498acb commit 92ba82a
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 281 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class TestSolanaSign {
owner = "4Yu2e1Wz5T1Ci2hAPswDqvMgSnJ1Ftw7ZZh8x7xKLx7S",
finalAmount = BigInteger.TEN.pow(com.wallet.core.primitives.Chain.Solana.asset().decimals),
info = SolanaSignerPreloader.Info(
blockhash = "",
blockhash = "DzfXchZJoLMG3cNftcf2sw7qatkkuwQf4xH15N5wkKAb",
senderTokenAddress = "",
recipientTokenAddress = null,
fee = GasFee(
Expand All @@ -60,7 +60,7 @@ class TestSolanaSign {
)
}
Assert.assertEquals(
"0x41513265386f632b323032796e7564666a6e73434c676a532b3334616175676c364665314655344d3936597a6a542f6246666465343637784e7869787863356f6e6570724268616e733062534d4644793243665541516342414145445365626b44466a2b415242396b4b486b394f4167745057456e614370426a475a3869707a61372f4e43577330767554324f647746546758414767305a6175317474703157354f7153437a77434c53384e5738357a7151414141414141414141414141414141414141414141414141414141414141414141414141414141414141415149434141454d416741414141444b6d6a734141414141",
"0x416349776a555a6b6f65466f7569306d6a38506a6861672f32333372787346687570304b2b4b3862575145774c77704b7678736d6e2f3761754c635059652f6b7378446c332b63346970574b38334e6d6e6c4d4a77774942414145445365626b44466a2b415242396b4b486b394f4167745057456e614370426a475a3869707a61372f4e43577330767554324f647746546758414767305a6175317474703157354f7153437a77434c53384e5738357a715141414141414141414141414141414141414141414141414141414141414141414141414141414141414177524870726c566e6a4f6f2b6532723437724d564a482b38475179334444524c66686c6d5850413172497742416749414151774341414141414d71614f7741414141413d",
sign.toHexString()
)
}
Expand Down
15 changes: 10 additions & 5 deletions app/src/main/java/com/gemwallet/android/data/buy/BuyRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import com.gemwallet.android.services.GemApiClient
import com.wallet.core.primitives.Asset
import com.wallet.core.primitives.AssetId
import com.wallet.core.primitives.FiatQuote
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class BuyRepository @Inject constructor(
private val configRepository: ConfigRepository,
private val remoteSource: GemApiClient,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {

fun getAvailable(): List<AssetId> {
Expand All @@ -27,12 +31,13 @@ class BuyRepository @Inject constructor(
fiatAmount: Double,
owner: String,
): Result<List<FiatQuote>> {
val result = remoteSource.getQuote(asset.id.toIdentifier(), fiatAmount, fiatCurrency, owner)
return result.mapCatching {
if (it.quotes.isEmpty()) {
throw Exception("Quotes not found")
return withContext(defaultDispatcher) {
remoteSource.getQuote(asset.id.toIdentifier(), fiatAmount, fiatCurrency, owner).mapCatching {
if (it.quotes.isEmpty()) {
throw Exception("Quotes not found")
}
it.quotes
}
it.quotes
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gemwallet.android.features.buy.models

sealed interface BuyError {
data object MinimumAmount : BuyError

data object QuoteNotAvailable : BuyError

data object ValueIncorrect : BuyError
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.gemwallet.android.features.buy.models

import androidx.compose.runtime.Stable
import com.wallet.core.primitives.Asset
import com.wallet.core.primitives.AssetType
import com.wallet.core.primitives.FiatProvider

sealed interface BuyUIState {
data class Idle(
val isQuoteLoading: Boolean = false,
val asset: Asset? = null,
val title: String = "",
val assetType: AssetType = AssetType.NATIVE,
val cryptoAmount: String = "",
val fiatAmount: String = "",
val currentProvider: Provider? = null,
val redirectUrl: String? = null,
val providers: List<Provider> = emptyList(),
val error: BuyError? = null,
) : BuyUIState {
fun isAvailable(): Boolean = !isQuoteLoading && error == null && redirectUrl != null && currentProvider != null
}

data class Fatal(
val message: String = "",
) : BuyUIState

@Stable
data class Provider(
val provider: FiatProvider,
val cryptoAmount: String,
val rate: String,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import com.gemwallet.android.data.asset.AssetsRepository
import com.gemwallet.android.data.buy.BuyRepository
import com.gemwallet.android.data.session.SessionRepository
import com.gemwallet.android.ext.asset
import com.gemwallet.android.features.buy.models.BuyError
import com.gemwallet.android.features.buy.models.BuyUIState
import com.gemwallet.android.math.numberParse
import com.gemwallet.android.model.AssetInfo
import com.gemwallet.android.model.CountingUnit
import com.gemwallet.android.model.Crypto
import com.gemwallet.android.model.Fiat
import com.gemwallet.android.model.format
import com.wallet.core.primitives.Asset
import com.wallet.core.primitives.AssetId
import com.wallet.core.primitives.AssetType
import com.wallet.core.primitives.Currency
import com.wallet.core.primitives.FiatProvider
import com.wallet.core.primitives.FiatQuote
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -29,7 +30,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.math.BigDecimal
import javax.inject.Inject

@HiltViewModel
Expand All @@ -38,9 +38,9 @@ class BuyViewModel @Inject constructor(
private val assetsRepository: AssetsRepository,
private val buyRepository: BuyRepository,
) : ViewModel() {
val state = MutableStateFlow(BuyViewModelState())
private val state = MutableStateFlow(BuyViewModelState())
val uiState: StateFlow<BuyUIState> = state.map { it.toUIState() }
.stateIn(viewModelScope, SharingStarted.Eagerly, BuyUIState.Success())
.stateIn(viewModelScope, SharingStarted.Eagerly, BuyUIState.Idle())

private var queryJob: Job? = null
var amount by mutableStateOf("")
Expand All @@ -55,13 +55,9 @@ class BuyViewModel @Inject constructor(
val assetInfo = assetsRepository.getById(session.wallet, assetId).getOrNull()?.firstOrNull()
if (assetInfo == null) {
state.update { it.copy(fatalError = "Asset not found") }
return
}
state.update {
BuyViewModelState(
isLoading = false,
assetInfo = assetInfo
)
}
state.update { BuyViewModelState(assetInfo = assetInfo) }
amount = state.value.fiatAmount.toInt().toString()
fiatAmount(state.value.fiatAmount)
}
Expand All @@ -76,7 +72,7 @@ class BuyViewModel @Inject constructor(

buyRepository.getQuote(
asset = assetSummary.asset,
fiatCurrency = currency,
fiatCurrency = currency.string,
fiatAmount = fiatAmount,
owner = assetSummary.owner.address,
).onFailure {
Expand Down Expand Up @@ -104,148 +100,82 @@ class BuyViewModel @Inject constructor(
if (queryJob?.isActive == true) {
queryJob?.cancel()
}
if (input.isEmpty()) {
state.update { it.copy(error = BuyError.MinimumAmount) }
return
}
state.update { it.copy(isQuoteLoading = false, error = null) }
val newValue = try {
input.numberParse().toDouble()
input.ifEmpty { "0.0" }.numberParse().toDouble()
} catch (err: Throwable) {
state.update { it.copy(error = BuyError.ValueIncorrect) }
return
}
if (newValue < 20) {
if (newValue < MIN_FIAT_AMOUNT) {
state.update { it.copy(error = BuyError.MinimumAmount) }
return
}
if (state.value.fiatAmount == newValue) {
return
}
state.update { it.copy(isQuoteLoading = input.isNotEmpty()) }
if (queryJob?.isActive == true) {
queryJob?.cancel()
}
state.update { it.copy(isQuoteLoading = true) }
queryJob = viewModelScope.launch {
delay(500)
fiatAmount(newValue.toDouble())
}
}
}

data class BuyViewModelState(
val isLoading: Boolean = true,
val isQuoteLoading: Boolean = false,
val assetInfo: AssetInfo? = null,
val currency: String = "USD",
val fiatAmount: Double = 50.0,
val quotes: List<FiatQuote> = emptyList(),
val selectProvider: FiatProvider? = null,
val redirectUrl: String? = null,
val error: BuyError? = null,
val fatalError: String? = null,
) {
fun toUIState(): BuyUIState = if (fatalError == null) {
val symbol = assetInfo?.asset?.symbol ?: ""
val decimals = assetInfo?.asset?.decimals ?: 0
val chain = assetInfo?.asset?.id?.chain
val quote = getQuote(selectProvider)
BuyUIState.Success(
isLoading = isLoading,
isQuoteLoading = isQuoteLoading,
asset = assetInfo?.asset,
title = assetInfo?.asset?.name ?: "",
assetType = chain?.asset()?.type ?: AssetType.NATIVE,
cryptoAmount = if (quote == null) {
" "
} else {
"~${
Crypto(quote.cryptoAmount.toBigDecimal(), decimals).format(
decimals,
symbol,
6,
CountingUnit.SignMode.NoSign,
true
)
}"
},
fiatAmount = "${fiatAmount.toInt()}",
selectProvider = if (quote != null) mapToProvider(quote, symbol, decimals) else null,
providers = quotes.map {
mapToProvider(it, symbol, decimals)
},
redirectUrl = quote?.redirectUrl,
error = error,
)
} else {
BuyUIState.Fatal(message = fatalError)
}

private fun mapToProvider(quote: FiatQuote, symbol: String, decimals: Int): BuyUIState.Provider {
return BuyUIState.Provider(
provider = quote.provider,
cryptoAmount = Crypto(quote.cryptoAmount.toBigDecimal(), decimals).format(
decimals,
symbol,
6,
CountingUnit.SignMode.NoSign,
true
),
rate = "1 $symbol ~ ${
Fiat(BigDecimal(quote.fiatAmount / quote.cryptoAmount)).format(
0,
"USD",
2
)
}"
)
}

private fun getQuote(provider: FiatProvider?): FiatQuote? {
if (quotes.isEmpty()) {
return null
fiatAmount(newValue)
}
if (provider == null) {
return quotes.firstOrNull()
}
return quotes.firstOrNull{ it.provider == provider } ?: quotes.firstOrNull()
}
}

sealed interface BuyUIState {
data class Success(
val isLoading: Boolean = false,
private data class BuyViewModelState(
val isQuoteLoading: Boolean = false,
val asset: Asset? = null,
val title: String = "",
val assetType: AssetType = AssetType.NATIVE,
val cryptoAmount: String = "",
val fiatAmount: String = "",
val selectProvider: Provider? = null,
val redirectUrl: String? = null,
val providers: List<Provider> = emptyList(),
val assetInfo: AssetInfo? = null,
val currency: Currency = Currency.USD,
val fiatAmount: Double = 50.0,
val quotes: List<FiatQuote> = emptyList(),
val selectProvider: FiatProvider? = null,
val error: BuyError? = null,
) : BuyUIState
val fatalError: String? = null,
) {
fun toUIState(): BuyUIState = if (fatalError == null && assetInfo != null) {
val chain = assetInfo.asset.id.chain
val quote = getQuote(selectProvider)
BuyUIState.Idle(
isQuoteLoading = isQuoteLoading,
asset = assetInfo.asset,
title = assetInfo.asset.name,
assetType = chain.asset().type,
cryptoAmount = if (quote == null) {
" "
} else {
"~${assetInfo.asset.format(quote.cryptoAmount, showSign = CountingUnit.SignMode.NoSign, dynamicPlace = true)}"
},
fiatAmount = "${fiatAmount.toInt()}",
currentProvider = if (quote != null) mapToProvider(quote, assetInfo.asset) else null,
providers = quotes.map {
mapToProvider(it, assetInfo.asset)
},
redirectUrl = quote?.redirectUrl,
error = error,
)
} else {
BuyUIState.Fatal(message = fatalError ?: "")
}

data class Fatal(
val message: String = "",
) : BuyUIState
private fun mapToProvider(quote: FiatQuote, asset: Asset): BuyUIState.Provider {
return BuyUIState.Provider(
provider = quote.provider,
cryptoAmount = asset.format(quote.cryptoAmount, 6, CountingUnit.SignMode.NoSign, true),
rate = "1 ${asset.symbol} ~ ${currency.format(quote.fiatAmount / quote.cryptoAmount).format("USD", 2)}"
)
}

data class BuyLot(
val title: String,
val value: Double,
)
private fun getQuote(provider: FiatProvider?): FiatQuote? {
if (quotes.isEmpty()) {
return null
}
if (provider == null) {
return quotes.firstOrNull()
}
return quotes.firstOrNull{ it.provider == provider } ?: quotes.firstOrNull()
}
}

data class Provider(
val provider: FiatProvider,
val cryptoAmount: String,
val rate: String,
)
companion object {
const val MIN_FIAT_AMOUNT = 20.0
}
}

sealed interface BuyError {
data object MinimumAmount : BuyError

data object QuoteNotAvailable : BuyError

data object ValueIncorrect : BuyError
}
Loading

0 comments on commit 92ba82a

Please sign in to comment.