Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a636de5
refactor: replace StringResource/runBlocking with FormattedString sea…
Feb 15, 2026
a4385eb
feat: add wasmJs target to all KMP modules
claude Feb 15, 2026
4e77ff7
feat(web): add wasmJs platform actuals for app module
claude Feb 15, 2026
58fca3d
feat(web): add web app shell with entry point, DI graph, and localSto…
Feb 15, 2026
8d3e934
refactor: extract PN533 protocol to commonMain, add WebUSB transport …
claude Feb 15, 2026
76afa5b
fix up web
Feb 16, 2026
1333081
feat(web): implement WebUSB NFC scanning, file picker, and MDST resou…
Feb 16, 2026
f4059a1
refactor: make PN533Transport and PN533 suspend-compatible
Feb 16, 2026
e9e1b1b
refactor: make NFC technology interfaces suspend-compatible
Feb 16, 2026
931f6bf
refactor: add suspend to all NFC technology implementations
Feb 16, 2026
2fa4bd5
refactor: move PN533FeliCaTagAdapter to commonMain
Feb 16, 2026
93ceb17
refactor: make protocol classes suspend-compatible
Feb 16, 2026
708ede1
refactor: make card readers and ISO7816Dispatcher suspend-compatible
Feb 16, 2026
cf2cd49
refactor: update platform call sites for suspend card readers
Feb 16, 2026
0f1700d
test: update tests for suspend NFC interfaces
Feb 16, 2026
ed0e847
feat(web): implement full card reading over WebUSB
Feb 16, 2026
c9ab312
fix: make DesktopCardScanner.discoverBackends() suspend
Feb 16, 2026
da19915
chore(devcontainer): allow nodejs.org for Kotlin/Wasm test runner
Feb 16, 2026
eef09e1
style: apply ktlintFormat to suspend-refactored files
Feb 16, 2026
b5daa22
merge: resolve conflicts with master (keep suspend interfaces)
Feb 16, 2026
71534f1
fix(build): disable wasmJs test compilation for library modules
Feb 16, 2026
c13ca91
fix(ci): disable wasmJs executable linking for library modules, use j…
Feb 16, 2026
96b6aa7
ci: split into parallel jobs per platform
Feb 16, 2026
b6aa31b
ci: run all jobs in parallel with no gate
Feb 16, 2026
332c610
fix: ktlint formatting in build.gradle.kts, allow GH Actions URLs in …
Feb 16, 2026
fc61a98
ci(web): use development build instead of production
Feb 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .devcontainer/init-firewall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ ALLOWED_DOMAINS=(
"cache-redirector.jetbrains.com"
"maven.pkg.jetbrains.space"
"packages.jetbrains.team"
# Node.js (Kotlin/Wasm test runner)
"nodejs.org"
# GitHub Actions (log access, artifact storage)
"productionresultssa7.blob.core.windows.net"
"blob.core.windows.net"
)

for domain in "${ALLOWED_DOMAINS[@]}"; do
Expand Down
74 changes: 67 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ concurrency:
cancel-in-progress: true

jobs:
build-and-test:
name: Build & Test (JVM + Android + Web)
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -26,20 +26,80 @@ jobs:

- uses: gradle/actions/setup-gradle@v4

- name: Lint
- name: ktlint + checkstyle
run: ./gradlew ktlintCheck checkstyle

- name: Run tests
run: xvfb-run ./gradlew allTests
test:
name: Test (JVM)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 23

- uses: gradle/actions/setup-gradle@v4

- name: Run JVM tests
run: xvfb-run ./gradlew jvmTest

android:
name: Build (Android)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 23

- uses: gradle/actions/setup-gradle@v4

- name: Build Android APK
- name: Build APK
run: ./gradlew :app:android:assembleDebug

desktop:
name: Build (Desktop)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 23

- uses: gradle/actions/setup-gradle@v4

- name: Build Desktop
run: ./gradlew :app:desktop:assemble

web:
name: Build (Web)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 23

- uses: gradle/actions/setup-gradle@v4

- name: Build Web
run: ./gradlew :app:web:wasmJsBrowserDistribution
run: ./gradlew :app:web:wasmJsBrowserDevelopmentExecutableDistribution

ios:
name: Build & Test (iOS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class DesktopCardScanner : CardScanner {
PN533Device.shutdown()
}

private fun discoverBackends(): List<NfcReaderBackend> {
private suspend fun discoverBackends(): List<NfcReaderBackend> {
val backends = mutableListOf<NfcReaderBackend>(PcscReaderBackend())
val transports = PN533Device.openAll()
if (transports.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class PN533ReaderBackend(
) : PN53xReaderBackend(transport) {
override val name: String = "PN533"

override fun initDevice(pn533: PN533) {
override suspend fun initDevice(pn533: PN533) {
val fw = pn533.getFirmwareVersion()
println("[$name] Firmware: $fw")
pn533.samConfiguration()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import com.codebutler.farebot.card.nfc.pn533.Usb4JavaPN533Transport
import com.codebutler.farebot.card.ultralight.UltralightCardReader
import com.codebutler.farebot.shared.nfc.ISO7816Dispatcher
import com.codebutler.farebot.shared.nfc.ScannedTag
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking

/**
* Abstract base for PN53x-family USB reader backends.
Expand All @@ -50,7 +52,7 @@ import com.codebutler.farebot.shared.nfc.ScannedTag
abstract class PN53xReaderBackend(
private val preOpenedTransport: Usb4JavaPN533Transport? = null,
) : NfcReaderBackend {
protected abstract fun initDevice(pn533: PN533)
protected abstract suspend fun initDevice(pn533: PN533)

protected open fun createTransceiver(
pn533: PN533,
Expand All @@ -70,14 +72,16 @@ abstract class PN53xReaderBackend(
transport.flush()
val pn533 = PN533(transport)
try {
initDevice(pn533)
pollLoop(pn533, onCardDetected, onCardRead, onError)
runBlocking {
initDevice(pn533)
pollLoop(pn533, onCardDetected, onCardRead, onError)
}
} finally {
pn533.close()
}
}

private fun pollLoop(
private suspend fun pollLoop(
pn533: PN533,
onCardDetected: (ScannedTag) -> Unit,
onCardRead: (RawCard<*>) -> Unit,
Expand All @@ -100,7 +104,7 @@ abstract class PN53xReaderBackend(
}

if (target == null) {
Thread.sleep(POLL_INTERVAL_MS)
delay(POLL_INTERVAL_MS)
continue
}

Expand Down Expand Up @@ -137,7 +141,7 @@ abstract class PN53xReaderBackend(
}
}

private fun readTarget(
private suspend fun readTarget(
pn533: PN533,
target: PN533.TargetInfo,
): RawCard<*> =
Expand All @@ -146,7 +150,7 @@ abstract class PN53xReaderBackend(
is PN533.TargetInfo.FeliCa -> readFeliCaCard(pn533, target)
}

private fun readTypeACard(
private suspend fun readTypeACard(
pn533: PN533,
target: PN533.TargetInfo.TypeA,
): RawCard<*> {
Expand Down Expand Up @@ -182,7 +186,7 @@ abstract class PN53xReaderBackend(
}
}

private fun readFeliCaCard(
private suspend fun readFeliCaCard(
pn533: PN533,
target: PN533.TargetInfo.FeliCa,
): RawCard<*> {
Expand All @@ -192,9 +196,9 @@ abstract class PN53xReaderBackend(
return FeliCaReader.readTag(tagId, adapter)
}

private fun waitForRemoval(pn533: PN533) {
private suspend fun waitForRemoval(pn533: PN533) {
while (true) {
Thread.sleep(REMOVAL_POLL_INTERVAL_MS)
delay(REMOVAL_POLL_INTERVAL_MS)
val target =
try {
pn533.inListPassiveTarget(baudRate = PN533.BAUD_RATE_106_ISO14443A)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.codebutler.farebot.card.nfc.PCSCUltralightTechnology
import com.codebutler.farebot.card.ultralight.UltralightCardReader
import com.codebutler.farebot.shared.nfc.ISO7816Dispatcher
import com.codebutler.farebot.shared.nfc.ScannedTag
import kotlinx.coroutines.runBlocking
import javax.smartcardio.CardException
import javax.smartcardio.CommandAPDU
import javax.smartcardio.TerminalFactory
Expand Down Expand Up @@ -93,7 +94,7 @@ class PcscReaderBackend : NfcReaderBackend {
println("[PC/SC] Tag ID: ${tagId.hex()}")

onCardDetected(ScannedTag(id = tagId, techList = listOf(info.cardType.name)))
val rawCard = readCard(info, channel, tagId)
val rawCard = runBlocking { readCard(info, channel, tagId) }
onCardRead(rawCard)
println("[PC/SC] Card read successfully")
} finally {
Expand All @@ -112,7 +113,7 @@ class PcscReaderBackend : NfcReaderBackend {
}
}

private fun readCard(
private suspend fun readCard(
info: PCSCCardInfo,
channel: javax.smartcardio.CardChannel,
tagId: ByteArray,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class RCS956ReaderBackend(
tg: Int,
): CardTransceiver = PN533CommunicateThruTransceiver(pn533)

override fun initDevice(pn533: PN533) {
override suspend fun initDevice(pn533: PN533) {
// nfcpy rcs956.py init(transport) + Device.__init__() sequence.
//
// 1. ACK clears device state after USB connect (init function)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ISO7816TagReader(
override fun getTech(tag: Tag): CardTransceiver = AndroidCardTransceiver(IsoDep.get(tag))

@Throws(Exception::class)
override fun readTag(
override suspend fun readTag(
tagId: ByteArray,
tag: Tag,
tech: CardTransceiver,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import com.codebutler.farebot.card.nfc.CardTransceiver
* then falls back to the DESFire protocol if no known application is found.
*/
object ISO7816Dispatcher {
fun readCard(
suspend fun readCard(
tagId: ByteArray,
transceiver: CardTransceiver,
): RawCard<*> {
Expand All @@ -47,7 +47,7 @@ object ISO7816Dispatcher {
return DesfireCardReader.readCard(tagId, transceiver)
}

private fun tryISO7816(
private suspend fun tryISO7816(
tagId: ByteArray,
transceiver: CardTransceiver,
): RawCard<*>? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.runBlocking
import platform.CoreNFC.NFCFeliCaTagProtocol
import platform.CoreNFC.NFCISO15693TagProtocol
import platform.CoreNFC.NFCMiFareDESFire
Expand Down Expand Up @@ -197,14 +198,16 @@ class IosNfcScanner : CardScanner {
}

private fun readTag(tag: Any): RawCard<*> =
when (tag) {
is NFCFeliCaTagProtocol -> readFelicaTag(tag)
is NFCMiFareTagProtocol -> readMiFareTag(tag)
is NFCISO15693TagProtocol -> readVicinityTag(tag)
else -> throw Exception("Unsupported NFC tag type")
runBlocking {
when (tag) {
is NFCFeliCaTagProtocol -> readFelicaTag(tag)
is NFCMiFareTagProtocol -> readMiFareTag(tag)
is NFCISO15693TagProtocol -> readVicinityTag(tag)
else -> throw Exception("Unsupported NFC tag type")
}
}

private fun readFelicaTag(tag: NFCFeliCaTagProtocol): RawCard<*> {
private suspend fun readFelicaTag(tag: NFCFeliCaTagProtocol): RawCard<*> {
val tagId = tag.currentIDm.toByteArray()
/*
* onlyFirst = true is an iOS-specific hack to work around
Expand All @@ -221,7 +224,7 @@ class IosNfcScanner : CardScanner {
return FeliCaReader.readTag(tagId, IosFeliCaTagAdapter(tag), onlyFirst = true)
}

private fun readMiFareTag(tag: NFCMiFareTagProtocol): RawCard<*> {
private suspend fun readMiFareTag(tag: NFCMiFareTagProtocol): RawCard<*> {
val tagId = tag.identifier.toByteArray()
return when (tag.mifareFamily) {
NFCMiFareDESFire -> {
Expand Down Expand Up @@ -272,7 +275,7 @@ class IosNfcScanner : CardScanner {
}
}

private fun readVicinityTag(tag: NFCISO15693TagProtocol): RawCard<*> {
private suspend fun readVicinityTag(tag: NFCISO15693TagProtocol): RawCard<*> {
val tagId = tag.identifier.toByteArray().reversedArray()
val tech = IosVicinityTechnology(tag)
tech.connect()
Expand Down
Loading