feat(web): full NFC card reading over WebUSB#231
Merged
codebutler merged 26 commits intomasterfrom Feb 16, 2026
Merged
Conversation
…led 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>
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>
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>
…rage persistence Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…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>
…rce 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>
Adds suspend to PN533Transport.sendCommand() and sendAck(), and propagates through all PN533 controller methods. WebUsbPN533Transport now delegates sendCommand() to sendCommandAsync() instead of throwing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CardTransceiver.transceive(), ClassicTechnology.readBlock()/auth, UltralightTechnology.readPages()/transceive(), FeliCaTagAdapter I/O methods, and VicinityTechnology.transceive() are now suspend functions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mechanical change: adds suspend keyword to all implementations of CardTransceiver, ClassicTechnology, UltralightTechnology, FeliCaTagAdapter, and VicinityTechnology across PN533, PCSC, Android, and iOS source sets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
No JVM-specific dependencies — now available for all platforms including wasmJs/web. Methods updated to suspend per FeliCaTagAdapter interface changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DesfireProtocol, ISO7816Protocol, CEPASProtocol, and UltralightProtocol now use suspend functions for NFC I/O operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add suspend to all card reader entry points and internal methods: - DesfireCardReader: readCard, readApplications, readFiles, readFile, etc. - ClassicCardReader: readCard - UltralightCardReader: readCard, detectCardType - FeliCaReader: readTag - CEPASCardReader: readCard - ISO7816CardReader: readCard, tryReadApplication, readSfiFile, etc. - VicinityCardReader: readCard - ISO7816Dispatcher: readCard, tryISO7816 Also update AppConfig lambda types to suspend function types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Desktop: wrap card reading in runBlocking on poll thread, replace Thread.sleep with coroutine delay. Android: propagate suspend through TagReader base and all implementations. iOS: use runBlocking on GCD worker queue for suspend card readers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add kotlinx-coroutines-test dependency to card sub-modules. Update mock/fake implementations to use suspend overrides. Wrap test methods calling suspend functions in runTest. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the detection-only stub with complete card reading for all card types supported by PN533 USB readers: DESFire, MIFARE Classic, MIFARE Ultralight, FeliCa, CEPAS, and ISO 7816. Uses the same card reader pipeline as desktop/Android/iOS, now possible because all NFC I/O interfaces are suspend-compatible. Removed duplicate inListPassiveTarget/parseTarget helper methods in favor of the standard PN533 class methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sendAck() and getFirmwareVersion() are now suspend functions after the NFC I/O suspend refactoring, so discoverBackends() needs to be suspend too. It's already called from within a coroutine. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Kotlin/Wasm (wasmJs) test task downloads Node.js from nodejs.org to execute tests. Add it to the firewall allowlist so `./gradlew allTests` works in the devcontainer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All conflicts resolved by keeping our branch's suspend-compatible versions of interfaces, implementations, and tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Kotlin/Wasm IR linker hangs when compiling test executables for all library modules. These modules get sufficient test coverage from jvmTest. Only app:web needs wasmJs test compilation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…vmTest The Kotlin/Wasm IR linker hangs when linking executables for library modules. Disable all wasmJs executable and test tasks for non-web modules — they only need klib compilation for app:web to consume, and get test coverage from jvmTest. Also switch CI from allTests (which includes iOS/wasmJs targets not available on ubuntu) to jvmTest. iOS tests run in the separate ios job. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lint runs first as a gate, then test/android/desktop/web/ios all run in parallel. Failures are isolated and the overall pipeline is faster. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…firewall Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The production wasmJsBrowserDistribution triggers expensive compileProductionExecutableKotlinWasmJsOptimize. The development distribution verifies compilation and linking without the slow optimization step. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
CardTransceiver,ClassicTechnology,UltralightTechnology,FeliCaTagAdapter,VicinityTechnology)suspend-compatible, bridging the sync/async mismatch between platform NFC APIs and WebUSB's Promise-based APIWebCardScannersupporting DESFire, MIFARE Classic, MIFARE Ultralight, FeliCa, CEPAS, and ISO 7816 cards — using the same readers as desktop/Android/iOSPN533Transportand allPN533protocol methods suspend, enabling WebUSB's async bulk transfers to work seamlessly through Kotlin coroutinesrunBlockingat platform boundaries where neededrunTestwrappers and suspend mock overridesnodejs.orgin devcontainer firewall for Kotlin/Wasm test runnerTest plan
./gradlew :card:jvmTest :card:desfire:jvmTest :card:classic:jvmTest :card:ultralight:jvmTest :card:felica:jvmTest :card:iso7816:jvmTest :card:vicinity:jvmTest)./gradlew :app:desktop:compileKotlinJvm)ktlintFormatpasses cleanly🤖 Generated with Claude Code