AppIcon.appiconset only defined an iOS "universal" 1024x1024 entry, so
the macOS build had no mac-idiom icon at all — App Store Connect
rejected uploads for missing the required 512pt@2x (1024x1024) image.
Generates the full mac icon set (16/32/128/256/512pt at 1x/2x) from
the existing 1024px source and wires it into Contents.json.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Web actually persists via localStorage (LocalStorageStores.kt), which
survives page reloads — only SQLDelight itself isn't used there, since
its web driver needs a worker plus a wasm sqlite binary. The doc
comments and AGENTS.md still described the in-memory state this
replaced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Desktop and Web are real future release targets, not permanent
dev-only/preview builds — they just lack a release/CI pipeline yet.
desktopApp already has full DMG/MSI/DEB packaging configured, and
webApp has production-build groundwork in place.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Drop the "developed on kmp, not main" framing now that kmp is merging
into main, which becomes the sole Kotlin Multiplatform implementation.
Keeps the warning about stale pre-rewrite branches (e.g. update-readme)
still describing the old Skip/SPM workflow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
README: drop the stale Skip attribution line (the project moved to
Kotlin Multiplatform entirely; the "Kotlin Multiplatform" line below
it already covers the current tooling) and fix the stated minimum
Android version — androidApp's actual minSdk is 24 (Android 7.0), not
10.0.
AGENTS.md: a navigation/orientation doc for AI agents, focused on the
things that aren't obvious from reading the code or that took real
trial-and-error to establish this session — the localization
architecture constraint (no synchronous, cross-platform way to
resolve a moko-resources string outside Compose/Apple, so shared code
must return data, not sentences), the Swift-interop plain-function
convention, per-platform currency-data quirks (web's ICU-dependent
currency list, missing color-emoji font), and the verification
workflow for each platform (especially that Compose for Web renders
to a <canvas>, so headless Chrome + specific GL flags is the way to
actually see a change, not DOM inspection).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Was above the card, right under the "SatsPrice" title; now it's the
last item inside the card's content, below "USD to Sats" — matching
where iOS's Section footer renders it. Muted with onSurfaceVariant to
read as a footer-style caption rather than primary content.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
statusLine() used to build the whole "via Coinbase, updated ..."
string in shared code with hardcoded English literals ("updated",
"loading rates…", "via ") — not localizable, since this shared code
has no Composable/moko-resources context to resolve a string from,
and said more than needed (the source is already shown in the Price
Source picker right above it).
Replaced with lastUpdatedDateTime(): String?, which returns only the
locale-formatted date/time (still shared, since that part is genuine
platform-native locale formatting, not translated text) — null for
Manual, or while the first fetch hasn't completed. Each UI layer now
builds the localized "Updated %1$s" / "Loading rates…" text itself
via stringResource()/IosLocalizationKt around that value, using two
new string resources.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
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
The "Currencies" section (not-yet-added currencies) is now two:
"Priced Currencies" and "Unpriced Currencies", partitioned by
whether the active source quotes a rate for each one. "Current
Currency" and "Selected Currencies" are unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Pulled the complete "Historical currency codes" table from
https://en.wikipedia.org/wiki/ISO_4217#Historical_codes and added
every code not already covered — ~85 more entries (Argentine austral,
Brazilian cruzeiro/cruzado through its several redenominations,
Yugoslav dinar variants, Bulgarian lev's earlier revisions, settlement/
funds-only codes like XEU/XFO/XFU/XRE, and more).
Cross-checked the full 127-entry list against the JVM's live-derived
"currently active" set to make sure nothing genuinely active got
added by mistake — clean, except for one deliberate exception:
Wikipedia lists "BGN" (a fourth, euro-replaced Bulgarian lev) as
historical, but the JVM still resolves BGN as Bulgaria's current
currency, so it's left out to stay consistent with what this app's
own JVM/Android path actually does — documented inline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
CLDR appends a "(1927–2002)" or "(2009–2024)" style validity-period
suffix to a historical/superseded currency's display name — a
broader, data-driven signal than the hand-maintained
WITHDRAWN_CURRENCY_CODES list, catching ones not added there yet
(confirmed AFA, ALK, AOK, AON, AOR, ROL, TRL, RUR, and others all get
this treatment). Not a full replacement for the list though: some
withdrawn currencies (pre-euro ones especially, e.g. "German Mark"
for DEM) carry no such hint in every ICU dataset.
Verified with Node against the real Intl.DisplayNames output: matches
every year-annotated name actually observed, and produces zero false
positives across all 162 currently-formattable codes (Node's
Intl.supportedValuesOf('currency') set) — the only two matches were
SLL and ZWL, which are already known-withdrawn.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Reverts the allowlist approach back to WITHDRAWN_CURRENCY_CODES per
request, and adds the codes actually reported: AFA, ALK, AOK, AON,
AOR, plus the full set of pre-euro legacy currencies (ATS, BEF, DEM,
FRF, ITL, ESP, GRD, PTE, NLG, IEP, and the rest), which are exactly
the kind of long-withdrawn code an ICU currency dataset would still
carry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
The previous WITHDRAWN_CURRENCY_CODES exclusion list only covered the
5 retired codes I'd found by diffing against the JVM — but which
codes Intl.supportedValuesOf('currency') returns is ICU-version-
dependent, and apparently some browsers' ICU data includes currencies
retired decades ago (AFA, ALK, AOK, AON, AOR, and evidently more)
alongside current ones. An exclusion list can't keep up with an
unbounded, browser-dependent set of historical codes.
Replaced with ACTIVE_CURRENCY_CODES, an allowlist taken directly from
the JVM's own live-derived "currently used" set (verified against a
real JVM run, 155 codes) plus PRECIOUS_METAL_CURRENCY_CODES. This
naturally excludes every retired code regardless of what a given
browser's Intl data happens to expose, rather than needing to know
about each one in advance.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Intl.supportedValuesOf('currency') includes several codes for
currencies that have actually been withdrawn/superseded — unlike
JVM/Android/Apple, which derive "currently used" from each ISO
country's live-assigned currency (java.util.Currency/NSLocale), the
web platform has no equivalent API, so this app was showing them
unfiltered.
Confirmed exactly which codes by diffing the web list against the
JVM's own live-derived set: ANG, CUC, HRK, SLL, and ZWL are genuinely
withdrawn/superseded national currencies, now excluded via a manually
maintained list (the web platform's only option here). XDR and XSU
were also missing from the JVM's set, but only because they aren't
tied to any ISO country — not because they're withdrawn — so those
stay, same reasoning this codebase already applies to precious
metals.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
jsCurrencyDisplayName() was hardcoding 'en' for Intl.DisplayNames,
so every currency name shown on web (USD, JPY, ...) stayed English
regardless of the user's locale — same class of bug as the "EU"
string, just for every currency rather than one region. Switched to
undefined, matching regionDisplayName()'s fix; verified with Node
that this actually produces locale-appropriate names (e.g. "US
Dollar" / "dollar des États-Unis" / "米ドル").
Left jsCurrencyFractionDigits()'s 'en' alone and documented why:
verified with Node that a currency's decimal-digit count doesn't
vary by locale, so locale there is just a required constructor
argument, not something the result depends on.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Verified directly (java.util.Locale, NSLocale, and Intl.DisplayNames
all tested outside the app) that every platform's locale data resolves
"EU" correctly on its own — "European Union" in en, "Union européenne"
in fr, "Europäische Union" in de, etc. — via CLDR, which defines "EU"
as a grouping in its own right despite it not being a real ISO 3166-1
country code. The hardcoded English fallback was based on an
assumption that was never actually verified.
Removes the special case from the shared code entirely. That required
two platform-side fixes to stop blocking "EU" from ever reaching the
platform lookup:
- JVM/Android: dropped the Locale.getISOCountries() membership guard
(which excluded "EU") in favor of detecting an unresolvable code by
getDisplayCountry() echoing it back unchanged.
- Web: jsRegionDisplayName() was hardcoding locale 'en' for
Intl.DisplayNames — changed to undefined (the browser's own locale),
matching what the other platforms already did. Without this fix,
"EU" would resolve, but only ever in English on web.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Previously the cache only filled lazily on first search use, so the
very first character typed into currency search still paid for every
region code at once. Kicking off warmRegionDisplayNameCache() in its
own coroutine from PriceViewModel's init moves that cost to app
startup, well before the user can reach the search field, without
delaying the ViewModel's own startup sequence (separate launch{}, not
folded into it).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
matchesCurrencySearch() re-derives a country name for every currency
on every keystroke; region codes don't change during a session, so
this was redoing the same locale lookups repeatedly — worst on web,
where each lookup constructs a fresh Intl.DisplayNames instance.
Caches by region code instead, including null results (regions the
platform doesn't recognize), so each code's lookup happens once ever.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
It was using the decimal sanitizer/keyboard, same as the rate field
next to it — but Sats are indivisible, so this one should behave like
the existing Sats amount field: integers only, numeric keypad.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Exceptions from the HTTP client or JSON parsing can carry verbose,
technical text (full response bodies, stack traces) — not meaningful
to a user seeing it as a red error banner.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Replaces the raw "2026-09-10 10:03:54" with each platform's own
locale-formatted, no-seconds rendering (e.g. "Sep 10, 2026, 10:03 AM"
in en-US) — mirroring how amounts are already locale-formatted
elsewhere (NumberFormat.kt). Android uses the pre-java.time
Calendar/DateFormat APIs since minSdk 24 predates java.time (API 26)
and the project has no core library desugaring configured.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Always render the field now (label included) with just its value
empty when unknown, rather than hiding the whole field.
In Manual source mode it's now a genuine input: typing into it
recomputes the "BTC to currency" rate the same way typing into that
field recomputes this one. Both edit the same underlying
ManualExchangeRateSource.rate via the same self-inverse division
(CurrencyConverter.satsPerCurrencyUnit of a rate gives Sats, and of a
Sats amount gives a rate back), tracked as a new manualSatsPerCurrencyInput
field alongside the existing manualRateInput.
Wired through PriceViewModel, the iOS bridge/ConverterViewModel, and
both UIs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Clearing the Manual rate cleared pricedCurrencyCodes but left
rateDisplays stale, so "currency to Sats" kept showing a value
computed from the old rate even after "BTC to currency" went empty.
refresh()'s early-return path now clears rateDisplays too.
oneCurrencyToSats() also now derives from defaultCurrencyRate()
directly instead of re-reading rateDisplays independently, so the
two rates structurally can't disagree about whether a rate is known,
and formats to a whole number of Sats (0 decimals) instead of 2 —
fractional Sats aren't a meaningful unit to show.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
"1 BTC to USD" -> "BTC to USD", "1 <currency> to Sats" -> "<currency>
to Sats" (the "1 " was redundant with the field itself now carrying
the label).
Compose: the BTC-to-currency rate and currency-to-sats value are now
OutlinedTextFields (read-only, matching the enabled/disabled styling
of the Currencies section's fields) instead of plain Text, with the
label folded into the field itself rather than a separate heading.
The manual-source rate field's label changes from generic "Rate" to
the same "BTC to <currency>".
iOS/macOS: dropped the Section's "1 BTC to <currency>" header;
the label now sits directly on the rate row (label left, value
right), matching the existing Sats/BTC/currency field rows and the
already-matching "<currency> to Sats" row below it.
rate_label is now unused and removed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Shows the inverse of the existing "1 BTC to <currency>" rate — how
many Sats one unit of the default currency is worth — right below it.
Useful since many currencies are worth less than, or a low multiple
of, a single Sat.
CurrencyConverter.satsPerCurrencyUnit(rate) does the actual division;
ConverterUiState.oneCurrencyToSats() derives the display string from
the already-fetched rate the same way defaultCurrencyRate() does, so
no new ViewModel state was needed. Wired into both Compose and the
iOS bridge/SwiftUI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Compose for Web (js/wasmJs) draws everything through Skia onto a
<canvas> rather than through the browser's own text stack, so it has
no color-emoji font to fall back to — a flag's regional-indicator
codepoints render as empty tofu boxes instead of a flag. Reproduced
via a headless-Chrome screenshot of the running dev server.
Adds a per-platform supportsFlagEmoji() (true everywhere except
js/wasmJs) that currencyFlagEmoji() now checks first, so currency
rows on web just show the plain code/name with no flag rather than a
broken glyph.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Combining .searchable, .toolbar, and .navigationTitle on the same
List triggered "Update ToolbarReader tried to update multiple times
per frame" every time the sheet's first presentation animated in.
Moving .searchable onto the NavigationStack instead — separate from
the .toolbar/.navigationTitle chain on the List — avoids it.
Reproduced and confirmed fixed via the unified log (Console shows the
same warning under com.apple.SwiftUI:Invalid Configuration); the
sheet, search field, and currency list all still render correctly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Searching "Japan" now finds JPY, "Canada" finds CAD, etc. — matching
just the currency's own display name wasn't enough, since e.g.
"Canadian Dollar" doesn't contain "Canada" as a substring.
Adds a per-platform regionDisplayName(regionCode) (java.util.Locale
on JVM/Android, NSLocale on Apple, Intl.DisplayNames on web) and
reuses CurrencyFlag.kt's currency-to-country-code derivation (now
extracted as issuingCountryCodes) to look up the country/countries
behind a currency code. "EU" is hardcoded to "European Union" since
it's not a real ISO 3166-1 code platform locale data can resolve.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Filters by code or display name, case-insensitively, across all three
sections (current, selected, and remaining currencies) — a section
disappears entirely once nothing in it matches. The matching logic
lives in shared code (matchesCurrencySearch) so Compose and SwiftUI
stay in sync; SwiftUI wires it into the native .searchable() field,
Compose gets a plain search text field with a clear button.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
macOS's Form isn't List-backed, so .onMove's drag-to-reorder (which
already worked on iOS) had no effect there — the only way to reorder
was the manual chevron up/down buttons. Those are replaced with real
drag-and-drop via .draggable/.dropDestination, and a right-click
context menu now offers Remove for any non-default currency, since
macOS has no swipe-to-delete gesture either.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
With a single currency shown there's nothing to reorder or remove, so
the per-row "..." menu (Compose) and the Edit button plus swipe-to-
delete/drag-to-reorder (iOS) no longer appear. This also covers
Manual, which always shows just the default currency.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Manual's default-currency field being unpriced just means no rate has
been typed in yet, not an actual pricing error, so the label (and the
red error styling that came with it) no longer applies when it's the
active source.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
"via Manual" had nothing useful left to say once the "updated" part
was dropped, so statusLine() now returns empty for Manual and both
UIs skip rendering it entirely instead of showing a blank line/footer.
onManualRateChanged also now clears manualSource.rate whenever the
field doesn't parse (emptied, or mid-edit on an incomplete number)
rather than only ever updating it on a successful parse. refresh()'s
early-return for a null Manual rate now also clears the cached rates
and pricedCurrencyCodes, so a cleared/incomplete manual rate stops
fiat amounts from continuing to show a stale computed value.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Refreshing has no meaning for a manually-typed rate, so the button
(and its loading spinner) is now hidden while Manual is selected.
Switching to Manual for the first time now seeds its rate field from
whichever source was active before, falling back to any other
source's last cached rate for the default currency. When neither is
available, the rate and amount fields are left empty instead of
showing stale values.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
The manual rate source only ever prices the default currency, so the
currency selector, additional currency rows, and the "updated" part
of the status line no longer make sense for it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
Derives a country flag from the ISO 4217 code's country prefix, with
an explicit map of country codes for currencies shared by multiple
countries (XAF, XCD, XCG, XOF, XPF) showing every issuing country's
flag, and the EU flag as a special case for EUR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
Move up/down/Remove no longer repeat the currency code in the menu item
text — the row's own field already shows it right next to the button.
Also adds Move to top / Move to bottom, enabled under the same conditions
as the existing up/down actions.
Switches from material-icons-core to material-icons-extended: the core set
is a bare ~50-icon subset with nothing suited to "move to top/bottom" (no
double-chevron or vertical-align icons), so KeyboardDoubleArrowUp/Down
needed the fuller set.
Compose-only: the native SwiftUI screen has no three-dot menu, it uses
native list reordering instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
Adds groupDigits(), a display-only formatter that inserts the current
locale's digit-grouping separators into an amount's integer part (e.g.
"1,000,000.5" in en-US vs "1.000.000,5" in de-DE, where the roles of ","
and "." are swapped, or hi-IN's irregular "12,34,567"). Delegates to each
platform's native locale APIs (java.text.NumberFormat/DecimalFormatSymbols,
NSNumberFormatter/NSLocale, Intl.NumberFormat with BigInt for arbitrary
precision) rather than reimplementing grouping rules, and never touches the
canonical '.'-decimal strings the rest of the app parses and stores.
Applied via a Compose VisualTransformation (BTC/Sats/fiat/manual-rate
fields, plus the read-only rate display) and, on the native SwiftUI screen,
by re-deriving the field's displayed text on every change. Also made input
parsing locale-aware: sanitizeDecimalInput now recognizes the locale's own
decimal separator (e.g. what a locale-aware decimal keypad sends), not just
"." — with a stricter rule on the SwiftUI side specifically, since its
single text buffer re-feeds the grouped display back through sanitize, so
a "." can be grouping noise there rather than a decimal point.
The exceeds-max-supply warning's "21,000,000 BTC" is no longer baked into
the localized string with hardcoded grouping; it's substituted in at
runtime via groupDigits so the link-detection text always matches exactly,
in every locale.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
Currency.getAvailableCurrencies() (JVM/Android) and NSLocale.ISOCurrencyCodes
(iOS/macOS) return every ISO 4217 code the platform has ever known about —
withdrawn currencies like the Deutsche Mark and French Franc, and test/
placeholder codes (XTS, XXX), included. Measured on the JVM: 232 total
codes, only 156 still assigned to a country.
Filters the list down to currencies actually assigned to an ISO country
today, which drops the withdrawn ones without needing a maintained
exclusion list. Precious metals (XAU, XAG, XPD, XPT) are added back in
since they're still actively traded despite not being tied to any country.
Web is left unfiltered — there's no reliable country-to-currency API there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
Previously a currency the active price source had no rate for was hidden
entirely, so switching sources (e.g. to CoinGecko, which quotes far fewer
fiat currencies than Coinbase) could silently drop it from the picker and
even from the user's own selection.
Every system currency is now always offered. ConverterUiState tracks which
codes the latest fetch actually priced, and both UIs (Compose and the
native SwiftUI screen, since iOS/macOS don't render Compose UI at all) show
a "Not priced by <source>" indicator for the rest, with the amount field
emptied and disabled rather than showing a stale or meaningless value.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
The earlier -Wl,-U,_sqlite3_enable_load_extension fix only satisfied the
linker; at runtime, Kotlin/Native's cinterop wrapper for that function still
resolves against the symbol the moment SQLDelight's native-driver opens a
connection, and Apple's system libsqlite3 doesn't export it on macOS — so
the app now crashed instead of failing to build.
Adds weak no-op definitions of sqlite3_enable_load_extension and
sqlite3_load_extension, compiled directly into the app (Xcode's
file-system-synchronized group picks the file up with no project.pbxproj
changes needed beyond what Xcode itself rewrote on this build). Weak means
a platform whose system library does provide the real symbol keeps using
that one; this only fills in where it's missing.
Verified there's no longer a crash by running the built macOS binary
directly and by installing/launching it on the iOS Simulator — neither
produced a dyld error or a crash report, versus two reproducible crash
reports beforehand with the exact reported failure signature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
Tapping "21,000,000 BTC" in the exceeds-max-supply warning now sets the BTC
field to that value, on both Compose (Android/Desktop/Web) and the native
SwiftUI screen (iOS/macOS) — Compose UI isn't rendered on iOS/macOS at all,
so that side needed its own AttributedString-based link.
The link is styled to match the surrounding warning text: Compose's
LinkAnnotation otherwise falls back to the theme's accent color unless every
interaction state's style is set explicitly, and SwiftUI renders `.link`
runs in the accent color regardless of the Text's own foregroundColor
modifier unless it's set directly on the link's AttributedString range.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
js("new Foo(x).bar()") can misparse so `new` binds to the whole chain
instead of just the constructor call, throwing "X.resolvedOptions is not a
constructor" at runtime on the JS target (Kotlin/Wasm was unaffected, since
its js() compiles through a different path). Binding the constructor result
to a local variable first removes the ambiguity.
This was also silently breaking systemCurrencies() and the currency display
name lookup, and had been cascading into an unrelated CoinGecko test
failure in the same karma bundle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
Adds a SQLDelight-backed AppDatabase (Android, Desktop, iOS/macOS) so the
converter survives restarts: the last-known rates per price source, the
user's selected fiat currencies (with order preserved), and the last-used
price source all reload before the first network fetch completes. Web isn't
a shipped platform and SQLDelight's driver there needs a worker plus a wasm
sqlite binary, so it gets a lighter localStorage-backed implementation of
the same store interfaces instead.
Android needs an app Context for its driver, wired via a new
SatsPriceApplication. iOS/macOS need libsqlite3 linked explicitly (Swift's
autolinking only covers Shared.framework itself, not its C library
dependencies), plus two extension-loading symbols marked optional since
macOS's system sqlite build omits them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp