# How to Add a QR Scanner to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

A QR scanner needs three pieces: a continuous camera detector (mobile_scanner in FlutterFlow — wraps ML Kit on Android and AVFoundation on iOS), a handler that routes the decoded value by type (URL, text, payment, WiFi), and a Supabase table for scan history with RLS. With FlutterFlow you can ship a working QR scanner in 1-3 hours for $0/month — ML Kit runs entirely on-device at no cost. The only recurring cost is Supabase for scan history storage, which starts free.

## What a QR Scanner Feature Actually Is

A QR scanner opens the device camera, continuously detects QR codes in the frame without any tap, and routes the decoded content to the appropriate action. A URL opens in an in-app WebView or the system browser. A payment code triggers a checkout flow. An event ticket QR validates against your database. A WiFi code offers to join the network. The decoding itself is free and on-device — mobile_scanner on Flutter uses Google ML Kit on Android and Apple's AVFoundation on iOS, both built into the operating system. The product decisions are what happens with the result: how you handle each content type, whether you store a history, and how you prevent the same code from being processed multiple times per second.

## Anatomy of the QR Scanner Feature

Six components. The deep link handler and the permission fallback state are where AI-built versions most commonly fall short.

- **Camera viewfinder and QR detector** (ui): mobile_scanner Flutter package provides a MobileScanner widget that streams the camera feed and continuously detects QR codes using ML Kit on Android and AVFoundation on iOS — all on-device, no API key, no internet required. On web output from FlutterFlow, the scanner falls back to html5-qrcode via a JSChannel bridge, but FlutterFlow web scanner output is unreliable.
- **Deep link handler** (backend): Routes the decoded QR value by content type. If the value starts with http:// or https://, launch it in a WebView (webview_flutter package) or the system browser via url_launcher. If it matches a custom app scheme (myapp://screen/id), pass it to Flutter's GoRouter for in-app navigation. If it is a WiFi code (WIFI:S:...), parse and offer to connect. If it is plain text, copy to clipboard and display. Unknown types fall back to a display-and-copy screen.
- **Scan history store** (data): Supabase table qr_scan_history storing user_id, qr_value, qr_type (url/text/payment/wifi/unknown), scanned_at, and action_taken. RLS policy restricts read and write to the authenticated user. An optional Hive local cache stores the last 20 scans for offline history access without a network request.
- **Torch controller** (ui): MobileScannerController.toggleTorch() toggles the device flashlight on and off. An icon button in the viewfinder overlay (typically a lightning bolt icon) calls this method. Torch state is tracked in a Flutter Riverpod or Bloc provider so the button icon reflects the current state. The built-in FlutterFlow Scan action does not expose torch control — it requires a Custom Action.
- **Permission handler** (ui): permission_handler package requests camera permission on first launch with a rationale dialog explaining why camera access is needed. On Android, if the user taps 'Never Ask Again', the app cannot re-request permission and must show an 'Open Settings' button calling openAppSettings(). On iOS, NSCameraUsageDescription in Info.plist is required or the app crashes on camera open.
- **Scan feedback** (ui): On successful QR detection: HapticFeedback.mediumImpact() provides tactile confirmation before any navigation occurs. An optional beep sound via the audioplayers package provides audio feedback. A 500ms duplicate detection window (bool flag or last-scanned-value ref) prevents the same code from triggering multiple actions per scan.

## Data model

One table stores scan history per user with RLS. Run this in the Supabase SQL editor (Dashboard → SQL Editor → New query):

```sql
create table public.qr_scan_history (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  qr_value text not null,
  qr_type text not null default 'unknown',
  scanned_at timestamptz not null default now(),
  action_taken text
);

alter table public.qr_scan_history enable row level security;

create policy "Users can view own scan history"
  on public.qr_scan_history for select
  using (auth.uid() = user_id);

create policy "Users can insert own scan history"
  on public.qr_scan_history for insert
  with check (auth.uid() = user_id);

create policy "Users can delete own scan history"
  on public.qr_scan_history for delete
  using (auth.uid() = user_id);

create index qr_scan_history_user_recent_idx
  on public.qr_scan_history (user_id, scanned_at desc);
```

The composite index on (user_id, scanned_at DESC) keeps the history screen fast even after thousands of scans. The qr_type column (url, text, payment, wifi, unknown) enables filter tabs on the history screen without a full table scan.

## Build paths

### Flutterflow — fit 5/10, 1-3 hours

Best path: FlutterFlow has a native Scan Barcode / QR Action in the Actions panel that wraps mobile_scanner — point-and-click setup for iOS and Android with zero code for the basic scan flow.

1. Add a new Page with a full-screen camera view; in the Action editor on the Scan button, select Camera → Scan Barcode/QR and store the result in a Page State variable (type: String)
2. Add a conditional action after the scan: check if the qr_value starts with 'http' and launch url_launcher, otherwise display the value in a bottom sheet
3. Add a Supabase Backend Action to INSERT a row into qr_scan_history with user_id (from auth.currentUser.uid), qr_value (from Page State), qr_type (computed), and scanned_at (now())
4. Enable camera permissions in Settings → App Details → iOS Permissions: add NSCameraUsageDescription with a description like 'Camera is used to scan QR codes' — without this string the app crashes on iOS
5. Add a History page with a Supabase backend query fetching the current user's qr_scan_history rows newest first, bound to a ListView widget

Limitations:

- The built-in Scan Barcode / QR action scans once per tap and closes the camera — continuous auto-detect mode requires a Custom Action using MobileScannerController with a stream listener
- Torch toggle is not exposed by the built-in action — requires a one-line Custom Action calling MobileScannerController.toggleTorch()
- QR type detection (URL vs WiFi vs vCard vs plain text) and type-specific routing require conditional logic in the Action editor or a Custom Action for complex parsing

### Lovable — fit 2/10, 4-8 hours

Lovable builds web-only React apps — not native mobile. html5-qrcode scans QR codes in mobile browsers on the published URL, but cannot access the torch, haptics, or custom deep links. Acceptable for simple web-based QR check-in flows.

1. Create a Lovable project with Lovable Cloud connected for Supabase auth and database
2. Paste the prompt below — Lovable will generate an html5-qrcode scanner component, the Supabase scan history insert, and a history list page
3. Publish the project; open the live HTTPS URL on a mobile browser — camera access is blocked in the preview iframe and only works on the deployed URL
4. Grant camera permission; scan a QR code and verify the result displays and the row appears in the qr_scan_history table via the Supabase Table Editor

Starter prompt:

```
Build a QR scanner feature for a web app using the html5-qrcode library. Scanner page: full-screen camera view rendered by html5-qrcode configured for QR_CODE format only. On successful scan: call navigator.vibrate(150) for haptic feedback, display the decoded value and detected type in a bottom sheet. Detect the QR type from the value: if it starts with 'http://' or 'https://', show an 'Open URL' button that opens window.open(url, '_blank'); if it starts with 'WIFI:', show a display-only card with the WiFi network name parsed from the string; otherwise show the raw text with a 'Copy' button. After the bottom sheet appears, insert a row into Supabase qr_scan_history table (user_id from auth.uid(), qr_value, qr_type as 'url'/'wifi'/'text', scanned_at, action_taken). Implement a 500ms duplicate detection cooldown using a ref: if the same qr_value is decoded within 500ms of the previous scan, skip the insert and suppress the bottom sheet. Handle camera permission denied with a clear explainer screen showing why camera access is needed and a 'Try Again' button. Add a History page listing the current user's last 20 scans newest first with relative timestamps and the qr_type badge. Include a search input on the history page that filters scans by qr_value text match.
```

Limitations:

- No native mobile output — this produces a web app that works on mobile browsers but cannot access torch, HapticFeedback.mediumImpact(), or GoRouter deep links
- Camera is blocked in the Lovable preview iframe — must test on the published HTTPS URL
- Deep links (custom app schemes like myapp://screen/id) are not supported in web browsers — web can only open http/https URLs

### Custom — fit 4/10, 1-2 days

Custom Flutter build with mobile_scanner and GoRouter gives production-grade continuous scanning, full deep link handling, and offline scan history cache — the right choice when scanning is the core of the app.

1. Add mobile_scanner and permission_handler to pubspec.yaml; set up MobileScannerController with a BarcodeCapture stream listener for continuous detection
2. Implement the QR type router: parse the decoded value string to detect URL, WiFi (WIFI:S:), vCard (BEGIN:VCARD), email (mailto:), phone (tel:), and plain text types
3. Wire GoRouter: QR values with custom app schemes (myapp://...) are passed to GoRouter.go(); http/https URLs open in webview_flutter; WiFi codes use wifi_configuration package to offer network join
4. Add offline history cache using Hive: write every scan to a local Hive box immediately; sync to Supabase qr_scan_history in the background; display from Hive cache if offline

Limitations:

- Requires Flutter SDK knowledge and separate iOS and Android device testing environments
- Apple App Store review scrutinises camera permission descriptions — NSCameraUsageDescription must be specific to your use case or the app is rejected

## Gotchas

- **FlutterFlow Scan Action fires once then stops** — FlutterFlow's built-in Scan Barcode / QR action opens the camera, waits for the first successful scan, then immediately closes the viewfinder. There is no option to keep scanning continuously — every subsequent scan requires the user to tap the scan button again. Apps built for inventory or check-in use cases where users scan many codes in sequence feel broken with this behaviour. Fix: For continuous scanning, write a Custom Action in FlutterFlow using MobileScannerController with a barcodeStream listener that keeps the camera open. Set a processing lock bool to prevent multiple simultaneous actions from the same scan. This requires Dart code but is a short, well-documented pattern.
- **Camera permission permanently denied with no recovery** — On Android, if the user taps 'Don't Ask Again' when the permission dialog appears, the system will never show the permission dialog again — calling Permission.camera.request() silently returns denied. AI tools rarely handle this state and show a broken empty scanner screen instead of a recovery path. Fix: Before rendering the camera widget, call Permission.camera.isPermanentlyDenied from permission_handler. If true, render a screen with an 'Open App Settings' button that calls openAppSettings() — this opens the device Settings screen where the user can manually re-enable camera access. In FlutterFlow, this is a Custom Action.
- **Same QR code detected multiple times fires duplicate actions** — mobile_scanner's BarcodeCapture stream delivers a result every frame the QR code is in view — typically 15-30 times per second. Without a guard, a single scan triggers a URL to open 30 times, inserts 30 history rows, and makes the app unusable. AI tools often omit the cooldown. Fix: Use a boolean flag _isProcessing that is set to true on the first valid scan and reset after 1 second (or after the action completes). In the stream handler, return early if _isProcessing is true. Alternatively, store the last scanned value and timestamp in a ref and reject any scan of the same value within 500ms.
- **Deep links from QR codes fail silently in WebView** — When a QR code contains a custom deep link (myapp://products/123) and the app opens it in a webview_flutter WebView, the WebView does not understand the custom scheme — it attempts to load myapp://products/123 as a URL and shows an error page or blank screen. The user sees a broken result. Fix: Route all QR values through your app's router before deciding how to handle them. Check for custom app schemes first using a RegExp or Uri.parse(). Pass matching schemes to GoRouter.go() for in-app navigation. Only fall back to webview_flutter for http:// and https:// URLs. Never send a custom scheme to a WebView.
- **iOS camera crash when NSCameraUsageDescription is missing** — Apple requires a NSCameraUsageDescription entry in the app's Info.plist explaining why camera access is needed. Flutter apps built and submitted without this string crash immediately when the camera is opened on iOS, with a CameraException thrown before the scanner even appears. FlutterFlow projects created before this became enforced may not have it set. Fix: Add NSCameraUsageDescription in FlutterFlow under Settings → App Details → iOS Permissions. Write a specific usage string: 'Camera is used to scan QR codes for [your use case].' Generic descriptions like 'Camera access required' or missing entries lead to App Store rejection. Verify the string is present in Xcode before submitting.

## Best practices

- Implement a 500ms duplicate detection cooldown on every QR scan — mobile_scanner fires the callback dozens of times per second on a held code and will trigger your action repeatedly without a guard
- Give haptic feedback (HapticFeedback.mediumImpact()) the instant a QR is detected, before any database insert or URL open — makes the scanner feel instantaneous even if the subsequent async operation takes 200ms
- Route QR values by content type before acting on them — at minimum distinguish URLs (open WebView), plain text (display and copy), and your app's own codes (in-app navigation)
- Show the torch toggle only when the device has a torch — check MobileScannerController.hasTorch and hide the button on devices without a flash rather than showing a broken toggle
- Log every scan to history with the action taken — users frequently need to retrieve a URL they scanned an hour ago and have no other way to find it
- Stop the camera stream when the scanner screen is popped from the navigation stack — active camera sessions consume significant battery and keep the camera LED lit
- Write NSCameraUsageDescription with your specific use case before every App Store submission — generic or missing descriptions are one of the most common reasons for iOS review rejection

## Frequently asked questions

### What is the easiest way to add QR scanning to a FlutterFlow app?

Use the built-in Scan Barcode / QR action in the Action editor under Camera → Scan Barcode/QR. It wraps mobile_scanner and works on both iOS and Android with no custom code. Store the result in a Page State variable, then add conditional actions to handle the scanned value. You will need one Custom Action to add torch control and another if you want continuous scanning instead of one-shot.

### Why does the QR scanner fire multiple times for the same code?

mobile_scanner continuously detects QR codes at camera frame rate — typically 15-30 times per second while the code is in view. Without a guard, every detected frame triggers your action handler independently. Fix: use a bool flag _isProcessing set to true on the first valid scan and reset after 1 second, or store the last scanned value and reject repeats within 500ms.

### How do I add a flashlight toggle to a QR scanner app?

The FlutterFlow built-in Scan action does not expose torch control. Create a Custom Action with this Dart code: MobileScannerController controller = MobileScannerController(); await controller.toggleTorch(); Call it from an icon button in the viewfinder overlay. Check MobileScannerController.hasTorch before rendering the button — some devices do not have a flash accessible to the front camera.

### Can I build a QR scanner that works in a web browser without a native app?

Yes, using html5-qrcode in a React or Next.js web app built with Lovable or V0. It uses the browser's getUserMedia() API to access the camera and decodes QR codes client-side. Limitations on web: no torch access, no haptic feedback, and camera requires HTTPS and is blocked in preview iframes. For a native feel and hardware-speed scanning, FlutterFlow's mobile_scanner is the better choice.

### How do I parse the different types of QR code content?

Parse the decoded string by prefix. URLs start with http:// or https:// — open in a WebView or url_launcher. WiFi codes start with WIFI:S: — parse the SSID and security type from the string. vCard contacts start with BEGIN:VCARD. Email addresses start with mailto:. Phone numbers start with tel:. Everything else is plain text. Handle each type with a different action route and display the type badge in the scan history.

### Why does the camera open but the QR code is never detected?

Three common causes: (1) The camera is physically too close — most phone cameras need 10-20cm distance to focus on a QR code. Show a 'Move back slightly' hint. (2) The format configuration is wrong — if you configured a barcode scanner for EAN_13 only and then try to scan a QR code, nothing fires. Ensure QR_CODE is in the formats list. (3) Lighting is too low — the torch toggle exists for this. Prompt users to tap the torch button in dim environments.

### How do I store a history of scanned QR codes in Supabase?

Create a qr_scan_history table with columns: id (uuid), user_id (uuid FK to auth.users), qr_value (text), qr_type (text), scanned_at (timestamptz, default now()), action_taken (text). Enable RLS with a policy restricting select and insert to auth.uid() = user_id. In FlutterFlow, add a Supabase Backend Action after the scan action to INSERT the row. Run the SQL schema from this page in your Supabase SQL editor.

### What iOS Info.plist settings are required for camera access?

NSCameraUsageDescription is required — without it, the app crashes on iOS when the camera is first opened. In FlutterFlow, add it under Settings → App Details → iOS Permissions with a description specific to your use case. Example: 'Camera is used to scan QR codes for event check-in.' Apple's review team rejects apps with generic descriptions like 'For camera access' or apps where the stated purpose does not match the actual camera usage in the app.

---

Source: https://www.rapidevelopers.com/app-features/qr-scanner
© RapidDev — https://www.rapidevelopers.com/app-features/qr-scanner
