Skip to content

feat: Add BlinkIdScannerView for custom scanner UI - #100

Open
alboiuvlad29 wants to merge 12 commits into
microblink:masterfrom
MewsSystems:noticket-add-custom-scanner-view-upstream
Open

feat: Add BlinkIdScannerView for custom scanner UI#100
alboiuvlad29 wants to merge 12 commits into
microblink:masterfrom
MewsSystems:noticket-add-custom-scanner-view-upstream

Conversation

@alboiuvlad29

@alboiuvlad29 alboiuvlad29 commented Aug 4, 2026

Copy link
Copy Markdown

Summary

Introduces a custom platform view scanner. We were using a similar version with v6 on this branch at Mews.

The solution offered by the package is not up to the par with the capabilities of native SDKs Microblink offers. This PR aims to contribute back and offer a custom scanner experience, that can be customised by client.

ScreenRecording_08-04-2026.15-30-27_1.mp4

(Using debug mode on iPad over wireless, the app looks slow but works bearer in prod build)

This PR is intentionally large — it cannot be split smaller without shipping a broken intermediate state.

What's in here

BlinkIdScannerView — custom camera UI

  • Flutter PlatformView that exposes the raw camera surface
  • BlinkIdScannerController (ChangeNotifier) driving status, phase, and guidance
  • Status lifecycle: uninitialized → loadingSdk → initializing → ready → scanning → processing → done / error
  • BlinkIdScanPhase: front / flip / back — with native pause during flip and suppressed guidance events
  • guidanceStream — per-frame detection hints (tooFar, blur, glare, etc.)
  • preferredCamera on initialize() + runtime switchCamera() (reinitializes session; cancels in-progress scan cleanly)
  • setDebugLogging(true) — opt-in native lifecycle forwarding to Flutter's debugPrint
  • Full camera permission handling surfaced as cameraPermissionRequired status

Example app

  • Home screen: structured result card (face avatar, personal info, document info, document images)
  • Custom scanner screen: guidance overlay, flip animation, scan timeout + retry dialog, camera switch button
  • Debug logging enabled in example

Docs

  • README updated with the new usage

Summary by CodeRabbit

  • New Features
    • Added a customizable Flutter scanner interface for Android and iOS.
    • Added real-time guidance, document-flip prompts, camera switching, retries, cancellation, and permission recovery.
    • Added structured scan status, phases, errors, and guidance APIs.
    • Added support for explicitly disabling scanning modules while preserving SDK defaults when settings are omitted.
    • Expanded the example app with native, Direct API, and custom scanning workflows.
  • Documentation
    • Added setup, license configuration, custom UI, and troubleshooting guidance.
  • Bug Fixes
    • Improved serialization of nested and optional scanning settings.

Adds a Flutter PlatformView that exposes the BlinkID camera scanning
engine with a fully custom Flutter overlay, alongside the existing
performScan (built-in native UX) path.

New public API:
- BlinkIdScannerController (ChangeNotifier) — scan lifecycle, phase,
  guidance. Call initialize() then scan(); retry via reset().
- BlinkIdScannerView — platform view rendering the native camera surface
  (Android: CameraX TextureView COMPATIBLE mode; iOS: AVFoundation).
- BlinkIdScanPhase — front / flip / back. Call onFlipComplete() after
  your flip animation or the scanner pauses indefinitely.
- BlinkIdScannerStatus — uninitialized → loadingSdk → initializing →
  ready → scanning → processing → done | error | cameraPermissionRequired.
- BlinkIdGuidance sealed class — per-frame hints (tooFar, tooClose,
  blur, glare, tilted, notFullyVisible, wrongSide, …).
- PreferredCamera enum + preferredCamera param on initialize().
- BlinkIdScannerController.switchCamera() — runtime camera switch;
  reinitializes native session, cancels in-progress scan cleanly.
- BlinkIdScannerController.setDebugLogging(bool) — opt-in native
  lifecycle forwarding to Flutter debugPrint; no channel traffic unless
  enabled.

Implementation:
- Android: ImageAnalysis → session.process() on single-thread executor;
  runBlocking bridges coroutine. TextureView COMPATIBLE mode composites
  into Flutter layer hierarchy.
- iOS: AVCaptureVideoDataOutput → session.process() on @ProcessingActor;
  isProcessingFrame guard prevents 30 fps backlog. Orientation via
  UIDevice.orientationDidChangeNotification.
- Flip gate: native pauses on SideScanned, resumes after resumeAfterFlip.
- Camera permission: surfaced as cameraPermissionRequired status rather
  than a hard crash; retryAfterPermissionGrant() resumes.

Fixes:
- Android: SIGSEGV on camera switch — recreate BlinkID session instead
  of rebinding CameraX; 500 ms drain before unbindAll().
- Android: session identity guard drops stale callbacks from prior session.
- iOS: NSNull vs absent-key distinction in ScanningSettings — null modules
  now correctly disable rather than falling back to SDK defaults.
- iOS: logSessionSettings wrapped in #if DEBUG; source label now correctly
  identifies the calling path (performScan / directApi / customScanner).

Example app:
- custom_scanner_screen.dart — guidance overlay, flip animation, 10 s
  scan timeout with retry dialog, camera switch button.
- home_screen.dart — structured result card: face avatar, personal info,
  document info, document images (front, back, signature).

Docs: README updated with Custom scanner UI section covering lifecycle,
guidance stream, flip phase, camera selection, and debug logging.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Custom scanner API and Flutter flow

Layer / File(s) Summary
Scanner controller, guidance, settings, and platform view
BlinkID/lib/src/scanner/*, BlinkID/lib/src/blinkid_settings.dart, BlinkID/lib/blinkid_flutter.dart
Added scanner lifecycle APIs, guidance types, scan phases, exceptions, platform-view rendering, and tri-state module settings.
Native Android and iOS scanner views
BlinkID/android/src/main/kotlin/..., BlinkID/ios/blinkid_flutter/Sources/...
Added platform-view factories and native camera pipelines with permission handling, frame processing, guidance events, result delivery, camera switching, and disposal.
Example scanning workflows
BlinkID/example/lib/*, BlinkID/example/README.md
Added native and custom scanning flows with timeout recovery, two-sided scanning, guidance overlays, camera switching, license configuration, and result rendering.
Example platform projects
BlinkID/example/android/*, BlinkID/example/ios/*, BlinkID/example/pubspec.yaml
Added complete Android and iOS Flutter project configuration, permissions, launch resources, build wiring, and metadata.
Build, serialization, and documentation support
BlinkID/android/build.gradle, BlinkID/ios/.../Serialization/*, BlinkID/build.yaml, README.md
Added CameraX and Kotlin build support, dependency constraints, JSON serialization updates, logging changes, formatting changes, and custom scanner documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ExampleApp
  participant BlinkIdScannerController
  participant BlinkIdScannerView
  participant NativeCamera
  ExampleApp->>BlinkIdScannerController: initialize and scan
  BlinkIdScannerController->>BlinkIdScannerView: create scanner platform view
  BlinkIdScannerController->>NativeCamera: start native scanning
  NativeCamera-->>BlinkIdScannerController: guidance and result events
  BlinkIdScannerController-->>ExampleApp: update status, phase, and result
Loading

Suggested reviewers: mipar52

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding BlinkIdScannerView for custom scanner UI. This matches the core functionality introduced across the entire changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@alboiuvlad29 alboiuvlad29 changed the title feat: add BlinkIdScannerView for custom scanner UI feat: Add BlinkIdScannerView for custom scanner UI Aug 4, 2026
_controller.cancel() throws BlinkIdScanCancelException which is already
caught in _startScanning() and calls _safePop(). The button's own
_safePop() caused two Navigator.pop() calls.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (3)
BlinkID/lib/src/scanner/blinkid_guidance.dart (2)

1-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Convert the header block to a doc comment.

BlinkIdGuidance is exported as public API from BlinkID/lib/blinkid_flutter.dart. The header uses // comments, so dartdoc does not attach this text to the class. Use /// on the lines that describe the class contract, and keep internal implementation notes as //.

♻️ Proposed doc-comment conversion
-// Guidance states emitted by [BlinkIdScannerController.guidanceStream].
-//
-// Android-verified DetectionStatus mapping (from compiled SDK):
-//   CameraTooFar              → tooFar
+/// Guidance states emitted by [BlinkIdScannerController.guidanceStream].
+///
+/// Android-verified DetectionStatus mapping (from compiled SDK):
+///   CameraTooFar              → tooFar
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BlinkID/lib/src/scanner/blinkid_guidance.dart` around lines 1 - 17, Convert
the public API description immediately above BlinkIdGuidance from // comments to
/// doc-comment lines so dartdoc attaches the guidance-state mapping and stream
behavior to the class; retain internal notes such as platform-specific
implementation details as regular // comments.

41-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Surface unknown guidance values in debug builds.

fromString maps every unrecognized value to searching. The header comment states that the iOS cases are unconfirmed. If the iOS native layer emits a different spelling, the overlay shows "searching" forever and no diagnostic appears. Add a debug-only signal so a native/Dart string mismatch is visible during development. Keep the safe production fallback.

♻️ Proposed debug signal
   static BlinkIdGuidance fromString(String value) => switch (value) {
+    'searching' => const BlinkIdGuidance.searching(),
     'tooFar' => const BlinkIdGuidance.tooFar(),
@@
-    _ => const BlinkIdGuidance.searching(),
+    _ => _unknown(value),
   };
+
+  static BlinkIdGuidance _unknown(String value) {
+    assert(() {
+      debugPrint('[BlinkID] Unknown guidance value: "$value"');
+      return true;
+    }());
+    return const BlinkIdGuidance.searching();
+  }

_unknown requires import 'package:flutter/foundation.dart';.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BlinkID/lib/src/scanner/blinkid_guidance.dart` around lines 41 - 55, Update
BlinkIdGuidance.fromString to detect unrecognized values before the existing
searching fallback, and emit the proposed _unknown debug signal using Flutter
foundation utilities. Keep the production behavior unchanged by retaining
searching as the fallback when assertions/debug-only code is disabled.
BlinkID/lib/src/scanner/blinkid_scanner_controller.dart (1)

146-165: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cancel the previous guidance subscription before reassigning it.

Line 152 overwrites _guidanceSub without cancelling the existing subscription. If onPlatformViewCreated runs a second time, the first EventChannel subscription stays active. Two listeners then drive _onGuidanceEvent, and the phase state machine advances twice per frame. A hot restart or a platform-view recreation reaches this path.

Line 164 also sets ready unconditionally, which would reset a cameraPermissionRequired or scanning status on recreation.

🐛 Proposed fix
   void onPlatformViewCreated(int id) {
+    _guidanceSub?.cancel().ignore();
+    _guidanceSub = null;
     final methodChannel = MethodChannel('com.microblink.blinkid.flutter/scanner/$id');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BlinkID/lib/src/scanner/blinkid_scanner_controller.dart` around lines 146 -
165, Update onPlatformViewCreated to cancel or otherwise dispose the existing
_guidanceSub before assigning the new EventChannel subscription, preventing
duplicate guidance listeners after recreation. Also avoid unconditionally
resetting the scanner status to ready; preserve cameraPermissionRequired or
scanning states when the platform view is recreated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidFlutterPlugin.kt`:
- Around line 389-403: Restore the ActivityResultListener registration in
onReattachedToActivityForConfigChanges alongside the existing permission
listener, matching the listener setup in onAttachedToActivity so performScan
delivers scan results after configuration changes.

In
`@BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerView.kt`:
- Around line 327-335: Suppress guidance events from obsolete scanning sessions
by validating session identity before dispatching normal guidance: in
BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerView.kt
lines 327-335, require scanningSession === session before
guidanceEventSink?.success; apply the equivalent blinkIdSession === session
check in
BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift
lines 310-318. Preserve the existing guidance selection logic.
- Around line 342-345: Wrap the camera selection, unbind, and bind operations in
the scanner view’s camera setup flow with try/catch, including
resolveCameraSelector and bindToLifecycle. When an exception occurs, invoke
methodChannel.invokeMethod("onScanError", ...) with the error details so
main-executor camera failures are reported instead of escaping unhandled.
- Around line 276-277: Wrap the imageProxy processing logic in a try/finally
block to guarantee imageProxy closure even when exceptions occur. Move the
current imageProxy.close() call from the success path into a finally block, and
ensure the try block includes the InputImage.createFromCameraXImageProxy and
session.process calls along with any post-processing result handling that
follows them. This ensures imageProxy is closed regardless of whether InputImage
creation, session.process execution, or downstream result handling throws an
exception.

In `@BlinkID/example/ios/Runner.xcodeproj/project.pbxproj`:
- Line 387: Remove the hardcoded DEVELOPMENT_TEAM assignment from all listed
build configuration entries in the project, leaving team selection available to
each developer’s local Xcode signing settings.

In `@BlinkID/example/lib/custom_scanner_screen.dart`:
- Around line 209-250: Update the permission button handler in the scanner
screen to request Permission.camera before invoking
_controller.retryAfterPermissionGrant(). Only reset and restart scanning after
the OS reports that camera permission was granted, while preserving the existing
permanentlyDenied behavior and _startScanning flow.
- Around line 104-107: Update _switchCamera to serialize camera changes: track
whether a switch is in progress, and queue the latest requested camera or reject
additional requests until _controller.switchCamera settles. Ensure the
in-progress state is cleared on both success and failure, preventing concurrent
native switchCamera calls while preserving the requested camera state.
- Around line 20-31: Make _safePop idempotent so repeated calls from the close
handler and scanner loop exit the scanner route only once. Add a navigation
guard that is set before popping, and have subsequent calls return without
invoking Navigator.pop while preserving timer cancellation and mounted checks.

In `@BlinkID/example/lib/home_screen.dart`:
- Around line 62-69: Update _openCustomScanner to clear both _result and _error
before pushing CustomScannerScreen, matching the reset behavior in _performScan.
Preserve the existing result assignment after navigation returns, including
leaving the cleared state intact when the custom scan is canceled.

In `@BlinkID/example/README.md`:
- Around line 15-19: The README documents DirectAPI scanning from device gallery
images with MultiSide and SingleSide options, but home_screen.dart only exposes
native and custom scan actions, and pubspec.yaml has no gallery integration
dependencies, making the documented flow unrunnable. Either remove the DirectAPI
scanning section from the README entirely, or implement the missing gallery
integration by adding required dependencies to pubspec.yaml and adding the
DirectAPI scan actions to home_screen.dart to match the documented behavior.
- Line 34: Add language specifiers to the fenced code blocks in the README.md
file to comply with markdown linting. At line 34 where the environment key
example is shown, add the language identifier dotenv after the opening backticks
(```dotenv). At line 61 where the directory tree is shown, add the language
identifier text after the opening backticks (```text). This will resolve the
MD040 linting violations for both code blocks.
- Around line 33-43: The README instructions tell users to set license keys in
.env and then run plain flutter run, but home_screen.dart uses
String.fromEnvironment() to read BLINKID_LICENSE_KEY_ANDROID and
BLINKID_LICENSE_KEY_IOS as compile-time Dart defines, which plain flutter run
does not provide. Update the "Run" section in the README to document the correct
command syntax using --dart-define flags to pass the license keys at compile
time, or alternatively add a .env loader implementation to the app so the plain
flutter run approach works as documented.

In
`@BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift`:
- Around line 147-157: Update startScan’s asynchronous Task around
createScanningSession to invalidate work during scanner teardown using a
disposal or generation token. Capture the token before awaiting session
creation, then validate it after the await and return without assigning
blinkIdSession, setting isScanning, or calling result if teardown occurred;
apply the same guard to the corresponding flow at the additional referenced
location.
- Around line 28-33: The properties isScanning, isProcessingResult,
isProcessingFrame, currentFrameOrientation, debugLoggingEnabled, and
blinkIdSession are marked with nonisolated(unsafe) which bypasses Swift's
actor-based synchronization, but they are accessed and mutated from multiple
concurrent execution contexts including MainActor, capture-output queue,
ProcessingActor, and callbacks. Remove the nonisolated(unsafe) modifier from
these properties and implement proper synchronization by either placing them
behind a dedicated actor or lock-based serializer to guarantee safe access
across all these concurrent contexts.

In `@BlinkID/lib/src/scanner/blinkid_scanner_controller.dart`:
- Around line 474-484: Update
BlinkID/lib/src/scanner/blinkid_scanner_controller.dart at lines 474-484 and
418-444: add a _disposed flag and shared pending-completer list; in dispose(),
set _disposed first, unregister the method-call handler before invoking native
disposal, and complete every pending _awaitReady completer with
BlinkIdScanDisposeException. In _handleMethodCall and _setStatus, return
immediately when disposed; in the scan/_awaitReady flow, return Future.error
with the same exception when disposed and register each local completer for
disposal draining.
- Around line 418-444: Update _awaitReady and dispose so readiness waiters are
tracked in a _pendingReady collection and every pending completer is completed
with BlinkIdScanDisposeException during disposal. Ensure listener registration
and removal are skipped or safely handled once disposed, so callers awaiting
scan() do not hang or trigger ChangeNotifier errors.
- Around line 126-141: Update initialize so a failed loadBlinkIdSdk attempt
restores _status to BlinkIdScannerStatus.uninitialized before rethrowing,
allowing subsequent initialization retries and preventing reset from exposing
incomplete creation parameters. Preserve the existing wrapped exception behavior
and success path.

In `@README.md`:
- Around line 675-677: Update the code fence introducing the BlinkID.strings
example in the README to declare the strings language as `strings`, while
leaving the example content unchanged.

---

Nitpick comments:
In `@BlinkID/lib/src/scanner/blinkid_guidance.dart`:
- Around line 1-17: Convert the public API description immediately above
BlinkIdGuidance from // comments to /// doc-comment lines so dartdoc attaches
the guidance-state mapping and stream behavior to the class; retain internal
notes such as platform-specific implementation details as regular // comments.
- Around line 41-55: Update BlinkIdGuidance.fromString to detect unrecognized
values before the existing searching fallback, and emit the proposed _unknown
debug signal using Flutter foundation utilities. Keep the production behavior
unchanged by retaining searching as the fallback when assertions/debug-only code
is disabled.

In `@BlinkID/lib/src/scanner/blinkid_scanner_controller.dart`:
- Around line 146-165: Update onPlatformViewCreated to cancel or otherwise
dispose the existing _guidanceSub before assigning the new EventChannel
subscription, preventing duplicate guidance listeners after recreation. Also
avoid unconditionally resetting the scanner status to ready; preserve
cameraPermissionRequired or scanning states when the platform view is recreated.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 85dd3a58-28e5-4a46-ac33-a068ac8362a4

📥 Commits

Reviewing files that changed from the base of the PR and between 4fbf6da and 18f9ea6.

⛔ Files ignored due to path filters (25)
  • BlinkID/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • BlinkID/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • BlinkID/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • BlinkID/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • BlinkID/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata is excluded by !**/*.xcworkspace/contents.xcworkspacedata
  • BlinkID/example/ios/Runner.xcworkspace/contents.xcworkspacedata is excluded by !**/*.xcworkspace/contents.xcworkspacedata
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png is excluded by !**/*.png
  • BlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png is excluded by !**/*.png
📒 Files selected for processing (68)
  • .vscode/launch.json
  • BlinkID/.gitignore
  • BlinkID/android/build.gradle
  • BlinkID/android/settings.gradle
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerView.kt
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerViewFactory.kt
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidDeserializationUtils.kt
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidFlutterPlugin.kt
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidSerializationUtils.kt
  • BlinkID/build.yaml
  • BlinkID/example/.env.example
  • BlinkID/example/.gitignore
  • BlinkID/example/.metadata
  • BlinkID/example/README.md
  • BlinkID/example/analysis_options.yaml
  • BlinkID/example/android/app/build.gradle
  • BlinkID/example/android/app/src/main/AndroidManifest.xml
  • BlinkID/example/android/app/src/main/kotlin/com/flutter/scanner/example/MainActivity.kt
  • BlinkID/example/android/app/src/main/res/drawable/ic_launcher_background.xml
  • BlinkID/example/android/app/src/main/res/drawable/ic_launcher_foreground.xml
  • BlinkID/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  • BlinkID/example/android/app/src/main/res/values/styles.xml
  • BlinkID/example/android/build.gradle
  • BlinkID/example/android/gradle.properties
  • BlinkID/example/android/gradle/gradle-daemon-jvm.properties
  • BlinkID/example/android/gradle/wrapper/gradle-wrapper.properties
  • BlinkID/example/android/settings.gradle
  • BlinkID/example/ios/.gitignore
  • BlinkID/example/ios/Flutter/AppFrameworkInfo.plist
  • BlinkID/example/ios/Flutter/Debug.xcconfig
  • BlinkID/example/ios/Flutter/Release.xcconfig
  • BlinkID/example/ios/Runner.xcodeproj/project.pbxproj
  • BlinkID/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  • BlinkID/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  • BlinkID/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  • BlinkID/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  • BlinkID/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  • BlinkID/example/ios/Runner/AppDelegate.swift
  • BlinkID/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  • BlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  • BlinkID/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  • BlinkID/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
  • BlinkID/example/ios/Runner/Base.lproj/Main.storyboard
  • BlinkID/example/ios/Runner/Info.plist
  • BlinkID/example/ios/Runner/Runner-Bridging-Header.h
  • BlinkID/example/ios/Runner/SceneDelegate.swift
  • BlinkID/example/ios/RunnerTests/RunnerTests.swift
  • BlinkID/example/lib/app.dart
  • BlinkID/example/lib/custom_scanner_screen.dart
  • BlinkID/example/lib/home_screen.dart
  • BlinkID/example/lib/main.dart
  • BlinkID/example/pubspec.yaml
  • BlinkID/example/test/widget_test.dart
  • BlinkID/ios/blinkid_flutter/Package.swift
  • BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift
  • BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerViewFactory.swift
  • BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkidFlutterPlugin.swift
  • BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/Serialization/BlinkIdDeserializationUtils.swift
  • BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/Serialization/BlinkIdSerializationUtils.swift
  • BlinkID/lib/blinkid_flutter.dart
  • BlinkID/lib/src/blinkid_settings.dart
  • BlinkID/lib/src/blinkid_settings.g.dart
  • BlinkID/lib/src/scanner/blinkid_guidance.dart
  • BlinkID/lib/src/scanner/blinkid_scanner_controller.dart
  • BlinkID/lib/src/scanner/blinkid_scanner_view.dart
  • BlinkID/lib/src/types.dart
  • BlinkID/lib/src/types.g.dart
  • README.md

Comment thread BlinkID/example/ios/Runner.xcodeproj/project.pbxproj Outdated
Comment thread BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift Outdated
Comment thread BlinkID/lib/src/scanner/blinkid_scanner_controller.dart
Comment thread BlinkID/lib/src/scanner/blinkid_scanner_controller.dart
Comment thread BlinkID/lib/src/scanner/blinkid_scanner_controller.dart
Comment thread README.md
Replace the all-null early-return block with inline ?: defaults per field.
Semantics unchanged — absent/null key still yields SDK-default module
settings — but mirrors how iOS handles the same case per-field.
iOS BlinkIdScannerView:
- Replace nonisolated(unsafe) vars with NSLock for correct cross-thread access
- Add _claimFrame() atomic helper to eliminate separate guard + assignment races
- Track _startScanTask so in-flight session setup can be cancelled on teardown
- Guard guidanceEventSink callbacks against stale session references

Android BlinkIdScannerView:
- Wrap frame analysis callback in try-catch to prevent uncaught exceptions crashing the analyzer thread
- Add addActivityResultListener in onAttachedToActivity

Dart scanner controller:
- On SDK load failure reset to uninitialized instead of error state (caller decides)
- Cancel existing guidance subscription in onPlatformViewCreated to prevent leaks on reuse
- Track _pendingReady completers and reject them all on dispose

Dart blinkid_guidance:
- Promote block comments to doc comments; add _unknown() fallback for unmapped strings

Example app:
- Add permission_handler for proper system settings redirect on permanently denied camera
- Guard _safePop with _isPopping flag to prevent concurrent double-pop race
- Guard _switchCamera with _isSwitchingCamera flag
- Clear result/error state before opening custom scanner
- Remove DEVELOPMENT_TEAM from example pbxproj; update README
Android emits the string "searching" for the searching/idle state but
it had no entry in the from-string switch, falling through to _unknown().
Dart:
- Add BlinkIdScanCameraSwitchException distinct from BlinkIdScanCancelException
  so scan loop can continue after switchCamera() without treating it as cancel
- Guard scan() against double-start with StateError
- Gate _handleMethodCall error logging on _debugLoggingEnabled
- Update guidance comment to reflect neither platform emits holdStill/blur/glare

Android BlinkIdScannerView:
- Mark debugLoggingEnabled/scanningSession/isScanning/preferredCameraOverride
  @volatile — all read from analyzer thread, written from main thread
- Drain analysisExecutor before setupCamera() in retryCamera and switchCamera
  to prevent race between in-flight frame and CameraX rebind
- Guard startScan against concurrent call (pendingStartResult != null)
- Catch and log analyzer exceptions via onDebugLog instead of crashing silently
- Move lifecycleRegistry.DESTROYED after executor drain in teardown()

iOS BlinkIdScannerView:
- Emit onScanError when camera device or session setup fails instead of
  returning silently, so Flutter can surface the error state

Example:
- Catch BlinkIdScanCameraSwitchException separately with continue
- Remove the status == .initializing hack from BlinkIdScanCancelException handler
Android & iOS BlinkIdScannerView:
- Emit 'blur' / 'glare' guidance from frame analysis (blurDetectionStatus /
  glareDetectionStatus == Detected) ahead of the DetectionStatus fallback
- Pass scan result as raw JSON string to onScanResult instead of
  deserializing to a nested Map — avoids Map<Object?,Object?> from
  StandardMessageCodec and removes the toNestedMap/toNestedList helpers
- Fix EventChannel teardown ordering: null guidanceEventSink in teardown()
  for immediate effect; setStreamHandler(null/nil) in the 'dispose'
  method-channel handler (Dart's 'cancel' always arrives first on same queue)

Dart scanner controller:
- cancel() when status is initializing now rejects all _pendingReady
  completers with BlinkIdScanCancelException so scan() unblocks immediately
  (fixes X button being stuck during camera switch)
- Simplify onScanResult JSON parsing: String path skips redundant Map.from()

Dart blinkid_guidance:
- Promote blur/glare to "Quality hints (emitted by both platforms)" section
- Update doc comment mapping table to reflect confirmed platform behaviour
Android: barcodeModule was missing ?: BarcodeModuleSettings() fallback
  added in the earlier ScanningSettings refactor — now consistent with
  mrz/viz/documentCapture modules.

Example: BlinkIdSdkInitException during initialize() now shows a snackbar
  and pops the scanner instead of returning silently.

README: correct switchCamera() docs to reference BlinkIdScanCameraSwitchException
  (was stale BlinkIdScanCancelException after the dedicated exception was added).
…t-cam rotation

Camera result deferral (Android + iOS):
- switchCamera/retryCamera no longer complete their Flutter result immediately;
  result is held in pendingCameraResult and completed from completeCameraResult()
  once the camera has actually bound (or definitively failed). Dart await on
  switchCamera() is now accurate rather than resolving before the bind.
- switchCamera() returns the lens actually bound (PreferredCamera), which may
  differ from the request on devices without a front camera. Also exposed as
  controller.activeCamera.
- abortPendingStart() / abortPendingCameraResult() helpers clean up in-flight
  operations on cancel, switch, and dispose.

Android:
- startJob + ensureActive() guard: a session created during an in-flight
  startScan is discarded if cancelled before it can be installed.
- resolveCameraLens() returns a String; preferredCameraOverride is updated to
  the actually resolved lens; cameraSelectorFor() is a separate helper.
- resolveModuleSettings() generic replaces 4 nearly-identical optional*Settings()
  helpers in BlinkidDeserializationUtils.
- All Log.i() → Log.d() gated behind Log.isLoggable(TAG, Log.DEBUG).
- Plugin logs gated behind Log.isLoggable as well.
- CAMERA permission declared in plugin manifest (merged automatically).

iOS:
- Front camera landscape rotation fix: front sensor is 180° rotated vs back in
  landscape; frontCameraLandscapeRotationAngle / frontCameraLandscapeAvOrientation
  extensions handle the inversion; automaticallyAdjustsVideoMirroring disabled
  before updateVideoOrientation() so manual mirroring isn't overridden.
- preferredCameraOverride pinned to resolved lens early in performCameraSetup().
- UIApplication.shared.windows → connectedScenes (deprecated API fix).
- Plugin logs wrapped in #if DEBUG.

Dart scanner controller:
- _disposed flag: guards _handleMethodCall, _onGuidanceEvent, _setStatus and
  notifyListeners() against late native callbacks racing dispose().
- dispose() sets _disposed first, nulls method call handler before invoking
  native dispose, closes guidance controller after.
- switchCamera() returns Future<PreferredCamera>; _retryCamera() updates
  _activeCamera from the resolved lens string.
- _lastEmittedGuidance: collapses consecutive duplicate guidance frames before
  they reach guidanceStream (native emits ~30/s; exact duplicates add no info).
  Phase change resets the last value so "searching" is re-emitted on back side.

Dart BlinkIdScanningSettings:
- Tri-state sentinel (_unsetModule): omit = SDK default (enabled), explicit null
  = disabled. @JsonSerializable(createFactory: false) + hand-written fromJson
  preserve absent-vs-null distinction on the way back in.
- @jsonkey(includeIfNull: false) annotations removed; toJson() now emits null for
  disabled modules so native can honour the explicit disable.

Example custom_scanner_screen.dart:
- Guidance state lifted to parent State: single _guidanceSub drives both the
  scan-timeout timer and the display. _GuidanceOverlay takes text + switcherKey
  instead of a controller reference; _FlipOverlay takes an onFlipComplete
  callback. Phase change resets guidance text immediately.
- _FlipOverlayState.dispose() calls _completeFlip() so onFlipComplete is
  guaranteed even if the overlay is torn down before animation finishes.
- canInteract excludes status == .initializing to hide X/camera buttons
  during camera rebind.

README: camera permission section, module omit-vs-null clarification,
  switchCamera return value, status table updated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
README.md (2)

414-424: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle camera-switch cancellation in the scan loop.

switchCamera() is documented to complete the active scan() future with BlinkIdScanCameraSwitchException, but this loop catches only BlinkIdScanCancelException and BlinkIdScanResetException. Add a camera-switch catch so _run() starts the next scan instead of ending with an unhandled exception.

Proposed fix
       } on BlinkIdScanResetException {
         // controller.reset() was called — loop restarts scan()
+      } on BlinkIdScanCameraSwitchException {
+        // switchCamera() canceled this scan; the loop restarts it.
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 414 - 424, Update the scan loop around
_controller.scan() in _run() to catch BlinkIdScanCameraSwitchException and
continue looping so the next scan starts, matching the existing reset handling;
preserve the current cancel behavior that pops the navigator and returns.

393-405: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Import dart:async before calling unawaited.

The quick-start imports only package:blinkid_flutter/blinkid_flutter.dart but calls unawaited(_run()); that helper comes from dart:async, so this example will not compile unless the standard library is imported.

Proposed fix
+import 'dart:async';
 import 'package:blinkid_flutter/blinkid_flutter.dart';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 393 - 405, Add the dart:async import to the README
quick-start example before using unawaited in _ScanScreenState.initState, while
retaining the existing blinkid_flutter import and scanner setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift`:
- Line 331: Serialize all AVCaptureSession lifecycle and configuration
operations used by startScan(), switchCamera, and teardown() on a single serial
capture-session queue instead of the global queue. Ensure queued startRunning
work verifies that its captured session is still the current captureSession
before starting, so replaced or nulled sessions cannot restart or receive
frames.

---

Outside diff comments:
In `@README.md`:
- Around line 414-424: Update the scan loop around _controller.scan() in _run()
to catch BlinkIdScanCameraSwitchException and continue looping so the next scan
starts, matching the existing reset handling; preserve the current cancel
behavior that pops the navigator and returns.
- Around line 393-405: Add the dart:async import to the README quick-start
example before using unawaited in _ScanScreenState.initState, while retaining
the existing blinkid_flutter import and scanner setup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bb719ccc-4648-4372-8d0f-16b7a59e4f12

📥 Commits

Reviewing files that changed from the base of the PR and between 9b88f2b and fc3893a.

📒 Files selected for processing (11)
  • BlinkID/android/src/main/AndroidManifest.xml
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkIdScannerView.kt
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidDeserializationUtils.kt
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidFlutterPlugin.kt
  • BlinkID/example/lib/custom_scanner_screen.dart
  • BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift
  • BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkidFlutterPlugin.swift
  • BlinkID/lib/src/blinkid_settings.dart
  • BlinkID/lib/src/blinkid_settings.g.dart
  • BlinkID/lib/src/scanner/blinkid_scanner_controller.dart
  • README.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • BlinkID/lib/src/blinkid_settings.g.dart
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidDeserializationUtils.kt
  • BlinkID/example/lib/custom_scanner_screen.dart
  • BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkidFlutterPlugin.swift
  • BlinkID/android/src/main/kotlin/com/microblink/blinkid/flutter/BlinkidFlutterPlugin.kt
  • BlinkID/lib/src/scanner/blinkid_scanner_controller.dart

Comment thread BlinkID/ios/blinkid_flutter/Sources/blinkid_flutter/BlinkIdScannerView.swift Outdated
…EADME fixes

Use a per-view serial DispatchQueue for all startRunning()/stopRunning() calls:
- Apple documents both as blocking — neither belongs on the main thread.
- Serial ordering (enqueued from main thread in call order) prevents a stale
  session's queued start from running after a later switchCamera()/teardown()
  has already stopped and detached it — no generation counter needed.
- stopCaptureSession() and teardown() capture the session reference before
  nil-ing captureSession, then dispatch stopRunning() on the queue so the stop
  is always ordered after this session's own queued start.

README: add missing 'dart:async' import to custom scanner example; add
  BlinkIdScanCameraSwitchException catch to the scan loop snippet.
types.dart — BarcodeModuleSettings, DocumentCaptureModuleSettings,
MrzModuleSettings, VizModuleSettings:
- All fields marked final; constructors made const.
- copyWith() added to each so callers can produce modified copies without
  constructing from scratch.

blinkid_settings.dart — BlinkIdScanningSettings:
- Replace _unsetModule sentinel + initialiser list with plain const-default
  parameters (this.field = const ModuleSettings()). Because the defaults are
  now const instances, the hand-written fromJson still distinguishes
  "key absent → leave the const default" from "key present and null →
  disabled" correctly via containsKey().
- Simplify the comment: the reason createFactory: false is still needed hasn't
  changed, only the constructor shape.

sample_files/module_settings_panel.dart:
- Switch all _updateBarcode/_update callbacks from direct mutation to copyWith()
  to match the now-immutable module settings types.
…t tests

iOS: teardown() now calls stopCaptureSession() instead of repeating the
  same capture/nil/sessionQueue.async pattern inline — eliminates the risk
  of the two diverging and ensures videoOutput is also cleared on teardown.

Tests (BlinkID/test/):
- blinkid_scanner_controller_test.dart: pure-Dart state-machine tests
  driven via mocked method/event channels (no device or license required).
  Covers: in-flight startScan surviving cancel/switchCamera; switchCamera
  not reporting ready until the native reply actually lands and returning
  the resolved lens; late native callbacks after dispose() not throwing or
  corrupting status; in-flight scan completing with BlinkIdScanDisposeException.
- blinkid_settings_test.dart: verifies the tri-state module contract on
  BlinkIdScanningSettings (unset → SDK-default enabled; explicit null →
  disabled; round-trip through toJson/fromJson is stable).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant