Add a Reset button to clear selected currencies, with confirmation

New "Reset" button in the currency picker's top bar (next to Done),
shown only when more than the pinned default currency is selected.
Tapping it asks for confirmation before removing every other
selected currency, via PriceViewModel.onSelectedCurrenciesReset() —
same recompute/persist pattern as onFiatCurrencyToggled.

Compose uses an AlertDialog; iOS/macOS uses .alert with a destructive
Reset action, matching the destructive-role toolbar button that
triggers it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
This commit is contained in:
2026-09-10 15:29:40 +03:00
co-authored by Claude Sonnet 5
parent 0cfa52de56
commit 70ce0bccb1
7 changed files with 86 additions and 2 deletions
+3 -1
View File
@@ -238,7 +238,9 @@ struct ContentView: View {
pricedCurrencyCodes: state.pricedCurrencyCodes, pricedCurrencyCodes: state.pricedCurrencyCodes,
sourceName: state.sourceName, sourceName: state.sourceName,
localeCurrencyCode: state.localeCurrencyCode, localeCurrencyCode: state.localeCurrencyCode,
onToggle: { viewModel.onFiatCurrencyToggled($0) } selectedCount: state.selectedCurrencyCodes.count,
onToggle: { viewModel.onFiatCurrencyToggled($0) },
onReset: { viewModel.onSelectedCurrenciesReset() }
) )
} }
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
+4
View File
@@ -39,6 +39,10 @@ final class ConverterViewModel: ObservableObject {
bridge.onFiatCurrenciesReordered(newOrder: newOrder) bridge.onFiatCurrenciesReordered(newOrder: newOrder)
} }
func onSelectedCurrenciesReset() {
bridge.onSelectedCurrenciesReset()
}
func onSourceSelected(_ name: String) { func onSourceSelected(_ name: String) {
bridge.onSourceSelected(name: name) bridge.onSourceSelected(name: name)
} }
+30
View File
@@ -8,10 +8,13 @@ struct CurrencyPickerSheet: View {
let pricedCurrencyCodes: [String] let pricedCurrencyCodes: [String]
let sourceName: String let sourceName: String
let localeCurrencyCode: String? let localeCurrencyCode: String?
let selectedCount: Int
let onToggle: (String) -> Void let onToggle: (String) -> Void
let onReset: () -> Void
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var searchQuery = "" @State private var searchQuery = ""
@State private var showResetConfirmation = false
private func matches(_ info: CurrencyInfo) -> Bool { private func matches(_ info: CurrencyInfo) -> Bool {
SystemCurrenciesKt.matchesCurrencySearch(info: info, query: searchQuery) SystemCurrenciesKt.matchesCurrencySearch(info: info, query: searchQuery)
@@ -60,6 +63,16 @@ struct CurrencyPickerSheet: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#endif #endif
.toolbar { .toolbar {
if selectedCount > 1 {
ToolbarItem(placement: .cancellationAction) {
Button(
IosLocalizationKt.localizedString(resource: MR.strings.shared.reset_selected_currencies_button),
role: .destructive
) {
showResetConfirmation = true
}
}
}
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button(IosLocalizationKt.localizedString(resource: MR.strings.shared.done)) { dismiss() } Button(IosLocalizationKt.localizedString(resource: MR.strings.shared.done)) { dismiss() }
} }
@@ -74,6 +87,23 @@ struct CurrencyPickerSheet: View {
text: $searchQuery, text: $searchQuery,
prompt: IosLocalizationKt.localizedString(resource: MR.strings.shared.search_currencies_placeholder) prompt: IosLocalizationKt.localizedString(resource: MR.strings.shared.search_currencies_placeholder)
) )
.alert(
IosLocalizationKt.localizedString(resource: MR.strings.shared.reset_selected_currencies_confirmation_title),
isPresented: $showResetConfirmation
) {
Button(
IosLocalizationKt.localizedString(resource: MR.strings.shared.reset_selected_currencies_button),
role: .destructive
) {
onReset()
}
Button(IosLocalizationKt.localizedString(resource: MR.strings.shared.cancel), role: .cancel) {}
} message: {
Text(IosLocalizationKt.localizedFormattedString(
resource: MR.strings.shared.reset_selected_currencies_confirmation_message,
args: [currentCurrency.code]
))
}
#if os(macOS) #if os(macOS)
// macOS sizes a .sheet() to its content's ideal size rather than the parent window's // macOS sizes a .sheet() to its content's ideal size rather than the parent window's
// size (unlike iOS, which presents modally full-size); without an explicit frame here, // size (unlike iOS, which presents modally full-size); without an explicit frame here,
@@ -63,6 +63,7 @@ class IosPriceViewModel {
fun onFiatAmountChanged(code: String, value: String) = viewModel.onFiatAmountChanged(code, value) fun onFiatAmountChanged(code: String, value: String) = viewModel.onFiatAmountChanged(code, value)
fun onFiatCurrencyToggled(code: String) = viewModel.onFiatCurrencyToggled(code) fun onFiatCurrencyToggled(code: String) = viewModel.onFiatCurrencyToggled(code)
fun onFiatCurrenciesReordered(newOrder: List<String>) = viewModel.onFiatCurrenciesReordered(newOrder) fun onFiatCurrenciesReordered(newOrder: List<String>) = viewModel.onFiatCurrenciesReordered(newOrder)
fun onSelectedCurrenciesReset() = viewModel.onSelectedCurrenciesReset()
fun onSourceSelected(name: String) = viewModel.onSourceSelected(name) fun onSourceSelected(name: String) = viewModel.onSourceSelected(name)
fun onManualRateChanged(value: String) = viewModel.onManualRateChanged(value) fun onManualRateChanged(value: String) = viewModel.onManualRateChanged(value)
fun onManualSatsPerCurrencyChanged(value: String) = viewModel.onManualSatsPerCurrencyChanged(value) fun onManualSatsPerCurrencyChanged(value: String) = viewModel.onManualSatsPerCurrencyChanged(value)
@@ -27,6 +27,7 @@ import androidx.compose.material.icons.filled.KeyboardDoubleArrowUp
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@@ -120,6 +121,7 @@ fun PriceScreen(
CurrencyPickerScreen( CurrencyPickerScreen(
state = state, state = state,
onToggle = viewModel::onFiatCurrencyToggled, onToggle = viewModel::onFiatCurrencyToggled,
onReset = viewModel::onSelectedCurrenciesReset,
onDone = { showCurrencyPicker = false }, onDone = { showCurrencyPicker = false },
) )
return return
@@ -501,8 +503,32 @@ private fun CurrencyRowMenu(
private fun CurrencyPickerScreen( private fun CurrencyPickerScreen(
state: ConverterUiState, state: ConverterUiState,
onToggle: (String) -> Unit, onToggle: (String) -> Unit,
onReset: () -> Unit,
onDone: () -> Unit, onDone: () -> Unit,
) { ) {
var showResetConfirmation by remember { mutableStateOf(false) }
if (showResetConfirmation) {
AlertDialog(
onDismissRequest = { showResetConfirmation = false },
title = { Text(stringResource(MR.strings.reset_selected_currencies_confirmation_title)) },
text = {
Text(stringResource(MR.strings.reset_selected_currencies_confirmation_message, state.defaultCurrencyCode))
},
confirmButton = {
TextButton(
onClick = {
showResetConfirmation = false
onReset()
},
) { Text(stringResource(MR.strings.reset_selected_currencies_button)) }
},
dismissButton = {
TextButton(onClick = { showResetConfirmation = false }) { Text(stringResource(MR.strings.cancel)) }
},
)
}
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -515,7 +541,14 @@ private fun CurrencyPickerScreen(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text(stringResource(MR.strings.currencies_section_title), style = MaterialTheme.typography.headlineSmall) Text(stringResource(MR.strings.currencies_section_title), style = MaterialTheme.typography.headlineSmall)
TextButton(onClick = onDone) { Text(stringResource(MR.strings.done)) } Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (state.selectedFiatCurrencies.size > 1) {
TextButton(onClick = { showResetConfirmation = true }) {
Text(stringResource(MR.strings.reset_selected_currencies_button))
}
}
TextButton(onClick = onDone) { Text(stringResource(MR.strings.done)) }
}
} }
var searchQuery by remember { mutableStateOf("") } var searchQuery by remember { mutableStateOf("") }
@@ -207,6 +207,16 @@ class PriceViewModel(
persistSelectionIfChanged(_uiState.value.selectedFiatCurrencies) persistSelectionIfChanged(_uiState.value.selectedFiatCurrencies)
} }
/** Removes every selected currency except the pinned [defaultCurrencyCode]. */
fun onSelectedCurrenciesReset() {
_uiState.update { state ->
if (state.selectedFiatCurrencies == listOf(defaultCurrencyCode)) return@update state
val newState = state.copy(selectedFiatCurrencies = listOf(defaultCurrencyCode))
rates?.let { recomputeFromKnownField(newState, it) } ?: newState
}
persistSelectionIfChanged(_uiState.value.selectedFiatCurrencies)
}
/** /**
* Reorders the selected currencies to [newOrder]. Any code in [newOrder] that isn't * Reorders the selected currencies to [newOrder]. Any code in [newOrder] that isn't
* currently selected is ignored, and any currently-selected code missing from [newOrder] * currently selected is ignored, and any currently-selected code missing from [newOrder]
@@ -3,6 +3,7 @@
<string name="bitcoin_section_title">Bitcoin</string> <string name="bitcoin_section_title">Bitcoin</string>
<string name="btc_label">BTC</string> <string name="btc_label">BTC</string>
<string name="btc_to_currency">BTC to %1$s</string> <string name="btc_to_currency">BTC to %1$s</string>
<string name="cancel">Cancel</string>
<string name="clear_search_content_description">Clear search</string> <string name="clear_search_content_description">Clear search</string>
<string name="currencies_section_title">Currencies</string> <string name="currencies_section_title">Currencies</string>
<string name="currencies_selected_count">%1$d selected</string> <string name="currencies_selected_count">%1$d selected</string>
@@ -22,6 +23,9 @@
<string name="priced_currencies_section_title">Priced Currencies</string> <string name="priced_currencies_section_title">Priced Currencies</string>
<string name="refresh_content_description">Refresh</string> <string name="refresh_content_description">Refresh</string>
<string name="remove_currency_content_description">Remove</string> <string name="remove_currency_content_description">Remove</string>
<string name="reset_selected_currencies_button">Reset</string>
<string name="reset_selected_currencies_confirmation_message">This keeps %1$s, but removes every other currency you\'ve added.</string>
<string name="reset_selected_currencies_confirmation_title">Remove all selected currencies?</string>
<string name="retry">Retry</string> <string name="retry">Retry</string>
<string name="sats_label">Sats</string> <string name="sats_label">Sats</string>
<string name="search_currencies_placeholder">Search currencies</string> <string name="search_currencies_placeholder">Search currencies</string>