Repository navigation
feat(truapi): let products scan codes with the host's viewfinder - #1307
Merged
Merged
Conversation
Adds the Host-drawn scanner RFC and its protocol surface: the Scanner trait (wire id 25) with scan, its v01 payloads and versioned envelopes, and the regenerated goldens. The runtime answers Unsupported until a host serves it, so the CLI battery and the playground Diagnosis skip the service for now.
- Add Codabar, which both platform decoders read. - Compare the prefix ignoring ASCII case, since QR codes often store URLs in capitals. - State the units of each limit, and refuse hints with line separators or direction-changing characters. - Say hosts report UPC-A as EAN-13 before filtering, and that a refused host answer reaches the product as Unknown. - Drop the UniFFI derives until a native caller needs them. - Bump @parity/truapi-host too, since its runtime now answers the method. - Ask in the RFC whether background executions may open the viewfinder.
…core Request limits, the prefix and format match, and ScanFilter, which both native hosts call for every code the camera reads so they accept the same codes and show the same messages.
Adds ScannerPlatform as an optional host capability. The core refuses an invalid request before any viewfinder opens, allows one open scan per host, closes the viewfinder when the product cancels, and checks the host's answer again before the product sees it.
contacts.pick ignored the call's cancellation, so a withdrawn pick kept the picker open until the user acted. The core now drops its wait, which is the host's signal to close the picker.
The Kotlin contacts adapter caught CancellationException and answered it as a host rejection, so a withdrawn pick never unwound. It now uses withHostRejection, which lets cancellation through. On Swift the four private copies of withHostRejection become one file-level pair, which the contacts adapter now uses too.
NativeScannerCallbacks, the ScannerHostBridge wrappers with setScanner, and ScanFilter and ScanVerdict, which a host's viewfinder calls for every code it reads.
…st host Wires the scanner capability through the WASM runtimes and the worker handshake. The mock host serves a scanner when created with a scanner answer, and setScanAnswer changes the next one. The Rust MockPlatform gets the same control so the two mocks stay in step.
Both CLI host roles serve the scanner. A scan answers TRUAPI_SCAN_TEXT as a QR code, or a dismissal when it is unset, so the battery now runs the Scanner example instead of skipping it.
These modules are declared inside the runtime_items! macro, so cargo fmt skips them. Formatted with rustfmt directly, touching only lines this branch added.
scripts/battery.sh --scanner-host (make e2e-scanner-cli) starts a signing host that scans a fixed code and checks, over the real wire and without a session, that a matching code reaches the product, that a code or format the product did not ask for never does, and that an invalid request is refused before the host is asked.
Scanning needs no session, so the pairing host runs the same cases unpaired.
Contributor
|
This pull request touches an app, which is not built by default. Add a label for each build you want:
Each starts as soon as it is added and follows the branch from then on. |
Review of #1307: - scanner.scan and contacts.pick turned any cancelled wait into CallError::Cancelled. Only the dispatcher may answer that, for a call the product withdrew, so both now answer Unknown with the reason. - ScanFilter names each wrong code once per scan, so two codes read in turn no longer repeat the message on every frame. - Hints may not carry U+061C, the last bidirectional mark missing. - until_cancelled is private again, and services use core atomics. - ScannerPlatform says a new scan can arrive before the host has closed a cancelled one, and that a JS host is not told about a cancel.
Contributor
|
CI Status: 24 required jobs green, 21 passed and 3 skipped by path filter. All job results
Signing credentials: failure as of 2026-10-09, a release may fail Commit |
Contributor
Bundle size reportCompared with
WebAssembly modules
Changed files (7)
Commit: 71fe4ff |
Review of #1307: - A format may be named only once, so the list the filter checks on every camera frame stays as short as the formats. - Hints may not carry invisible characters (zero-width, word joiners, byte order mark, annotation marks). - One unknown() helper builds the handler's catch-all error. - The JS mock keeps its scan answer only when it serves a scanner, so it holds no default that can never be read. - setScanAnswer is no longer listed as part of host-api-test-sdk's surface, which has no scanner. - The README line for make e2e-scanner-cli names both host roles.
A code the core would accept as a pairing request is never delivered, whatever the request's prefix, since it lets whoever answers it pair with the device that showed it. The prefix stays optional: barcodes have none, and a required one would not stop a product asking for the pairing link. Settles the open question: an App or Widget scans only while it is on screen, and a Worker only within 5 seconds of a user tap; anything else is answered with the new NotVisible error.
# Conflicts: # docs/rfcs/host-scanner.md
Whoever answers a pairing link pairs with the device that showed it, so the scan filter and the core's re-check refuse any code the core would accept as a pairing request, with or without the pair link around the handshake and whatever the request's prefix.
A Worker may scan only within 5 seconds of a renderer action the host delivered to it; otherwise the core answers NotVisible without asking the host. For an App or Widget the host answers HostScan::NotVisible when it is not on screen, so native hosts now receive the execution kind.
# Conflicts: # rust/crates/truapi/src/runtime/tests.rs
valentinfernandez1
added this pull request to stack #1311
October 7, 2026 18:13
The host's viewfinder rules now live only on ScannerPlatform. The native trait, the Swift and Kotlin bridges, RUNTIME.md and the truapi-host README point to it instead of repeating it, and the setter docs are one line. The tests keep every case but drop the scaffolding: case tables instead of one assert per block, one scanner stub constructor, and the CLI scanner cases inline in their battery script.
Same behaviour covered with fewer tests: - The runtime host-answer tests become one table, and the two Worker tap tests one test. - The request validation tests become one. - The Rust mock and CLI scanner unit tests are gone: the mock parity test and the CLI battery already exercise both. - The CLI battery keeps one case each for accepted, refused by the core and refused before the host, since unit tests cover the variations. - The two mock client scanner tests become one.
Native executions publish renderer actions straight to the channel they share with every connection, so the tap never reached the connection and a Worker on Android or iOS was always told NotVisible. The channel now keeps the time of its last action, which every path goes through.
A renderer action the channel refuses no longer opens a scan window. A native test publishes a tap on the execution and checks it on the connection, which is the path Android and iOS use. The backdating setter is test-only.
The tap time is written before the action reaches a live subscriber, so a Worker that scans as soon as it hears the tap finds it. A refused action still does not count, and a test pins that.
# Conflicts: # README.md # docs/rfcs/host-scanner.md # rust/crates/truapi/src/runtime/capabilities/scanner.rs # rust/crates/truapi/src/runtime/tests.rs # rust/crates/truapi/src/v01/scanner.rs
The tap check reuses the native renderer action test's setup instead of repeating it. The hint test keeps one refused character per rule, a dead error mapping goes, and an unrelated test keeps main's formatting.
filvecchiato
requested changes
Oct 8, 2026
filvecchiato
left a comment
Collaborator
There was a problem hiding this comment.
Looks good apart from one gap in the pairing-request check, inline.
The check went through the core's own decoder, which reads only a bare V2 proposal after the first `?handshake=`. Wallets also read `0x` hex, V1 proposals, and a handshake among other query items, so those reached the product. The check now goes by shape: any `handshake=` value or the bare text, in hex with or without `0x`, starting with a known version tag and long enough for the device keys.
# Conflicts: # rust/crates/truapi/RUNTIME.md # rust/crates/truapi/src/host_core.rs
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.
Host scanner
A product can call
scanner.scanto have the host open its own QR and barcode viewfinder. The product gets back the text and format of the code the user scanned. It never sees the camera, so there is no permission prompt.What the core does
When a product calls
scanner.scan, the core:Unsupportedif the host has no scannerInvalidRequestif the request breaks a limitNotVisiblefor a Worker the user has not tapped in the last 5 secondsBusyif another scan is open, since the device has one viewfinderNotVisibleitself for an App or Widget that is not on screen.UnknownSteps 1 to 4 never reach the host. A tap is any action the host publishes to the product's renderer.
A pairing handshake never reaches a product, whatever the product asked for. Whoever answers one pairs with the device that showed it. The core matches it by shape, in every form a wallet reads: any
handshake=value or the bare handshake, in hex with or without0x, of any proposal version.If the product cancels, the core stops waiting, which tells the host to close the viewfinder.
Scanning needs no signed-in session.
What hosts get
ScannerPlatform, installed withset_scanner_platform.ScannerHostBridgeandsetScanner. The bridge receives the product id, the execution kind and the request.ScanFilter: a host calls it for every code the camera reads. It answers accept, "not for this product", or ignore, so every host applies the same rules.scannercallbacks. dot.li supplies none, so it answersUnsupported.Testing
createMockHost({ scanner: answer })serves a scanner, andsetScanAnswerchanges the next answer. Without it, scans areUnsupported.TRUAPI_SCAN_TEXTas a QR code, or a dismissal when it is unset.Scanner/scanpasses on the signing host. The run used the public test mnemonic, which has no username and no personhood, so the examples that need them fail. None of those failures involve the scanner. The paired phase needs a real identity and was not run.Contact picker
Notes for review
cargo fmtskips the files thatlib.rsdeclares inside theruntime_items!macro, so no CI job formats them. The new code in those files was formatted withrustfmtdirectly.