mmuqiitf/filament-qr-code
Filament QR Code provides a simple QR code field and display component for Filament forms and tables, letting you generate scannable codes from URLs, text, or model data with minimal setup in Laravel admin panels.
php artisan filament:assets
v0.6.0 changed the scanner/sequence views and assets again. If you have customised views, republish them as described in UPGRADE.md — the scanner's x-data no longer embeds options (they now live on data-qr-options) and the sequence view gained a data-qr-options wrapper.UPGRADE.md before upgrading. v0.6.0 documents the 0.5 → 0.6 steps: rebuild assets, republish customised views, replace $record->getKey() with ScanResult::recordKey(), and move off the deprecated filament-qr-code.image route name (use filamentQrCode.image; the alias is removed in 1.0). If you are still on 0.4.x, apply the intermediate 0.5 steps from UPGRADE.md first.use Mmuqiitf\FilamentQrCode\Fields\QrScanner;
QrScanner::make('sku')
->label('Product SKU')
->required();
With no server-side checks configured this still behaves like the older releases: no extra Livewire request, no cooldown, single success beep.config/qr-code.php — scan.cooldown_ms, scan.clear_on_reject, scan.sync_options, feedback.success, feedback.error, audit.enabled.docs/customizing.md (new extension points), docs/recipes (pos, warehouse, manufacturing, generator-page, custom-input, styling) and UPGRADE.md.QrScanner/QrScanSequence/ScanStep classes — validateUsing(), lookup(), lookupUsing(), normalizeWith(), normalizeUsing(), pipe(), onScanned(), onRejected(), onStepAccepted(), onStepRejected(), clearOnReject(), beep(), vibrate(), flash().QrScanner::make('product_sku')
->lookup(Product::class, 'sku')
->clearOnReject()
->beep()
->flash();
php artisan make:qr-scan-field CustomerBadge --sequence
Publish the stubs with --tag=filament-qr-code-stubs if you want to customise them.Validation-only scans. Use Laravel rules, rule objects, or closures:
QrScanner::make('barcode')
->validateUsing([
'required',
'string',
'max:64',
Rule::exists('products', 'barcode'),
]);
Checks execute in a fixed order: rules, then lookup()/lookupUsing(), then any pipe() stages and closures. Execution stops at the first failure.
Record lookup and custom resolution. Use lookup() for simple model/column resolution and lookupUsing() when the value resolves to something other than a model row:
QrScanner::make('asset_code')
->lookup(
Asset::class,
'code',
modifyQueryUsing: fn ($query) => $query->where('active', true),
);
QrScanner::make('coupon')
->lookupUsing(fn (string $code) => Coupon::findByCode($code), fallback: true);
lookup() now accepts closures for the model and column. fallback: true runs lookupUsing() after a normal miss. ScanResult::$record is typed mixed — lookups may return arrays or objects, so always use ScanResult::recordKey() for the key.
Normalisation. normalizeUsing() now runs on the server before rules and lookup, not just on stored state, and normalizeWith() provides shared presets that apply in both the browser and server:
QrScanner::make('serial')
->normalizeWith(trim: true, uppercase: true, stripPrefix: 'SN-');
Keep rule expectations aligned with the normalised value — validation sees the normalised code (also on submit). ScanStep::normalizeUsing() exists for sequence steps.
Live scan callbacks. Use onScanned() / onRejected() for live-scan UX only:
QrScanner::make('product_sku')
->lookup(Product::class, 'sku')
->onScanned(function (ScanResult $result, Get $get, Set $set) {
$set('product_name', $result->record?->name);
})
->onRejected(function (ScanResult $result) {
Notification::make()->title('Code rejected')->danger()->send();
})
->clearOnReject();
Upload scans do not fire these callbacks; they go through ScanChecksRule on submit.
Custom check stages. For reusable checks, implement ScanCheckStage and attach with pipe():
QrScanner::make('lot')
->lookup(Lot::class, 'code')
->pipe(EnsureLotNotExpired::class);
Stages are container-resolved and run after built-in checks, on live scans and on submit. Keep them deterministic and idempotent; use onScanned() for one-off side effects.
Sequences. Multi-step flows use QrScanSequence and ScanStep, including conditional steps:
QrScanSequence::make('pick')
->steps([
ScanStep::make('po')->lookup(PurchaseOrder::class, 'number'),
ScanStep::make('sku')->lookup(Product::class, 'sku'),
ScanStep::make('lot')->when(fn (array $values) => $values['sku_requires_lot'] ?? false),
ScanStep::make('serials')->repeat(),
])
->onStepAccepted(fn (ScanStep $step, ScanResult $result) => /* ... */)
->onStepRejected(fn (ScanStep $step, ScanResult $result) => /* ... */);
A skipped when() step is not required and its value is dropped. An optional step whose check fails now gives way to a following repeat step, so po, sku, lot?, serials works. Step closures receive $get, $livewire, $step and $values; ScanStep is macroable. From page/Livewire code use goToStep(), resetSequence() and setRepeatCount().
Runtime control. Scanners and sequences now pick up server-side option changes on every Livewire re-render without being re-created; an in-progress scan completes first. Default scan.sync_options is 'morph'. Switch to explicit refresh with syncOptions(OptionsSync::Refresh) and dispatch qr-scanner-refresh / qr-sequence-refresh when you want precise control.
Per-element control is available via element.qrScanner / element.qrScanSequence (feed, pause, resume, reset, setOptions, refresh, getOptions, getState, plus goToStep on sequences) and matching window events (qr-scanner-feed|pause|resume|reset|refresh|set-options, qr-sequence-feed|go-to|reset|pause|resume|refresh|set-options|set-repeat). The shared hardware input router is public as window.FilamentQrCode.router / createInputRouter.
Submit-time consistency. Server checks re-run on submit via ScanChecksRule; pipe() stages run there too. Treat rules, lookup, stages and closures as validation: deterministic and idempotent. A scan accepted live can still be rejected on submit if the record changed.
Feedback configuration. Keep global defaults in qr-code.feedback.success/error, override per field:
QrScanner::make('serial')
->beep()
->vibrate(success: [80, 50, 80], error: [300])
->flash();
Useful in warehouse and field-service apps where the operator is not looking at the screen.
Events and auditing. ScanAccepted/ScanRejected now carry statePath, livewireClass, livewireId, sequence and values; ScanSequenceCompleted carries livewireClass/livewireId; QrCodeScanned context gains state_path. Accepted live scans fire QrCodeScanned, audited when qr-code.audit.enabled is on. Browser events qr-scan-accepted, qr-scan-rejected, qr-scan-duplicate remain the quickest JS integration/debug surface.
Styling and render hooks. Views are split into publishable sub-views with fi-qr-* classes, data-qr-part markers, extraAttributes() on roots, QrCodeRenderHook render hooks and --fi-qr-* CSS variables. Rendered output is otherwise unchanged, so prefer these hooks over editing published views.
Duplicate suppression and deployment. Enable checks on a field and rely on qr-code.scan.cooldown_ms for duplicate suppression (handheld bursts are also page-level checked). Add php artisan filament:assets to deploy scripts after composer install, republish customised views, and clear/rebuild config caches when changing qr-code.* values.
Upgrade checklist is not optional. Run php artisan filament:assets, republish customised views, switch to ScanResult::recordKey(), and replace filament-qr-code.image with filamentQrCode.image. The old route alias disappears in 1.0.
Republish views for option/runtime behaviour. x-data no longer embeds options; they sit on data-qr-options, and the sequence view wraps it in a <div data-qr-options>. Stale published views break runtime option sync and per-element APIs in ways that look like JavaScript bugs.
ScanResult::$record is mixed. Do not call $result->record->getKey(); use recordKey(). Lookups may legitimately return arrays or DTOs.
Options sync has two modes. With the default 'morph', options update on every Livewire re-render; the scan in progress finishes first, so changes are not instantaneous mid-scan. With syncOptions(OptionsSync::Refresh) you must dispatch the refresh events yourself. If options look stale, confirm the mode before chasing Alpine state.
Checks are ordered and fail-fast. Rules first, then lookup/lookupUsing, then pipe() stages and closures. Put cheap syntactic checks before database work.
Checks re-run on submit. A live-accepted scan can be rejected at submit. Keep checks stable; avoid time-sensitive logic unless intended. Custom ScanCheckStage implementations must be container-resolvable and idempotent.
Normalisation now affects server validation. normalizeUsing() runs before rules and lookup, so rules previously written against raw input may need adjusting. Prefer normalizeWith() presets so browser and server stay consistent, and validate the same normalised shape you store.
Conditional steps drop values. ScanStep::when() skipped steps are not required and their value is removed from the sequence values. Optional step failure can fall through to a following repeat step — step order matters for po, sku, lot?, serials flows.
Sequence construction changed. QrScanSequence::make() runs setUp() and configureUsing() callbacks, and sequences with a state path now fill to [] rather than remaining null. Check code that relied on null/absent sequence state.
Callbacks are live-scan only. onScanned() / onRejected() and onStepAccepted() / onStepRejected() do not fire for upload scans. Use ScanChecksRule/normal validation for upload paths.
Cooldown only matters when server checks exist. Plain scanner fields still make no extra request and have no cooldown. If duplicate suppression is missing, add a check.
Config cache can hide changes. After editing qr-code.scan.cooldown_ms, clear_on_reject, sync_options, feedback or audit settings, run php artisan config:clear locally and rebuild the config cache in production.
Translation keys matter. Failure messages use record_not_found and scan_failed. Publish/override package translations for localised apps.
Flash timing and legacy selectors. The outcome flash plays after the modal closes (or on the open panel) and lasts 900 ms. Update CSS targeting the old 300 ms ring, and do not target the hidden file input for upload-scan styling — v0.4.0+ resolves the wrapper via $root.
Per-element events are scoped. qr-scanner-*/qr-sequence-* window events target one field/state path. If nothing happens, check the event name and target match; prefer element.qrScanner when you already hold the element. Overriding createInputRouter affects every routed hardware input globally.
Event payloads expanded. Listeners consuming ScanAccepted/ScanRejected should use the new statePath, livewireClass, livewireId, sequence, values fields rather than inferring context. QrCodeScanned context adds state_path.
Debugging checklist:
qr-scan-duplicate, qr-scan-accepted, qr-scan-rejected, plus qr-scanner-refresh / qr-sequence-refresh when using explicit sync.handleScan() per scan and unexpected re-renders.ScanAccepted, ScanRejected, ScanSequenceCompleted and QrCodeScanned.config('qr-code.scan') and config('qr-code.feedback') after cache clears; confirm sync_options.php artisan filament:assets and republished views.Audit volume. QrCodeScanned fires for accepted live scans when auditing is enabled; prepare listeners and storage for high-volume scanning.
Pre-1.0 package. v0.6.0 is feature-rich but still pre-1.0: pin the version, read UPGRADE.md before each bump, and keep asset rebuilds and view republication in the deployment checklist.
How can I help you explore Laravel packages today?