tauri-apps / tauri-apps/plugins-workspace
[barcode-scanner] Android: cancel() never settles the pending scan() promise (destroy() nulls savedInvoke first)
- Dominant language
- Rust
- Stars
- 1.8k
- Forks
- 602
- Avg merge
- 4d 14h
- Merged PRs (30d)
- 9
Description
### Describe the bug
On Android, calling `cancel()` tears the camera down but leaves the pending `scan()` promise **never settled** — it neither resolves nor rejects. Frontend code that awaits `scan()` is stranded forever.
The cause is an ordering problem in `BarcodeScannerPlugin.kt` (still present on `main` as of today, and in the published 2.4.5):
```kotlin
@Command
fun cancel(invoke: Invoke) {
destroy() // <- sets savedInvoke = null
savedInvoke?.reject("cancelled") // <- therefore a no-op
invoke.resolve()
}
```
```kotlin
private fun destroy() {
dismantleCamera()
savedInvoke = null
...
}
```
Since `destroy()` nulls `savedInvoke` before the `?.reject("cancelled")` runs, the reject never reaches the pending invoke.
The intent of the code is clearly that cancelling rejects the scan with `"cancelled"` — the JSDoc on the JS side documents `cancel()` as cancelling the current scan process, and the reject call is right there.
### Impact
This is particularly nasty in **windowed mode**, where the app has made its page background transparent to reveal the camera preview behind the WebView. The cancel path then leaves the app in a state where:
- the camera preview is already torn down, but
- the awaiting frontend code never runs its cleanup, so the page stays transparent and the scanning overlay stays mounted
The result is a blank, unresponsive-looking screen, and tapping cancel again cannot recover it because the native side has already dropped the invoke.
### Reproduction
1. Start a scan: `await scan({ windowed: true, formats: [Format.QRCode] })`
2. While it is running, call `cancel()` from a cancel button
3. The promise from step 1 never settles — nothing after the `await` ever runs (no resolve, no reject, no `finally`)
Tested against `@tauri-apps/plugin-barcode-scanner` 2.4.5 / `tauri-plugin-barcode-scanner` 2.4.5 on Android.
### Expected behavior
`cancel()` rejects the pending `scan()` promise (with `"cancelled"`, as the existing call intends), so callers can distinguish a cancelled scan from a successful one and run their cleanup.
### Suggested fix
Reject before tearing down, so the saved invoke still exists:
```kotlin
@Command
fun cancel(invoke: Invoke) {
savedInvoke?.reject("cancelled")
destroy()
invoke.resolve()
}
```
I'm happy to open a PR if that's a welcome change.
### Related
- #3348 reports what looks like the same class of symptom on iOS in windowed mode (promise never resolves), though I have not verified whether the iOS cause is this same ordering issue.
- #2238 is a different problem (scan never returning at all during scanning), not the cancel path.
### Platform and versions
- Platform: Android
- `tauri-plugin-barcode-scanner`: 2.4.5 (bug also present on `main`)
- Tauri: v2
Contributor guide
Research direction
Open BarcodeScannerPlugin.kt and inspect the cancel() and destroy() entry points, along with the existing JS-side cancel() documentation. Verify the pending scan invoke is rejected with "cancelled" before teardown, then check that cancel() still resolves its own invoke and that the scan promise settles for callers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 90/100