-
Notifications
You must be signed in to change notification settings - Fork 179
SELF-725: add iOS qrcode opener and aadhaar screen #1038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
ef2f6a7
c2f5ed4
1d3049f
6705d8c
fe9ef1a
999937a
81aa409
6f1c97d
e36f3db
5ab135f
7e6b5fc
3933f59
f1f65c6
b6bf07b
3040442
da64feb
f9b0e68
abbf13e
a33580e
fdcb6d9
708bad9
3c0c2dd
ab61711
ea89c5f
3474761
63f4076
9d60335
01d630a
5d264bf
c79adaa
1743737
12c3866
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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)? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var errorHandler: ((Error) -> Void)? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| override func viewDidLoad() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super.viewDidLoad() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| checkPhotoLibraryPermissionAndPresentPicker() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+32
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use PHPicker (no permission prompt) instead of legacy permission flow.
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 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Remove UIImagePicker presenter (replaced by PHPicker). The 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,8 @@ | |
| import Foundation | ||
| import SwiftQRScanner | ||
| import React | ||
| import UIKit | ||
| import CoreImage | ||
|
|
||
| @objc(QRScannerBridge) | ||
| class QRScannerBridge: NSObject { | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Present from the topmost VC and return structured cancellation codes. Using 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 // 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 |
||
| } | ||
| 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'; | ||
| 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
|
||
| 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
|
||
| <XStack gap={8}> | ||
| <YStack flex={1} height={4} backgroundColor="#00D4FF" borderRadius={2} /> | ||
|
Check warning on line 82 in app/src/components/NavBar/AadhaarNavBar.tsx
|
||
| <YStack flex={1} height={4} backgroundColor={slate300} borderRadius={2} /> | ||
|
Check warning on line 83 in app/src/components/NavBar/AadhaarNavBar.tsx
|
||
| <YStack flex={1} height={4} backgroundColor={slate300} borderRadius={2} /> | ||
|
Check warning on line 84 in app/src/components/NavBar/AadhaarNavBar.tsx
|
||
| </XStack> | ||
| </YStack> | ||
| </YStack> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
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:📝 Committable suggestion