Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ef2f6a7
add iOS qrcode opener and aadhaar screen
remicolin Sep 10, 2025
c2f5ed4
Merge branch 'dev' into app/add-aadhaar-onboarding
transphorm Sep 11, 2025
1d3049f
format
transphorm Sep 11, 2025
6705d8c
fix test
transphorm Sep 11, 2025
fe9ef1a
Merge branch 'dev' of https://github.com/selfxyz/self into app/add-aa…
remicolin Sep 16, 2025
999937a
add Image-picker android (#1077)
seshanthS Sep 16, 2025
81aa409
Merge branch 'app/add-aadhaar-onboarding' of https://github.com/selfx…
remicolin Sep 16, 2025
6f1c97d
feat: implement Aadhaar upload success and error screens, enhance Aad…
remicolin Sep 17, 2025
e36f3db
feat: generate mock aadhar (#1083)
seshanthS Sep 17, 2025
5ab135f
Merge branch 'dev' of https://github.com/selfxyz/self into app/add-aa…
remicolin Sep 17, 2025
7e6b5fc
update protocolStore, update types, start modifying provingMachine
remicolin Sep 18, 2025
3933f59
Register mock aadhar (#1093)
seshanthS Sep 18, 2025
f1f65c6
Add Aadhaar support to ID card component and screens
remicolin Sep 18, 2025
b6bf07b
aadhaar disclose - wip (#1094)
seshanthS Sep 19, 2025
3040442
fix: timestamp cal of extractQRDataFields
Vishalkulkarni45 Sep 19, 2025
da64feb
Feat/aadhar fixes (#1099)
seshanthS Sep 19, 2025
f9b0e68
yarn nice
remicolin Sep 19, 2025
abbf13e
Merge branch 'dev' of https://github.com/selfxyz/self into app/add-aa…
remicolin Sep 19, 2025
a33580e
run prettier
remicolin Sep 19, 2025
fdcb6d9
Add mock Aadhaar certificates for development
remicolin Sep 19, 2025
708bad9
prettier write
remicolin Sep 19, 2025
3c0c2dd
add 'add-aadhaar' button (#1100)
seshanthS Sep 19, 2025
ab61711
Update .gitleaks.toml to include path for mock certificates in the co…
remicolin Sep 19, 2025
ea89c5f
yarn nice
remicolin Sep 19, 2025
3474761
Enhance Aadhaar error handling with specific error types
remicolin Sep 19, 2025
63f4076
Update passport handling in proving machine to support Aadhaar docume…
remicolin Sep 19, 2025
9d60335
tweak layout, text, change email to support, hide help button
transphorm Sep 19, 2025
01d630a
fix ci, remove aadhaar logging, add aadhaar events
transphorm Sep 19, 2025
5d264bf
remove unused aadhaar tracking events
transphorm Sep 19, 2025
c79adaa
update globs
transphorm Sep 19, 2025
1743737
fix gitguardian config
transphorm Sep 20, 2025
12c3866
don't track id
transphorm Sep 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions app/ios/PhotoLibraryQRScannerViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//
// PhotoLibraryQRScannerViewController.swift
// Self
//
// Created by Rémi Colin on 09/09/2025.
//


// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11

//
// PhotoLibraryQRScannerViewController.swift
// OpenPassport
//
// Created by AI Assistant on 01/03/2025.
//

import Foundation
import UIKit
import CoreImage
import Photos

class PhotoLibraryQRScannerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var completionHandler: ((String) -> Void)?
Comment on lines +18 to +24
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Adopt PhotosUI and update protocol conformance.

Add PhotosUI and conform to PHPickerViewControllerDelegate:

 import Foundation
 import UIKit
 import CoreImage
 import Photos
+import PhotosUI
 
-class PhotoLibraryQRScannerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
+class PhotoLibraryQRScannerViewController: UIViewController, PHPickerViewControllerDelegate, UINavigationControllerDelegate {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import Foundation
import UIKit
import CoreImage
import Photos
class PhotoLibraryQRScannerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var completionHandler: ((String) -> Void)?
import Foundation
import UIKit
import CoreImage
import Photos
import PhotosUI
class PhotoLibraryQRScannerViewController: UIViewController, PHPickerViewControllerDelegate, UINavigationControllerDelegate {
var completionHandler: ((String) -> Void)?

var errorHandler: ((Error) -> Void)?

override func viewDidLoad() {
super.viewDidLoad()
checkPhotoLibraryPermissionAndPresentPicker()
}

Comment on lines +27 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Switch to PHPicker on load.

   override func viewDidLoad() {
     super.viewDidLoad()
-    checkPhotoLibraryPermissionAndPresentPicker()
+    presentPHPicker()
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override func viewDidLoad() {
super.viewDidLoad()
checkPhotoLibraryPermissionAndPresentPicker()
}
override func viewDidLoad() {
super.viewDidLoad()
presentPHPicker()
}

private func checkPhotoLibraryPermissionAndPresentPicker() {
let status = PHPhotoLibrary.authorizationStatus()

switch status {
case .authorized, .limited:
presentImagePicker()
case .notDetermined:
PHPhotoLibrary.requestAuthorization { [weak self] status in
DispatchQueue.main.async {
if status == .authorized || status == .limited {
self?.presentImagePicker()
} else {
self?.handlePermissionDenied()
}
}
}
case .denied, .restricted:
handlePermissionDenied()
@unknown default:
handlePermissionDenied()
}
}
Comment on lines +32 to +53
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use PHPicker (no permission prompt) instead of legacy permission flow.

UIImagePickerController + PHPhotoLibrary.requestAuthorization() is legacy and increases friction. PHPickerViewController requires no permission, aligns with modern privacy, and reduces denial-path errors.

Replace the permission block with a PHPicker presenter:

-  private func checkPhotoLibraryPermissionAndPresentPicker() {
-    let status = PHPhotoLibrary.authorizationStatus()
-    switch status {
-    case .authorized, .limited:
-      presentImagePicker()
-    case .notDetermined:
-      PHPhotoLibrary.requestAuthorization { [weak self] status in
-        DispatchQueue.main.async {
-          if status == .authorized || status == .limited {
-            self?.presentImagePicker()
-          } else {
-            self?.handlePermissionDenied()
-          }
-        }
-      }
-    case .denied, .restricted:
-      handlePermissionDenied()
-    @unknown default:
-      handlePermissionDenied()
-    }
-  }
+  private func presentPHPicker() {
+    var config = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
+    config.filter = .images
+    config.selectionLimit = 1
+    let picker = PHPickerViewController(configuration: config)
+    picker.delegate = self
+    present(picker, animated: true)
+  }

And in viewDidLoad call presentPHPicker() instead of the permission gate (see diff in next comment).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private func checkPhotoLibraryPermissionAndPresentPicker() {
let status = PHPhotoLibrary.authorizationStatus()
switch status {
case .authorized, .limited:
presentImagePicker()
case .notDetermined:
PHPhotoLibrary.requestAuthorization { [weak self] status in
DispatchQueue.main.async {
if status == .authorized || status == .limited {
self?.presentImagePicker()
} else {
self?.handlePermissionDenied()
}
}
}
case .denied, .restricted:
handlePermissionDenied()
@unknown default:
handlePermissionDenied()
}
}
private func presentPHPicker() {
var config = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
}
🤖 Prompt for AI Agents
In app/ios/PhotoLibraryQRScannerViewController.swift around lines 32 to 53,
replace the legacy PHPhotoLibrary.authorizationStatus() +
UIImagePickerController flow with a PHPicker-based presenter: remove the
permission switch and requestAuthorization usage and implement a
presentPHPicker() helper that configures a PHPickerConfiguration (set filter to
images and selectionLimit as needed), creates a PHPickerViewController with a
delegate, and presents it on the main thread; update viewDidLoad to call
presentPHPicker() instead of checkPhotoLibraryPermissionAndPresentPicker(), and
ensure the delegate handles picked UIImage results and denial paths where
appropriate.


private func presentImagePicker() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
imagePicker.mediaTypes = ["public.image"]
present(imagePicker, animated: true, completion: nil)
}
Comment on lines +55 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove UIImagePicker presenter (replaced by PHPicker).

The presentImagePicker() flow becomes obsolete with PHPicker. Delete this method.

🤖 Prompt for AI Agents
In app/ios/PhotoLibraryQRScannerViewController.swift around lines 55 to 61, the
presentImagePicker() method that creates and presents a UIImagePickerController
is obsolete because the project uses PHPicker; remove this entire method and any
direct references/calls to presentImagePicker(), and if those calls exist
replace them with the PHPicker-based flow already implemented (or call the
existing PHPicker wrapper) so there are no unused UIImagePickerController
presenters or dead method references left.


private func handlePermissionDenied() {
let error = NSError(
domain: "QRScannerError",
code: 1001,
userInfo: [NSLocalizedDescriptionKey: "Photo library access is required to scan QR codes from photos. Please enable access in Settings."]
)
errorHandler?(error)
dismiss(animated: true, completion: nil)
}

// MARK: - UIImagePickerControllerDelegate

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
picker.dismiss(animated: true) { [weak self] in
guard let self = self else { return }

if let selectedImage = info[.originalImage] as? UIImage {
self.detectQRCode(in: selectedImage)
} else {
let error = NSError(
domain: "QRScannerError",
code: 1002,
userInfo: [NSLocalizedDescriptionKey: "Failed to load the selected image."]
)
self.errorHandler?(error)
self.dismiss(animated: true, completion: nil)
}
}
}
Comment on lines +75 to +91
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace UIImagePicker delegates with PHPicker delegate and handle cancellation.

-  func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
-    picker.dismiss(animated: true) { [weak self] in
-      guard let self = self else { return }
-      if let selectedImage = info[.originalImage] as? UIImage {
-        self.detectQRCode(in: selectedImage)
-      } else {
-        let error = NSError(
-          domain: "QRScannerError",
-          code: 1002,
-          userInfo: [NSLocalizedDescriptionKey: "Failed to load the selected image."]
-        )
-        self.errorHandler?(error)
-        self.dismiss(animated: true, completion: nil)
-      }
-    }
-  }
-
-  func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
-    picker.dismiss(animated: true) { [weak self] in
-      let error = NSError(
-        domain: "QRScannerError",
-        code: 1003,
-        userInfo: [NSLocalizedDescriptionKey: "User cancelled photo selection."]
-      )
-      self?.errorHandler?(error)
-      self?.dismiss(animated: true, completion: nil)
-    }
-  }
+  func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
+    picker.dismiss(animated: true) { [weak self] in
+      guard let self = self else { return }
+      guard let provider = results.first?.itemProvider else {
+        let error = NSError(
+          domain: "QRScannerError",
+          code: 1003,
+          userInfo: [NSLocalizedDescriptionKey: "User cancelled photo selection."]
+        )
+        self.errorHandler?(error)
+        self.dismiss(animated: true)
+        return
+      }
+      if provider.canLoadObject(ofClass: UIImage.self) {
+        provider.loadObject(ofClass: UIImage.self) { object, loadError in
+          DispatchQueue.main.async {
+            if let loadError = loadError {
+              let error = NSError(
+                domain: "QRScannerError",
+                code: 1002,
+                userInfo: [NSLocalizedDescriptionKey: loadError.localizedDescription]
+              )
+              self.errorHandler?(error)
+              self.dismiss(animated: true)
+            } else if let image = object as? UIImage {
+              self.detectQRCode(in: image)
+            } else {
+              let error = NSError(
+                domain: "QRScannerError",
+                code: 1002,
+                userInfo: [NSLocalizedDescriptionKey: "Failed to load the selected image."]
+              )
+              self.errorHandler?(error)
+              self.dismiss(animated: true)
+            }
+          }
+        }
+      } else {
+        let error = NSError(
+          domain: "QRScannerError",
+          code: 1002,
+          userInfo: [NSLocalizedDescriptionKey: "Unsupported item type."]
+        )
+        self.errorHandler?(error)
+        self.dismiss(animated: true)
+      }
+    }
+  }

Also applies to: 93-103

🤖 Prompt for AI Agents
In app/ios/PhotoLibraryQRScannerViewController.swift around lines 75-91 (and
similarly 93-103), you are using UIImagePickerController delegates; replace them
with PHPickerViewController and implement PHPickerViewControllerDelegate
methods: create a PHPickerConfiguration (filter images, selectionLimit 1),
present PHPickerViewController, and implement picker(_:didFinishPicking:) to
dismiss the picker, extract the UIImage from the PHPickerResult's itemProvider
(loadObject of class UIImage), then call detectQRCode(in:) on the loaded image;
also handle cancellation or empty results by dismissing and invoking
errorHandler? with a suitable NSError (e.g., code 1002) so cancellation/failed
loads are reported and no retain cycles occur (use [weak self] where
appropriate).


func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true) { [weak self] in
let error = NSError(
domain: "QRScannerError",
code: 1003,
userInfo: [NSLocalizedDescriptionKey: "User cancelled photo selection."]
)
self?.errorHandler?(error)
self?.dismiss(animated: true, completion: nil)
}
}

// MARK: - QR Code Detection

private func detectQRCode(in image: UIImage) {
guard let ciImage = CIImage(image: image) else {
let error = NSError(
domain: "QRScannerError",
code: 1004,
userInfo: [NSLocalizedDescriptionKey: "Failed to process the selected image."]
)
errorHandler?(error)
dismiss(animated: true, completion: nil)
return
}

let detector = CIDetector(
ofType: CIDetectorTypeQRCode,
context: nil,
options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]
)

guard let detector = detector else {
let error = NSError(
domain: "QRScannerError",
code: 1005,
userInfo: [NSLocalizedDescriptionKey: "Failed to initialize QR code detector."]
)
errorHandler?(error)
dismiss(animated: true, completion: nil)
return
}

let features = detector.features(in: ciImage) as? [CIQRCodeFeature] ?? []

if let firstQRCode = features.first, let qrCodeString = firstQRCode.messageString {
completionHandler?(qrCodeString)
dismiss(animated: true, completion: nil)
} else {
let error = NSError(
domain: "QRScannerError",
code: 1006,
userInfo: [NSLocalizedDescriptionKey: "No QR code found in the selected image. Please try with a different image."]
)
errorHandler?(error)
dismiss(animated: true, completion: nil)
}
}
Comment on lines +107 to +150
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Move QR detection off the main thread, fix orientation, and downscale large images to avoid jank/OOM.

Heavy CI work on the main thread will stutter the UI and large photos can spike memory. Also, orientation must be respected for robust detection.

-  private func detectQRCode(in image: UIImage) {
-    guard let ciImage = CIImage(image: image) else {
+  private func detectQRCode(in image: UIImage) {
+    DispatchQueue.global(qos: .userInitiated).async {
+      let maxDim: CGFloat = 2048
+      let scaledImage: UIImage
+      if max(image.size.width, image.size.height) > maxDim {
+        let scale = maxDim / max(image.size.width, image.size.height)
+        let newSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
+        UIGraphicsBeginImageContextWithOptions(newSize, true, 1.0)
+        image.draw(in: CGRect(origin: .zero, size: newSize))
+        scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
+        UIGraphicsEndImageContext()
+      } else {
+        scaledImage = image
+      }
+
+      guard let cg = scaledImage.cgImage else {
+        DispatchQueue.main.async {
+          let error = NSError(domain: "QRScannerError", code: 1004, userInfo: [NSLocalizedDescriptionKey: "Failed to process the selected image."])
+          self.errorHandler?(error)
+          self.dismiss(animated: true)
+        }
+        return
+      }
+      let cgOrientation = CGImagePropertyOrientation(scaledImage.imageOrientation)
+      let ciImage = CIImage(cgImage: cg, options: [CIImageOption.properties: [kCGImagePropertyOrientation as String: cgOrientation.rawValue]])
+      
-      let error = NSError(
-        domain: "QRScannerError",
-        code: 1004,
-        userInfo: [NSLocalizedDescriptionKey: "Failed to process the selected image."]
-      )
-      errorHandler?(error)
-      dismiss(animated: true, completion: nil)
-      return
-    }
-
-    let detector = CIDetector(
-      ofType: CIDetectorTypeQRCode,
-      context: nil,
-      options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]
-    )
-
-    guard let detector = detector else {
-      let error = NSError(
-        domain: "QRScannerError",
-        code: 1005,
-        userInfo: [NSLocalizedDescriptionKey: "Failed to initialize QR code detector."]
-      )
-      errorHandler?(error)
-      dismiss(animated: true, completion: nil)
-      return
-    }
-
-    let features = detector.features(in: ciImage) as? [CIQRCodeFeature] ?? []
-
-    if let firstQRCode = features.first, let qrCodeString = firstQRCode.messageString {
-      completionHandler?(qrCodeString)
-      dismiss(animated: true, completion: nil)
-    } else {
-      let error = NSError(
-        domain: "QRScannerError",
-        code: 1006,
-        userInfo: [NSLocalizedDescriptionKey: "No QR code found in the selected image. Please try with a different image."]
-      )
-      errorHandler?(error)
-      dismiss(animated: true, completion: nil)
-    }
+      let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
+      guard let detector = detector else {
+        DispatchQueue.main.async {
+          let error = NSError(domain: "QRScannerError", code: 1005, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize QR code detector."])
+          self.errorHandler?(error)
+          self.dismiss(animated: true)
+        }
+        return
+      }
+      let features = detector.features(in: ciImage) as? [CIQRCodeFeature] ?? []
+      DispatchQueue.main.async {
+        if let firstQRCode = features.first, let qrCodeString = firstQRCode.messageString {
+          self.completionHandler?(qrCodeString)
+          self.dismiss(animated: true)
+        } else {
+          let error = NSError(domain: "QRScannerError", code: 1006, userInfo: [NSLocalizedDescriptionKey: "No QR code found in the selected image. Please try with a different image."])
+          self.errorHandler?(error)
+          self.dismiss(animated: true)
+        }
+      }
+    }
   }

Add this helper (outside the diffed block) to translate orientations:

fileprivate extension CGImagePropertyOrientation {
  init(_ uiOrientation: UIImage.Orientation) {
    switch uiOrientation {
    case .up: self = .up
    case .down: self = .down
    case .left: self = .left
    case .right: self = .right
    case .upMirrored: self = .upMirrored
    case .downMirrored: self = .downMirrored
    case .leftMirrored: self = .leftMirrored
    case .rightMirrored: self = .rightMirrored
    @unknown default: self = .up
    }
  }
}
🤖 Prompt for AI Agents
In app/ios/PhotoLibraryQRScannerViewController.swift around lines 107–150, the
QR detection is running heavy CI work on the main thread, ignores image
orientation, and may process huge images causing jank/OOM; move detection to a
background queue, downscale very large UIImages before creating the CIImage, and
pass the correct CGImagePropertyOrientation (use the provided fileprivate
CGImagePropertyOrientation init from UIImage.Orientation placed outside this
block) when creating the CIImage so orientation is respected; after detection
return results on the main queue to call completionHandler/errorHandler and
dismiss the controller.

}

2 changes: 2 additions & 0 deletions app/ios/QRScannerBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ @interface RCT_EXTERN_MODULE(QRScannerBridge, NSObject)

RCT_EXTERN_METHOD(scanQRCode:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(scanQRCodeFromPhotoLibrary:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)

@end
17 changes: 17 additions & 0 deletions app/ios/QRScannerBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import Foundation
import SwiftQRScanner
import React
import UIKit
import CoreImage

@objc(QRScannerBridge)
class QRScannerBridge: NSObject {
Expand All @@ -29,4 +31,19 @@ class QRScannerBridge: NSObject {
rootViewController?.present(qrScannerViewController, animated: true, completion: nil)
}
}

@objc
func scanQRCodeFromPhotoLibrary(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
DispatchQueue.main.async {
let rootViewController = UIApplication.shared.keyWindow?.rootViewController
let photoLibraryQRScanner = PhotoLibraryQRScannerViewController()
photoLibraryQRScanner.completionHandler = { result in
resolve(result)
}
photoLibraryQRScanner.errorHandler = { error in
reject("QR_SCAN_ERROR", error.localizedDescription, error)
}
rootViewController?.present(photoLibraryQRScanner, animated: true, completion: nil)
}
}
Comment on lines +35 to +48
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Present from the topmost VC and return structured cancellation codes.

Using UIApplication.shared.keyWindow?.rootViewController can be nil on iOS 13+ Scene-based apps and can cause “attempt to present...” crashes. Also, surface a stable USER_CANCELLED code (vs. parsing localized strings in JS) when the photo picker is cancelled.

Apply this diff:

   @objc
   func scanQRCodeFromPhotoLibrary(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
     DispatchQueue.main.async {
-      let rootViewController = UIApplication.shared.keyWindow?.rootViewController
-      let photoLibraryQRScanner = PhotoLibraryQRScannerViewController()
-      photoLibraryQRScanner.completionHandler = { result in
-        resolve(result)
-      }
-      photoLibraryQRScanner.errorHandler = { error in
-        reject("QR_SCAN_ERROR", error.localizedDescription, error)
-      }
-      rootViewController?.present(photoLibraryQRScanner, animated: true, completion: nil)
+      guard let presenter = RCTPresentedViewController() else {
+        reject("QR_PRESENTATION_ERROR", "Unable to find a presenter view controller.", nil)
+        return
+      }
+      let photoLibraryQRScanner = PhotoLibraryQRScannerViewController()
+      photoLibraryQRScanner.completionHandler = { result in
+        resolve(result)
+      }
+      photoLibraryQRScanner.errorHandler = { err in
+        if let nsErr = err as NSError?, nsErr.domain == "QRScannerError", nsErr.code == 1003 {
+          // Normalize cancellation for JS
+          reject("USER_CANCELLED", nsErr.localizedDescription, nil)
+        } else {
+          reject("QR_SCAN_ERROR", err.localizedDescription, err)
+        }
+      }
+      presenter.present(photoLibraryQRScanner, animated: true, completion: nil)
     }
   }

Also apply the same presentation guard pattern to scanQRCode(...) to avoid similar crashes there. For example:

// In scanQRCode(...):
DispatchQueue.main.async {
  guard let presenter = RCTPresentedViewController() else {
    reject("QR_PRESENTATION_ERROR", "Unable to find a presenter view controller.", nil)
    return
  }
  let vc = QRScannerViewController()
  vc.completionHandler = { resolve($0) }
  presenter.present(vc, animated: true)
}
🤖 Prompt for AI Agents
In app/ios/QRScannerBridge.swift around lines 35–48, the code uses
UIApplication.shared.keyWindow?.rootViewController which is nil on iOS 13+
scene-based apps and can cause presentation crashes; also cancellation from the
photo picker should surface a stable USER_CANCELLED code instead of localized
strings. Replace keyWindow usage with a safe presenter lookup (e.g., guard let
presenter = RCTPresentedViewController() else { reject("QR_PRESENTATION_ERROR",
"Unable to find a presenter view controller.", nil); return }) and present the
PhotoLibraryQRScannerViewController from that presenter. Change the errorHandler
to return a structured cancellation code (reject("USER_CANCELLED", "User
cancelled photo picker", nil)) when appropriate and keep other errors as before.
Apply the same presentation guard pattern to the scanQRCode(...) method to
prevent similar crashes.

}
14 changes: 6 additions & 8 deletions app/ios/Self.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
165E76BD2B8DC4A00000FA90 /* MRZScannerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165E76BC2B8DC4A00000FA90 /* MRZScannerModule.swift */; };
165E76BF2B8DC53A0000FA90 /* MRZScannerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 165E76BE2B8DC53A0000FA90 /* MRZScannerModule.m */; };
165E76C32B8DC8370000FA90 /* ScannerHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165E76C22B8DC8370000FA90 /* ScannerHostingController.swift */; };
1668A53F2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1668A53E2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift */; };
1686F0DC2C500F3800841CDE /* QRScannerBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1686F0DB2C500F3800841CDE /* QRScannerBridge.swift */; };
1686F0DE2C500F4F00841CDE /* QRScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1686F0DD2C500F4F00841CDE /* QRScannerViewController.swift */; };
1686F0E02C500FBD00841CDE /* QRScannerBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 1686F0DF2C500FBD00841CDE /* QRScannerBridge.m */; };
Expand Down Expand Up @@ -57,6 +58,7 @@
165E76BC2B8DC4A00000FA90 /* MRZScannerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MRZScannerModule.swift; sourceTree = "<group>"; };
165E76BE2B8DC53A0000FA90 /* MRZScannerModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MRZScannerModule.m; sourceTree = "<group>"; };
165E76C22B8DC8370000FA90 /* ScannerHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannerHostingController.swift; sourceTree = "<group>"; };
1668A53E2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryQRScannerViewController.swift; sourceTree = "<group>"; };
1686F0DB2C500F3800841CDE /* QRScannerBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerBridge.swift; sourceTree = "<group>"; };
1686F0DD2C500F4F00841CDE /* QRScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerViewController.swift; sourceTree = "<group>"; };
1686F0DF2C500FBD00841CDE /* QRScannerBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QRScannerBridge.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -102,6 +104,7 @@
13B07FAE1A68108700A75B9A /* OpenPassport */ = {
isa = PBXGroup;
children = (
1668A53E2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift */,
BF6F0D542E38ED81008EA85C /* SelfAnalytics.swift */,
BFBA0C782E33A01F00E82A52 /* NativeLoggerBridge.m */,
BFBA0C762E339D2B00E82A52 /* NativeLoggerBridge.swift */,
Expand Down Expand Up @@ -405,6 +408,7 @@
1648EB782CC9564D003BEA7D /* LottieView.swift in Sources */,
164FD9672D569A640067E63B /* QRCodeScannerViewManager.swift in Sources */,
165E76BD2B8DC4A00000FA90 /* MRZScannerModule.swift in Sources */,
1668A53F2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift in Sources */,
BF6F0D552E38ED81008EA85C /* SelfAnalytics.swift in Sources */,
BF1044812DD53540009B3688 /* LiveMRZScannerView.swift in Sources */,
164FD9692D569C1F0067E63B /* QRCodeScannerViewManager.m in Sources */,
Expand Down Expand Up @@ -785,10 +789,7 @@
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
Expand Down Expand Up @@ -878,10 +879,7 @@
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down
89 changes: 89 additions & 0 deletions app/src/components/NavBar/AadhaarNavBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.

import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button, Text, XStack, YStack } from 'tamagui';

Check warning on line 7 in app/src/components/NavBar/AadhaarNavBar.tsx

View workflow job for this annotation

GitHub Actions / build-deps

'Text' is defined but never used
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
import { ChevronLeft, HelpCircle } from '@tamagui/lucide-icons';

import { NavBar } from '@/components/NavBar/BaseNavBar';
import { black, slate100, slate200, slate300, white } from '@/utils/colors';

Check warning on line 12 in app/src/components/NavBar/AadhaarNavBar.tsx

View workflow job for this annotation

GitHub Actions / build-deps

'white' is defined but never used

Check warning on line 12 in app/src/components/NavBar/AadhaarNavBar.tsx

View workflow job for this annotation

GitHub Actions / build-deps

'slate200' is defined but never used
import { extraYPadding } from '@/utils/constants';
import { buttonTap } from '@/utils/haptic';

export const AadhaarNavBar = (props: NativeStackHeaderProps) => {
const insets = useSafeAreaInsets();

const handleClose = () => {
buttonTap();
props.navigation.goBack();
};

const handleHelp = () => {
buttonTap();
// Handle help action - could open a modal or navigate to help screen
console.log('Help pressed');
};

return (
<YStack backgroundColor={slate100}>
<NavBar.Container
backgroundColor={slate100}
barStyle={'dark'}
padding={20}
justifyContent="space-between"
paddingTop={Math.max(insets.top, 15) + extraYPadding}
paddingBottom={10}
borderBottomWidth={0}
borderBottomColor="transparent"
>
<NavBar.LeftAction
component={
<Button
unstyled
onPress={handleClose}
padding={8}
borderRadius={20}
hitSlop={10}
>
<ChevronLeft size={24} color={black} />
</Button>
}
/>

<NavBar.Title fontSize={16} color={black} fontWeight="600">
AADHAAR REGISTRATION
</NavBar.Title>

<NavBar.RightAction
component={
<Button
unstyled
onPress={handleHelp}
padding={8}
borderRadius={20}
hitSlop={10}
width={32}
height={32}
justifyContent="center"
alignItems="center"
>
<HelpCircle size={20} color={black} />
</Button>
}
/>
</NavBar.Container>

{/* Progress Bar - now part of the navbar */}
<YStack paddingHorizontal={20} paddingBottom={15} backgroundColor={slate100}>

Check warning on line 80 in app/src/components/NavBar/AadhaarNavBar.tsx

View workflow job for this annotation

GitHub Actions / build-deps

Replace `·paddingHorizontal={20}·paddingBottom={15}·backgroundColor={slate100}` with `⏎········paddingHorizontal={20}⏎········paddingBottom={15}⏎········backgroundColor={slate100}⏎······`
<XStack gap={8}>
<YStack flex={1} height={4} backgroundColor="#00D4FF" borderRadius={2} />

Check warning on line 82 in app/src/components/NavBar/AadhaarNavBar.tsx

View workflow job for this annotation

GitHub Actions / build-deps

Replace `·flex={1}·height={4}·backgroundColor="#00D4FF"·borderRadius={2}` with `⏎············flex={1}⏎············height={4}⏎············backgroundColor="#00D4FF"⏎············borderRadius={2}⏎·········`
<YStack flex={1} height={4} backgroundColor={slate300} borderRadius={2} />

Check warning on line 83 in app/src/components/NavBar/AadhaarNavBar.tsx

View workflow job for this annotation

GitHub Actions / build-deps

Replace `·flex={1}·height={4}·backgroundColor={slate300}·borderRadius={2}` with `⏎············flex={1}⏎············height={4}⏎············backgroundColor={slate300}⏎············borderRadius={2}⏎·········`
<YStack flex={1} height={4} backgroundColor={slate300} borderRadius={2} />

Check warning on line 84 in app/src/components/NavBar/AadhaarNavBar.tsx

View workflow job for this annotation

GitHub Actions / build-deps

Replace `·flex={1}·height={4}·backgroundColor={slate300}·borderRadius={2}` with `⏎············flex={1}⏎············height={4}⏎············backgroundColor={slate300}⏎············borderRadius={2}⏎·········`
</XStack>
</YStack>
</YStack>
);
};
Binary file added app/src/images/512w.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions app/src/navigation/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.

import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';

Check failure on line 5 in app/src/navigation/home.ts

View workflow job for this annotation

GitHub Actions / build-deps

Run autofix to sort these imports!

import { HomeNavBar, IdDetailsNavBar } from '@/components/NavBar';
import { AadhaarNavBar } from '@/components/NavBar/AadhaarNavBar';
import DisclaimerScreen from '@/screens/home/DisclaimerScreen';
import HomeScreen from '@/screens/home/HomeScreen';
import IdDetailsScreen from '@/screens/home/IdDetailsScreen';
import ProofHistoryDetailScreen from '@/screens/home/ProofHistoryDetailScreen';
import ProofHistoryScreen from '@/screens/home/ProofHistoryScreen';
import AadhaarUploadScreen from '@/screens/document/aadhaar/AadhaarUploadScreen';

const homeScreens = {
Disclaimer: {
Expand Down Expand Up @@ -48,6 +50,14 @@
headerBackVisible: false, // Hide default back button
},
},
AadhaarUpload: {
screen: AadhaarUploadScreen,
options: {
title: 'AADHAAR REGISTRATION',
header: AadhaarNavBar,
headerBackVisible: false,
} as NativeStackNavigationOptions,
},
};

export default homeScreens;
1 change: 1 addition & 0 deletions app/src/screens/dev/DevSettingsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ function ParameterSection({

const items = [
'DevSettings',
'AadhaarUpload',
'DevFeatureFlags',
'DevHapticFeedback',
'DevPrivateKey',
Expand Down
Loading
Loading