Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ fun PriceAlertTargetNavScreen(
val type by viewModel.type.collectAsStateWithLifecycle()
val direction by viewModel.direction.collectAsStateWithLifecycle()
val error by viewModel.error.collectAsStateWithLifecycle()
val priceSuggestions by viewModel.priceSuggestions.collectAsStateWithLifecycle()
val percentageSuggestions by viewModel.percentageSuggestions.collectAsStateWithLifecycle()
val asset by viewModel.asset.collectAsStateWithLifecycle()
val priceFormatted by viewModel.priceFormatted.collectAsStateWithLifecycle()
val priceChangeFormatted by viewModel.priceChangeFormatted.collectAsStateWithLifecycle()
val priceState by viewModel.priceState.collectAsStateWithLifecycle()

PriceAlertTargetScene(
value = viewModel.value,
Expand All @@ -32,6 +38,12 @@ fun PriceAlertTargetNavScreen(
currency = currency,
currentPriceValue = currentPriceValue,
currentPriceFormatted = currentPriceFormatted,
priceSuggestions = priceSuggestions,
percentageSuggestions = percentageSuggestions,
asset = asset,
assetPriceFormatted = priceFormatted,
assetPriceChangeFormatted = priceChangeFormatted,
assetPriceState = priceState,
error = error,
onType = viewModel::onType,
onDirection = viewModel::onDirection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,25 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.gemwallet.android.domains.price.PriceState
import com.gemwallet.android.ui.R
import com.gemwallet.android.ui.components.TabsBar
import com.gemwallet.android.ui.components.buttons.MainActionButton
import com.gemwallet.android.ui.components.clickable
import com.gemwallet.android.ui.components.image.AssetIcon
import com.gemwallet.android.ui.components.list_item.Badge
import com.gemwallet.android.ui.components.list_item.ListItem
import com.gemwallet.android.ui.components.list_item.ListItemTitleText
import com.gemwallet.android.ui.components.list_item.PriceInfo
import com.gemwallet.android.ui.components.parseMarkdownToAnnotatedString
import com.gemwallet.android.ui.components.screen.Scene
import com.gemwallet.android.ui.models.ListPosition
import com.gemwallet.android.ui.theme.WalletTheme
import com.gemwallet.android.ui.theme.paddingHalfSmall
import com.gemwallet.android.ui.theme.paddingLarge
import com.gemwallet.android.ui.theme.paddingSmall
import com.gemwallet.android.features.settings.price_alerts.viewmodels.models.PriceAlertTargetError
import com.wallet.core.primitives.Asset
import com.wallet.core.primitives.Currency
import com.wallet.core.primitives.PriceAlertDirection
import com.wallet.core.primitives.PriceAlertNotificationType
Expand All @@ -65,6 +73,12 @@ fun PriceAlertTargetScene(
currency: Currency,
currentPriceValue: Double,
currentPriceFormatted: String,
priceSuggestions: List<Pair<String, String>> = emptyList(),
percentageSuggestions: List<Int> = listOf(5, 10, 15),
asset: Asset? = null,
assetPriceFormatted: String = "",
assetPriceChangeFormatted: String = "",
assetPriceState: PriceState = PriceState.None,
error: PriceAlertTargetError?,
onType: (PriceAlertNotificationType) -> Unit,
onDirection: (PriceAlertDirection) -> Unit,
Expand Down Expand Up @@ -102,11 +116,31 @@ fun PriceAlertTargetScene(
}
},
mainAction = {
MainActionButton(
title = stringResource(R.string.transfer_confirm),
enabled = error == null && value.text.isNotEmpty(),
onClick = onConfirm,
)
if (value.text.isEmpty()) {
val suggestions = when (type) {
PriceAlertNotificationType.Price -> priceSuggestions
PriceAlertNotificationType.PricePercentChange -> percentageSuggestions.map { "$it%" to it.toString() }
else -> emptyList()
}
if (suggestions.isNotEmpty()) {
TabsBar(
tabs = suggestions,
selected = "" to "",
onSelect = { pair ->
value.edit { this.replace(0, this.length, pair.second) }
},
equalWidth = false,
) { pair ->
Text(pair.first)
}
}
} else {
MainActionButton(
title = stringResource(R.string.transfer_confirm),
enabled = error == null,
onClick = onConfirm,
)
}
},
onClose = onCancel,
) {
Expand Down Expand Up @@ -146,25 +180,35 @@ fun PriceAlertTargetScene(
horizontalArrangement = Arrangement.spacedBy(paddingHalfSmall),
) {
Box(Modifier.weight(1f)) {
if (type == PriceAlertNotificationType.PricePercentChange) {
Icon(
modifier = Modifier.align(Alignment.CenterEnd).clickable {
val direction = when (direction) {
PriceAlertDirection.Up -> PriceAlertDirection.Down
PriceAlertDirection.Down -> PriceAlertDirection.Up
}
onDirection(direction)
},
imageVector = when (direction) {
PriceAlertDirection.Up -> Icons.Default.ArrowCircleUp
PriceAlertDirection.Down -> Icons.Default.ArrowCircleDown
},
contentDescription = "",
tint = when (direction) {
PriceAlertDirection.Up -> MaterialTheme.colorScheme.tertiary
PriceAlertDirection.Down -> MaterialTheme.colorScheme.error
},
)
when (type) {
PriceAlertNotificationType.Price -> {
Text(
modifier = Modifier.align(Alignment.CenterEnd),
text = java.util.Currency.getInstance(currency.string).symbol,
style = MaterialTheme.typography.displaySmall,
)
}
PriceAlertNotificationType.PricePercentChange -> {
Icon(
modifier = Modifier.align(Alignment.CenterEnd).clickable {
val direction = when (direction) {
PriceAlertDirection.Up -> PriceAlertDirection.Down
PriceAlertDirection.Down -> PriceAlertDirection.Up
}
onDirection(direction)
},
imageVector = when (direction) {
PriceAlertDirection.Up -> Icons.Default.ArrowCircleUp
PriceAlertDirection.Down -> Icons.Default.ArrowCircleDown
},
contentDescription = "",
tint = when (direction) {
PriceAlertDirection.Up -> MaterialTheme.colorScheme.tertiary
PriceAlertDirection.Down -> MaterialTheme.colorScheme.error
},
)
}
else -> {}
}
}
BasicTextField(
Expand All @@ -185,30 +229,37 @@ fun PriceAlertTargetScene(
}
)
Box(Modifier.weight(1f)) {
Text(
text = when (type) {
PriceAlertNotificationType.Auto -> ""
PriceAlertNotificationType.Price -> java.util.Currency.getInstance(currency.string).symbol
PriceAlertNotificationType.PricePercentChange -> "%"
},
style = MaterialTheme.typography.displaySmall,
)
if (type == PriceAlertNotificationType.PricePercentChange) {
Text(
text = "%",
style = MaterialTheme.typography.displaySmall,
)
}
}
}
}
item {
Text(
text = parseMarkdownToAnnotatedString("${stringResource(R.string.price_alerts_set_alert_current_price)} **$currentPriceFormatted**"),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyLarge,
)
}

if (type == PriceAlertNotificationType.PricePercentChange) {
val percentage = listOf("5", "10", "15")
if (asset != null) {
item {
TabsBar(tabs = percentage, selected = value.text, onSelect = { select -> value.edit { this.replace(0, this.length, select) } }) {
Text("$it%")
}
ListItem(
listPosition = ListPosition.Single,
leading = { AssetIcon(asset) },
title = { ListItemTitleText(asset.name, titleBadge = { Badge(asset.symbol) }) },
subtitle = {
PriceInfo(
price = assetPriceFormatted,
changes = assetPriceChangeFormatted,
state = assetPriceState,
style = MaterialTheme.typography.bodyMedium,
)
},
)
}
}
}
Expand All @@ -224,8 +275,10 @@ fun PriceAlertTargetScenePricePreview() {
direction = PriceAlertDirection.Up,
type = PriceAlertNotificationType.Price,
currency = Currency.USD,
currentPriceFormatted = "901.8$",
currentPriceFormatted = "$901.80",
currentPriceValue = 901.8,
priceSuggestions = listOf("$850" to "850", "$950" to "950"),
percentageSuggestions = listOf(3, 6, 9),
error = null,
onType = {},
onDirection = {},
Expand All @@ -244,8 +297,10 @@ fun PriceAlertTargetScenePercentagePreview() {
direction = PriceAlertDirection.Up,
type = PriceAlertNotificationType.PricePercentChange,
currency = Currency.USD,
currentPriceFormatted = "901.8$",
currentPriceFormatted = "$901.80",
currentPriceValue = 901.8,
priceSuggestions = listOf("$850" to "850", "$950" to "950"),
percentageSuggestions = listOf(3, 6, 9),
error = null,
onType = {},
onDirection = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import com.gemwallet.android.data.repositories.assets.AssetsRepository
import com.gemwallet.android.domains.pricealerts.direction
import com.gemwallet.android.domains.pricealerts.formatAmount
import com.gemwallet.android.ext.toAssetId
import com.gemwallet.android.domains.percentage.formatAsPercentage
import com.gemwallet.android.domains.price.PriceState
import com.gemwallet.android.domains.price.toPriceState
import com.gemwallet.android.model.compactFormatter
import com.gemwallet.android.model.format
import com.gemwallet.android.features.settings.price_alerts.viewmodels.models.PriceAlertConfirmResult
import com.gemwallet.android.features.settings.price_alerts.viewmodels.models.PriceAlertTargetError
import com.wallet.core.primitives.Asset
import com.wallet.core.primitives.Currency
import com.wallet.core.primitives.PriceAlertDirection
import com.wallet.core.primitives.PriceAlertNotificationType
Expand All @@ -26,8 +31,11 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import uniffi.gemstone.PriceAlertFormatter
import java.math.BigDecimal
import javax.inject.Inject

@OptIn(ExperimentalCoroutinesApi::class)
Expand All @@ -38,6 +46,9 @@ class PriceAlertTargetViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel() {

private val priceAlertFormatter = PriceAlertFormatter()
private val suggestionOffsetPercent = 5.0

val value = TextFieldState()

val assetId = savedStateHandle.getStateFlow<String?>("assetId", null)
Expand All @@ -55,6 +66,34 @@ class PriceAlertTargetViewModel @Inject constructor(
val currentPriceValue = assetInfo.map { it?.price?.price?.price ?: 0.0 }
.stateIn(viewModelScope, SharingStarted.Eagerly, 0.0)

val asset: StateFlow<Asset?> = assetInfo.map { it?.asset }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)

val priceFormatted: StateFlow<String> = assetInfo.map { info ->
val priceInfo = info?.price ?: return@map ""
priceInfo.currency.compactFormatter(priceInfo.price.price)
}.stateIn(viewModelScope, SharingStarted.Eagerly, "")

val priceChangeFormatted: StateFlow<String> = assetInfo.map {
it?.price?.price?.priceChangePercentage24h.formatAsPercentage()
}.stateIn(viewModelScope, SharingStarted.Eagerly, "")

val priceState: StateFlow<PriceState> = assetInfo.map {
it?.price?.price?.priceChangePercentage24h.toPriceState()
}.stateIn(viewModelScope, SharingStarted.Eagerly, PriceState.None)

val priceSuggestions: StateFlow<List<Pair<String, String>>> = combine(currentPriceValue, currency) { price, currency ->
if (price <= 0.0) return@combine emptyList()
priceAlertFormatter.roundedValues(price, suggestionOffsetPercent).map { value ->
val formatted = currency.format(BigDecimal.valueOf(value), dynamicPlace = true)
formatted to value.toBigDecimal().stripTrailingZeros().toPlainString()
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

val percentageSuggestions: StateFlow<List<Int>> = currentPriceValue.map { price ->
priceAlertFormatter.percentageSuggestions(price).map { it.toInt() }
}.stateIn(viewModelScope, SharingStarted.Eagerly, listOf(5, 10, 15))

val error: StateFlow<PriceAlertTargetError?> = snapshotFlow { value.text }.map {
val value = try { it.toString().toDouble() } catch (_: Throwable) { 0.0 }
if (value <= 0.0) {
Expand Down
2 changes: 2 additions & 0 deletions ios/Features/PriceAlerts/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ let package = Package(
.package(name: "Style", path: "../../Packages/Style"),
.package(name: "Localization", path: "../../Packages/Localization"),
.package(name: "PrimitivesComponents", path: "../../Packages/PrimitivesComponents"),
.package(name: "Gemstone", path: "../../Packages/Gemstone"),

.package(name: "Store", path: "../../Packages/Store"),
.package(name: "Preferences", path: "../../Packages/Preferences"),
Expand All @@ -34,6 +35,7 @@ let package = Package(
"Style",
"Localization",
"PrimitivesComponents",
"Gemstone",
"Store",
.product(name: "PriceAlertService", package: "FeatureServices"),
"Preferences",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Components
import Formatters
import Foundation
import Gemstone
import Localization
import Preferences
import PriceAlertService
Expand All @@ -19,9 +20,8 @@ public final class SetPriceAlertViewModel {
private let onComplete: StringAction
private let preferences = Preferences.standard
private let currencyFormatter = CurrencyFormatter(currencyCode: Preferences.standard.currency)
private let roundingFormatter = RoundingFormatter()
private let percentageSuggestionsFormatter = PercentageSuggestionsFormatter()
private let priceSuggestionPercent: Double = 5
private let priceAlertFormatter = PriceAlertFormatter()
private let suggestionOffsetPercent: Double = 5

var state: SetPriceAlertViewModelState

Expand All @@ -44,17 +44,17 @@ public final class SetPriceAlertViewModel {

func percentageSuggestions(for price: Price?) -> [PercentageSuggestion] {
guard let currentPrice = price?.price else { return [] }
return percentageSuggestionsFormatter.suggestions(for: currentPrice).map {
PercentageSuggestion(value: $0)
return priceAlertFormatter.percentageSuggestions(price: currentPrice).map {
PercentageSuggestion(value: $0.asInt)
}
}

func priceSuggestions(for price: Price?) -> [PriceSuggestion] {
guard let currentPrice = price?.price else { return [] }
return roundingFormatter.roundedValues(for: currentPrice, byPercent: priceSuggestionPercent).map { value in
return priceAlertFormatter.roundedValues(price: currentPrice, byPercent: suggestionOffsetPercent).map {
PriceSuggestion(
title: currencyFormatter.string(value),
value: value,
title: currencyFormatter.string($0),
value: $0,
)
}
}
Expand Down

This file was deleted.

Loading
Loading