Barcode scanner
Barcode scanning is a host operation because live scanning usually owns camera permission, camera preview, autofocus, torch, and decoder selection. The app describes acceptable formats and receives portable decoded results.
By the end of this guide, you will know how to build a scan button that accepts QR codes and Code 128 labels and stores the decoded value. The same pattern applies to larger apps: declare the capability when native configuration is needed, store the result in typed app state, request the host operation from a reducer, and test both success and failure with a memory host.
Do not build a camera-specific barcode workflow in shared reducers. Keep camera and decoder details behind BarcodeScannerHost, and use ordinary Fission UI for the screen around it.
1. Start with the state the screen needs
Add the smallest state fields the screen needs to render honestly. For this flow, useful state is:
#[derive(Default)]
struct BarcodeScannerState {
// Add these fields to your existing app state.
last_barcode: Option<String>,
barcode_error: Option<String>,
scanning: bool,
}
impl GlobalState for BarcodeScannerState {}
State is the source of truth for the UI. Do not store whether the host "probably" succeeded; wait for the typed success action and store the returned value.
Run this once from the project root:
fission add-capability barcode-scanner --project-dir .
That command records the capability in fission.toml and updates generated platform files where Fission can do so safely. It does not replace product-specific review of native manifests, usage text, entitlements, service identifiers, domains, or store policy.
The CLI adds camera-related mobile configuration because live scanning normally uses the camera. Apps that only decode existing image bytes may not need camera access on every target, but the capability declaration keeps future live scanning explicit.
This is the difference between the Fission API, a provider, and packaging configuration:
The Fission API is the typed Rust contract your reducer calls, such as ctx.effects.camera().capture_photo(...).
The provider is the shell-side Rust implementation that turns that typed request into the correct OS, browser, or hardware call.
Packaging configuration is the manifest, plist, entitlement, service-worker, protocol, domain, or store metadata that lets the platform permit that call in a real installed app.
If the API exists but the provider is missing, the request returns a typed unsupported error. If the provider exists but packaging is missing, the operating system or browser may deny the request before the provider can complete it.
3. Define reducers for success and failure
Capability work finishes later, after the reducer that started it has returned. You therefore need one reducer for the successful result and one reducer for the error path.
#[fission_reducer(BarcodeScanned)]
fn on_barcode_scanned(state: &mut BarcodeScannerState, results: BarcodeScanResults) {
state.scanning = false;
state.last_barcode = results.items.first().map(|item| item.value.clone());
state.barcode_error = None;
}
#[fission_reducer(BarcodeScanFailed)]
fn on_barcode_scan_failed(state: &mut BarcodeScannerState, error: BarcodeScannerError) {
state.scanning = false;
state.barcode_error = Some(error.message);
}
Keep both paths explicit. A denied permission, unavailable device, unsupported host, or cancelled prompt is not an exceptional mystery; it is normal product behavior the UI should explain.
4. Request the capability from a reducer
Call the capability from the reducer that handles the user's intent. The reducer queues a host effect and returns. The runtime later dispatches either the success reducer or the failure reducer.
state.scanning = true;
ctx.effects
.barcode_scanner()
.scan(BarcodeScanRequest {
formats: vec![BarcodeFormat::QrCode, BarcodeFormat::Code128],
prompt: Some("Scan the label".into()),
timeout_ms: Some(20_000),
allow_multiple: false,
..Default::default()
})
.on_ok(ctx.effects.bind(
BarcodeScanned(BarcodeScanResults::default()),
reduce_with!(on_barcode_scanned),
))
.on_err(ctx.effects.bind(
BarcodeScanFailed(BarcodeScannerError::default()),
reduce_with!(on_barcode_scan_failed),
));
The call does not run the OS API directly. It records a typed host request. That keeps shared app logic portable across Fission targets where the capability applies, plus static-site previews and tests.
5. Trigger the reducer from the UI
Wire the user-facing control with the normal Fission action pattern. This example uses a small widget so the binding has one clear home.
struct ScanBarcodeButton;
impl From<ScanBarcodeButton> for Widget {
fn from(component: ScanBarcodeButton) -> Self {
let (ctx, view) = fission::build::current::<BarcodeScannerState>();
let run = with_reducer!(ctx, ScanBarcode, on_scan_barcode);
Button::new("Scan barcode")
.disabled(view.state().scanning)
.on_press(run)
.into()
}
}
Rename the widget and action for your product, but keep the shape: the widget dispatches one typed action, the reducer starts the capability request, and the success or failure reducer updates state.
6. Provide and test the host behavior
Register a BarcodeScannerHost with .with_barcode_scanner_host(...). Use MemoryBarcodeScannerHost for deterministic tests. A provider can support live scanning, image decoding, or both; unsupported operations should return BarcodeScannerError.
Test accepted formats, no-result scans, cancellation, timeout, and decoding existing image bytes. A UI test should prove the scan button becomes busy while the host owns the live scan.
A useful first test creates the app with the memory host, dispatches the start action, feeds the queued effect through the host, and asserts that the reducer updates state from the returned payload. Add a second test for the error path before you ship the screen.
Android and iOS normally scan through the camera. Web can use camera APIs and browser barcode detection where available, with a decoder fallback when appropriate. Desktop support depends on camera providers or image-decoding libraries.
For a cross-platform summary, read the capability matrix. For exact request fields, provider traits, and generated configuration, see the Barcode scanner reference.
Finished capability flow
A complete Barcode scanner integration has all of these pieces in place:
| |
|---|
| fission.toml declares the capability and generated platform files contain the permission text, manifest entry, entitlement, domain, or protocol metadata the target needs. |
| App state records the in-flight flag, the successful value, and the user-facing error separately. |
| One reducer starts the host request, one handles success, and one handles denial, unsupported hosts, cancellation, or provider errors. |
| The shell registers a real provider for production and a memory provider for tests. |
| At least one test covers success and one covers permission denial or unsupported host behavior. |
Diagnose capability failures
Capability failures should be safe to retry and easy to explain. Start by checking fission.toml, then run fission doctor, then inspect the generated target files. If the provider is missing, the request should return an unsupported error. If the provider exists but the platform configuration is wrong, the OS or browser will usually deny the request before the provider can complete it. Keep those cases distinct in the UI so users know whether they can retry, change settings, connect hardware, or choose another path.