You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add web (wasmJs) target with FormattedString refactor (#226)
* refactor: replace StringResource/runBlocking with FormattedString sealed class
Introduce FormattedString sealed class (Literal, Resource, Plural, Concat)
to defer string resolution to the UI layer, eliminating all runBlocking and
getStringBlocking usage that blocked the JS event loop on wasmJs.
Key changes:
- All user-facing string fields (cardName, agencyName, routeName,
stationName, subscriptionName, warning, emptyStateMessage, etc.) now
return FormattedString instead of String
- Remove StringResource interface, DefaultStringResource, TestStringResource,
and all getStringBlocking platform actuals
- Remove ObfuscatedTrip and TripObfuscator (unused)
- Update FareBotUiTree/ListItem/HeaderListItem to use FormattedString
- Update all ~100 transit modules to use FormattedString(Res.string.xxx)
- For @serializable types (Station, TransitBalance), use @transient
formattedName/formattedStationName fields alongside serializable String fields
- Update App.kt, HelpScreen, TripMapScreen, CardViewModel, HistoryViewModel
- Update all test files to use assertFormattedEquals() helper and
assertResourceEquals() for resource key comparison
- wasmJs and JVM targets compile and tests pass
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add wasmJs target to all KMP modules
Add wasmJs { browser() } target to the root build.gradle.kts so all
KMP subprojects automatically get a WebAssembly compilation target.
Guard the compose.desktop.currentOs injection to only apply when the
jvm target exists. Add sqldelight-web-worker-driver to the version
catalog for future use.
Convert getStringBlocking/getPluralStringBlocking to expect/actual
since runBlocking is unavailable on wasmJs. Provide wasmJs actual
implementations for all expect declarations in the base module:
ResourceAccessor, BundledDatabaseDriverFactory, SystemLocale, and
GetStringBlocking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(web): add wasmJs platform actuals for app module
Provide wasmJs actual implementations for the three expect declarations
in the app module:
- DeviceRegion: reads navigator.language to extract country code
- CardsMapScreen: no-op (platformHasCardsMap = false), matching JVM
- TripMapScreen: no-op composable, matching JVM
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(web): add web app shell with entry point, DI graph, and localStorage persistence
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract PN533 protocol to commonMain, add WebUSB transport stub
Move pure-logic PN533 files (PN533.kt, PN533CardInfo.kt, PN533CardTransceiver.kt,
PN533CommunicateThruTransceiver.kt, PN533ClassicTechnology.kt, PN533UltralightTechnology.kt)
from jvmMain to commonMain so they can be used on all Kotlin targets.
Create a PN533Transport interface in commonMain with PN533Exception and
PN533CommandException. Rename the usb4java implementation to Usb4JavaPN533Transport
(stays in jvmMain). Add a WebUsbPN533Transport stub in wasmJsMain as a placeholder
for future WebUSB JS interop.
Replace String.format() calls with multiplatform-compatible hex formatting.
Remove Thread.sleep(10) from PN533.resetMode() (not available in commonMain,
negligible delay).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix up web
* feat(web): implement WebUSB NFC scanning, file picker, and MDST resource loading
Replaces stub implementations with working WebUSB PN533 transport (card
detection via poll loop), browser file picker for JSON/binary import, and
synchronous XHR-based MDST resource loading for wasmJs. Updates tests to
use runTest instead of runBlocking for wasmJs compatibility. Adds web
build step to CI and updates CLAUDE.md to reflect web target and
FormattedString conventions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@codebutler.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+36-25Lines changed: 36 additions & 25 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
## Project Overview
4
4
5
-
FareBot is a Kotlin Multiplatform (KMP) Android/iOS app for reading NFC transit cards. It is being ported from/aligned with [Metrodroid](https://github.com/metrodroid/metrodroid).
5
+
FareBot is a Kotlin Multiplatform (KMP) Android/iOS/Web app for reading NFC transit cards. It is being ported from/aligned with [Metrodroid](https://github.com/metrodroid/metrodroid). The web target uses Kotlin/Wasm (wasmJs) with WebUSB for NFC reader support.
6
6
7
7
**Metrodroid source code is in the `metrodroid/` directory in this repo.** Always use this local copy for comparisons and porting — do not fetch from GitHub.
8
8
@@ -32,10 +32,11 @@ When porting code from Metrodroid: **do a faithful port**. Do not simplify, abbr
-`String` (user-facing) → `FormattedString` (sealed class in `base/util/`)
39
40
40
41
Do NOT:
41
42
- Skip features "for later"
@@ -57,24 +58,30 @@ Do NOT make speculative changes hoping they fix the issue. Each failed guess was
57
58
58
59
### 5. All code in commonMain unless it requires OS APIs
59
60
60
-
Write all code in `src/commonMain/kotlin/`. Only use `androidMain`or `iosMain` for code that directly interfaces with platform APIs (NFC hardware, file system, UI system dialogs). No Objective-C. Tests use `kotlin.test`.
61
+
Write all code in `src/commonMain/kotlin/`. Only use `androidMain`, `iosMain`, or `wasmJsMain` for code that directly interfaces with platform APIs (NFC hardware, file system, UI system dialogs, WebUSB). No Objective-C. Tests use `kotlin.test`.
61
62
62
-
### 6. Use StringResource for all user-facing strings
63
+
### 6. Use FormattedString for all user-facing strings
64
+
65
+
All user-facing strings use the `FormattedString` sealed class, which defers string resolution to the UI layer (avoiding `runBlocking` that blocks the JS event loop on wasmJs).
63
66
64
-
All user-facing strings must go through Compose Multiplatform resources:
65
67
- Define strings in `src/commonMain/composeResources/values/strings.xml`
66
-
- For UI labels in `TransitInfo.getInfo()`, use `ListItem(Res.string.xxx, value)` or `HeaderListItem(Res.string.xxx)` directly
67
-
- For dynamic string formatting, use `runBlocking { getString(Res.string.xxx) }`
68
-
- Legacy pattern: Pass `StringResource` to factories — still works but not required for new code
68
+
- Use `FormattedString(Res.string.xxx)` for resource-backed strings
69
+
- Use `FormattedString("literal")` for dynamic/computed strings
70
+
- Use `FormattedString(Res.string.xxx, arg1, arg2)` for formatted strings
71
+
- Use `FormattedString.plural(Res.plurals.xxx, count, args...)` for plurals
72
+
- Concatenate with `+` operator: `FormattedString("a") + FormattedString("b")`
73
+
74
+
The UI resolves strings via `@Composable formattedString.resolve()` or `suspend formattedString.resolveAsync()`.
69
75
70
76
Example patterns:
71
77
```kotlin
72
-
//Preferred for static labels
73
-
ListItem(Res.string.card_type, cardType)
74
-
HeaderListItem(Res.string.card_details)
78
+
//In transit modules — return FormattedString, not String
-`/Users/eric/Code/farebot/REMAINING-WORK.md` — tracked remaining work
109
-
- Session transcripts in `/Users/eric/.claude/projects/-Users-eric-Code-farebot/`
110
-
111
-
When continuing from a previous session, read these files to recover context rather than starting from scratch.
113
+
When continuing from a previous session, check for implementation plans and session transcripts in `~/.claude/` to recover context rather than starting from scratch.
112
114
113
115
## Build Commands
114
116
115
117
```bash
116
-
./gradlew allTests # Run all tests
117
-
./gradlew assemble # Full build (Android + iOS frameworks)
118
+
./gradlew allTests # Run all tests
119
+
./gradlew assemble # Full build (Android + iOS + Web)
118
120
./gradlew :app:android:assembleDebug # Android only
121
+
./gradlew :app:web:wasmJsBrowserDistribution # Web (Wasm) only
Copy file name to clipboardExpand all lines: README.md
+5-1Lines changed: 5 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
Read your remaining balance, recent trips, and other information from contactless public transit cards using your NFC-enabled Android or iOS device.
4
4
5
-
FareBot is a [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) app built with [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/), targeting Android (NFC), iOS (CoreNFC), and macOS (experimental, via PC/SC smart card readers or PN533 raw USB NFC controllers).
5
+
FareBot is a [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) app built with [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/), targeting Android (NFC), iOS (CoreNFC), macOS (experimental, via PC/SC smart card readers or PN533 raw USB NFC controllers), and Web (experimental, via WebAssembly).
6
6
7
7
## Platform Compatibility
8
8
@@ -184,6 +184,7 @@ Some MIFARE Classic cards require encryption keys to read. You can obtain keys u
184
184
***Android:** NFC-enabled device running Android 6.0 (API 23) or later
185
185
***iOS:** iPhone 7 or later with iOS support for CoreNFC
186
186
***macOS** (experimental): Mac with a PC/SC-compatible NFC smart card reader (e.g., ACR122U), a PN533-based USB NFC controller (e.g., SCL3711), or a Sony RC-S956 (PaSoRi) USB NFC reader
187
+
***Web** (experimental): Any modern browser with WebAssembly support. Card data can be imported from JSON files exported by other platforms. NFC reading via WebUSB is planned but not yet implemented.
187
188
188
189
## Building
189
190
@@ -201,6 +202,8 @@ $ make # show all targets
201
202
|`make ios-sim`| Build iOS app for simulator |
202
203
|`make ios-install`| Build and install on connected iOS device (auto-detects device) |
203
204
|`make desktop`| Run macOS desktop app (experimental) |
205
+
|`make web`| Build web app (experimental, WebAssembly) |
206
+
|`make web-run`| Run web app dev server with hot reload |
204
207
|`make test`| Run all tests |
205
208
|`make clean`| Clean all build artifacts |
206
209
@@ -257,6 +260,7 @@ Compatible with [Zed](https://zed.dev/docs/dev-containers), VS Code (Remote - Co
0 commit comments