Commit Graph
122 Commits
Author SHA1 Message Date
tyiuandClaude Sonnet 5 a0dce39925 Preload Compose Skia WASM chunks to fix serial load waterfall
Deploy website to GitHub Pages / deploy (push) Canceled after 0s
Skiko's Skia-via-WebAssembly runtime is loaded through a dynamic
import() inside webApp.js, so the browser previously only discovered
those multi-MB .wasm files after fully downloading and executing the
JS bundle. Inject <link rel="preload"> hints for the content-hashed
.wasm output into index.html after each browser distribution build so
the WASM fetch starts in parallel with the JS instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 15:43:18 +03:00
tyiuandClaude Sonnet 5 29512d153a Update screenshots for iOS 27 and iPadOS 27
Deploy website to GitHub Pages / deploy (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 13:52:29 +03:00
tyiuandClaude Sonnet 5 1852d790ac Deploy web app to satsprice.app/app/ and link it from the website
Deploy website to GitHub Pages / deploy (push) Canceled after 0s
The GitHub Pages workflow now builds the Compose wasmJs web app and
publishes it alongside the marketing site, so it's reachable at
/app/. Add nav/hero/download links to it and list Web among the
supported platforms. Add website/serve-local.sh to reproduce the
production layout locally, and document the setup in README/AGENTS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 08:40:08 +03:00
tyiuandClaude Sonnet 5 e387f2906c Point zapstore website field and README to satsprice.app
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 08:17:27 +03:00
tyiuandClaude Sonnet 5 4891f7ecf9 Add GitHub Pages deployment workflow for website
Deploy website to GitHub Pages / deploy (push) Canceled after 0s
Publishes website/ via GitHub Actions on every push to main that
touches it, and configures the custom domain satsprice.app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AejmWBrdE3DbJWbXoaE3aX
2026-09-14 17:48:19 +03:00
tyiuandClaude Sonnet 5 bc483144cd Add missing library attributions to README
Credits Ktor, SQLDelight, moko-resources, kotlinx.coroutines/serialization/datetime,
kotlin-wrappers, and kotlin-multiplatform-bignum, which were used but not listed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AejmWBrdE3DbJWbXoaE3aX
2026-09-14 17:05:06 +03:00
tyiuandClaude Sonnet 5 b984c19711 Add marketing website for SatsPrice
Static, dark-themed landing page showcasing the app's capabilities with
real screenshots for Android, iOS, iPad, and macOS, plus download links
for all supported distribution channels.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AejmWBrdE3DbJWbXoaE3aX
2026-09-14 15:41:55 +03:00
tyiu 85b1f08d0c Add icon to fastlane images directory 2026-09-14 09:13:18 +03:00
tyiuandClaude Sonnet 5 84fcbb5753 Add Fastlane metadata for F-Droid (title, descriptions, changelog, screenshots)
F-Droid's RFP scanner flagged the repo for missing Fastlane metadata
(https://gitlab.com/fdroid/rfp/-/work_items/4395#note_3827250669):
without it, F-Droid can't show app screenshots and description edits
require going through the F-Droid team directly.

Screenshots are symlinked from docs/assets/screenshots/android/ rather
than duplicated, keeping one source of truth. Also drops the stock
fastlane .gitignore rule that was silently excluding these PNGs --
unlike Play Store screenshots, F-Droid reads them straight from the
repo with no separate regeneration step, so they're meant to be
committed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-14 07:52:19 +03:00
tyiuandClaude Sonnet 5 147267200f Remove unused foojay-resolver-convention plugin
Auto-downloads a JDK toolchain over the network when the required
version isn't installed locally, which conflicts with F-Droid's
offline/reproducible build requirements and was flagged by their RFP
scanner (https://gitlab.com/fdroid/rfp/-/work_items/4395#note_3827250669)
as a network-access risk. Confirmed unused: no module in this project
declares an explicit jvmToolchain, so it was inert KMP-wizard
scaffolding to begin with.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-14 07:51:20 +03:00
tyiuandClaude Sonnet 5 0d29bd29b4 Fix Gradle wrapper version mismatch, add distribution checksum
gradle-wrapper.jar was still 9.1.0's wrapper while gradle-wrapper.properties
declared 9.3.1, and the distribution had no distributionSha256Sum to verify
the download against. Flagged by F-Droid's RFP scanner
(https://gitlab.com/fdroid/rfp/-/work_items/4395#note_3827250669); fixed
via the standard `./gradlew wrapper --gradle-version ... --gradle-distribution-sha256-sum ...`
regeneration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-14 07:51:04 +03:00
tyiu 39ed9ecd35 Update README download buttons 2026-09-13 23:23:15 +03:00
tyiuandClaude Sonnet 5 c1acd92d9d Bump desktopApp packageVersion to match the app's real version
Was still the stale 1.0.0 default from when the module was
scaffolded, instead of the 2.0.0 shared by Android/iOS/macOS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-13 23:10:55 +03:00
tyiu 46064e1783 Add iosApp/release to .gitignore 2026-09-13 23:08:10 +03:00
tyiu 4fd333e799 Update zapstore.yaml 2026-09-13 23:07:09 +03:00
tyiuandClaude Sonnet 5 db270c8aa3 Add app screenshots to README
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-13 22:27:50 +03:00
tyiu 3e222c38bf Enable hardened runtime for macOS 2026-09-13 22:23:12 +03:00
tyiu 0411ef1fb1 Add androidApp/release to .gitignore 2026-09-13 22:13:11 +03:00
tyiuandClaude Sonnet 5 b856454df5 Clean up templated text in README's Kotlin Multiplatform section
The module-layout bullets were still generic KMP-wizard boilerplate
("your project", a hypothetical CoreCrypto example never used here)
and omitted macOS, androidApp, desktopApp, and webApp entirely.
Replaces them with an accurate description of the actual layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-12 12:43:54 +03:00
tyiu 10b1b914b7 Remove centering div in README.md 2026-09-12 12:26:28 +03:00
tyiu c9074e6e4d Bump build version to 13 2026-09-12 12:02:59 +03:00
tyiuandClaude Sonnet 5 c6e54e6536 Add a way to dismiss the keyboard on iOS
NumericField's .decimalPad/.numberPad keyboards have no Return/Done
key of their own, and there was previously no way to dismiss them
once shown. Tried .scrollDismissesKeyboard(.interactively) alone
first, but it's unreliable in a Form: sections without .onMove/
.onDelete (Price Source, Bitcoin) don't consistently wire up the
drag-to-dismiss gesture, so it only worked when dragging within the
Currencies section. Adds a "Done" button to the keyboard toolbar as a
deterministic fallback that works from any field, keeping the scroll
dismissal too since it's a free bonus where it does work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-12 11:55:56 +03:00
tyiuandClaude Sonnet 5 92bd63d07b Bump build number for release, keep version at 2.0.0
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-11 15:55:41 +03:00
tyiuandClaude Sonnet 5 c45c4446e6 Fix digit corruption in Apple digit grouping for very large numbers
localizedGroupedInteger() routed the digit string through
NSDecimalNumber before formatting, which has a ~38-significant-digit
precision ceiling — past that, it silently replaced the excess
trailing digits with zeros. Since every field re-groups on each edit,
a Sats amount typed past ~38 digits kept re-corrupting through this on
every keystroke, visibly changing without the user typing anything new.

Groups the digit string directly instead, using only the locale's
grouping metadata (separator + primary/secondary group sizes) — pure
string manipulation with no numeric precision limit. Verified against
NSNumberFormatter's own output across multiple locales (including
hi-IN's irregular "12,34,567" grouping) and added a regression test
for a 60-digit input.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-11 15:11:53 +03:00
tyiuandClaude Sonnet 5 4388350230 Widen macOS amount fields so large values don't clip
NumericField's macOS-only frame was hard-capped at 140pt, but the
app's own max BTC supply (21,000,000) means Sats amounts can reach a
~21-character grouped string, which didn't fit — the field visibly
clipped once typed input exceeded the cap. Lets it grow up to 220pt
instead of a fixed 140pt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-11 15:11:45 +03:00
tyiuandClaude Sonnet 5 5411455d88 Add missing macOS app icon sizes
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
2026-09-11 15:11:38 +03:00
tyiu 05d7873c7d Replace deprecated usage of monthNumber and dayOfMonth on LocalDateTime 2026-09-11 11:30:53 +03:00
tyiuandClaude Sonnet 5 3f15757449 Fix stale docs claiming web persistence is in-memory only
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
2026-09-11 07:21:56 +03:00
tyiuandClaude Sonnet 5 feaa637c53 Correct AGENTS.md on Desktop/Web release status
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
2026-09-11 00:11:04 +03:00
tyiuandClaude Sonnet 5 5caedc28d8 Drop stale update-readme branch reference from AGENTS.md
That branch has been deleted (local and remote), so the warning about
it no longer applies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
2026-09-11 00:02:58 +03:00
tyiuandClaude Sonnet 5 2c034a275c Update AGENTS.md for the kmp -> main merge
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
2026-09-10 23:58:39 +03:00
tyiuandClaude Sonnet 5 c00f124b28 Update README, add AGENTS.md
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
2026-09-10 16:30:39 +03:00
tyiuandClaude Sonnet 5 a6398e8926 Move Compose's status line under the Price Source card, matching iOS
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
2026-09-10 16:13:14 +03:00
tyiuandClaude Sonnet 5 cd37d5c472 Drop the source name from the status line, make it localized
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
2026-09-10 15:45:52 +03:00
tyiuandClaude Sonnet 5 70ce0bccb1 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
2026-09-10 15:29:40 +03:00
tyiuandClaude Sonnet 5 0cfa52de56 Split the currency picker's browsable list into Priced/Unpriced
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
2026-09-10 15:16:27 +03:00
tyiuandClaude Sonnet 5 cc1495a899 Expand withdrawn currency list with Wikipedia's full historical table
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
2026-09-10 15:03:33 +03:00
tyiuandClaude Sonnet 5 9a064b55d9 Also filter web currencies by year annotation in their display name
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
2026-09-10 14:58:45 +03:00
tyiuandClaude Sonnet 5 468f24a0b9 Revert to the withdrawn-currency exclusion list, extend it
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
2026-09-10 14:49:31 +03:00
tyiuandClaude Sonnet 5 9ba812c82d Switch web's currency filter from exclude-list to allowlist
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
2026-09-10 14:38:07 +03:00
tyiuandClaude Sonnet 5 e03e742b50 Exclude withdrawn currencies from the web currency list
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
2026-09-10 14:31:05 +03:00
tyiuandClaude Sonnet 5 f7fdb2b823 Use the browser's own locale for web currency display names
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
2026-09-10 14:15:15 +03:00
tyiuandClaude Sonnet 5 bb4602e8fa Resolve "EU" through platform locale data instead of hardcoding it
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
2026-09-10 13:21:24 +03:00
tyiuandClaude Sonnet 5 4f190308a3 Warm the region display-name cache at startup
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
2026-09-10 12:39:24 +03:00
tyiuandClaude Sonnet 5 3bdf320f5c Cache region display-name lookups used by currency search
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
2026-09-10 12:35:39 +03:00
tyiuandClaude Sonnet 5 293c822c86 Reject decimal input in the manual "currency to Sats" field
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
2026-09-10 12:24:13 +03:00
tyiuandClaude Sonnet 5 69762133fe Show a fixed, generic message for fetch errors instead of e.message
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
2026-09-10 12:01:15 +03:00
tyiuandClaude Sonnet 5 3ce3bab7c6 Format the "updated" timestamp as a locale-aware date/time
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
2026-09-10 11:57:19 +03:00
tyiuandClaude Sonnet 5 43b66b6665 Keep the "currency to Sats" field visible, make it editable in Manual
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
2026-09-10 11:16:35 +03:00
tyiuandClaude Sonnet 5 bf91abdd12 Keep "currency to Sats" empty when the BTC rate is, and whole-Sats only
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
2026-09-10 10:21:26 +03:00