diff --git a/.gitguardian.yml b/.gitguardian.yml
index 81a77acff..71514d3c5 100644
--- a/.gitguardian.yml
+++ b/.gitguardian.yml
@@ -1,12 +1,16 @@
-version: 2
+# GitGuardian configuration for ggshield
+# This file configures which files and secrets to ignore during scanning
-# Ignore specific file patterns (newer format)
-ignore:
+# Ignore specific file patterns
+paths-ignore:
# Mock certificates for testing (these are intentionally committed test data)
- "**/mock_certificates/**/*.key"
- "**/mock_certificates/**/*.crt"
- "**/mock_certificates/**/*.pem"
- "**/constants/mockCertificates.ts"
+ - "common/src/mock_certificates/**"
+ - "common/src/mock_certificates/aadhaar/mockAadhaarCert.ts"
+ - "common/src/utils/passports/genMockIdDoc.ts"
# Test data files
- "**/test/**/*.key"
@@ -24,45 +28,17 @@ ignore:
# Demo app test data
- "**/demo-app/**/mock/**"
- "**/demo-app/**/test-data/**"
-
-# Keep the old format for backward compatibility
-exclusion_globs:
- # Mock certificates for testing (these are intentionally committed test data)
- - "common/src/mock_certificates/**"
- - "common/src/constants/mockCertificates.ts"
- "**/test-data/**"
- "**/mock-data/**"
- # Test files with mock certificates
- - "**/test/**/*.key"
- - "**/test/**/*.crt"
- - "**/test/**/*.pem"
- - "**/tests/**/*.key"
- - "**/tests/**/*.crt"
- - "**/tests/**/*.pem"
-
- # Demo app test data
- - "**/demo-app/**/mock/**"
- - "**/demo-app/**/test-data/**"
-
# Generated test files
- "**/generated/**/*.key"
- "**/generated/**/*.crt"
- "**/generated/**/*.pem"
# Ignore specific secret types for mock files
-ignore_secrets:
+secrets-ignore:
- "Generic Private Key" # For mock certificate keys
- "Generic Certificate" # For mock certificates
- "RSA Private Key" # For mock RSA keys
- "EC Private Key" # For mock EC keys
-
-# Advanced: Ignore based on file content patterns
-ignore_patterns:
- # Ignore files that contain "mock" in the path and have key/cert content
- - pattern: "mock.*\\.(key|crt|pem)$"
- reason: "Mock certificate files for testing"
-
- # Ignore TypeScript files that export mock data
- - pattern: ".*mock.*\\.ts$"
- reason: "Mock data export files for testing"
diff --git a/.gitleaks.toml b/.gitleaks.toml
index 3f929c79a..45864ca3d 100644
--- a/.gitleaks.toml
+++ b/.gitleaks.toml
@@ -22,7 +22,9 @@ paths = [
'''pnpm-lock.yaml''',
'''Podfile.lock''',
'''common/src/mock_certificates/.*''',
+ '''common/dist/.*/mock_certificates/.*''',
'''common/src/constants/mockCertificates.ts''',
+ '''common/src/utils/passports/genMockIdDoc.ts''',
'''Database.refactorlog''',
'''vendor''',
'''.*tamagui-components\.config\.cjs$''',
diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle
index 38a55284a..4ad9c6090 100644
--- a/app/android/app/build.gradle
+++ b/app/android/app/build.gradle
@@ -217,5 +217,8 @@ dependencies {
implementation "com.google.guava:guava:31.1-android"
implementation "androidx.profileinstaller:profileinstaller:1.3.1"
+ implementation "androidx.activity:activity:1.9.3"
+ implementation "androidx.activity:activity-ktx:1.9.3"
+
implementation "com.google.android.play:app-update:2.1.0"
}
diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml
index 9e269c724..ec75692a7 100644
--- a/app/android/app/src/main/AndroidManifest.xml
+++ b/app/android/app/src/main/AndroidManifest.xml
@@ -82,5 +82,20 @@
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/notification_color"
tools:replace="android:resource" />
+
+
+
+
+
+
+
+
+
diff --git a/app/android/app/src/main/java/com/proofofpassportapp/PhotoPickerActivity.java b/app/android/app/src/main/java/com/proofofpassportapp/PhotoPickerActivity.java
new file mode 100644
index 000000000..a184d089e
--- /dev/null
+++ b/app/android/app/src/main/java/com/proofofpassportapp/PhotoPickerActivity.java
@@ -0,0 +1,77 @@
+// 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.
+
+package com.proofofpassportapp;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.PickVisualMediaRequest;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.appcompat.app.AppCompatActivity;
+
+public class PhotoPickerActivity extends AppCompatActivity {
+
+ public static final String EXTRA_SELECTED_URI = "selected_uri";
+ public static final String EXTRA_ERROR_MESSAGE = "error_message";
+ private static final String TAG = "PhotoPickerActivity";
+
+ private ActivityResultLauncher photoPickerLauncher;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Register the photo picker launcher using the recommended API
+ photoPickerLauncher = registerForActivityResult(
+ new ActivityResultContracts.PickVisualMedia(),
+ this::handlePhotoPickerResult
+ );
+
+ launchPhotoPicker();
+ }
+
+ private void launchPhotoPicker() {
+ try {
+ Log.d(TAG, "Launching modern PickVisualMedia photo picker");
+
+ // Create the request using the recommended builder pattern
+ PickVisualMediaRequest request = new PickVisualMediaRequest.Builder()
+ .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE)
+ .build();
+
+ photoPickerLauncher.launch(request);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to launch photo picker: " + e.getMessage());
+ finishWithError("Failed to launch photo picker: " + e.getMessage());
+ }
+ }
+
+ private void handlePhotoPickerResult(Uri selectedUri) {
+ if (selectedUri != null) {
+ Log.d(TAG, "Photo picker returned URI: " + selectedUri);
+ finishWithResult(selectedUri);
+ } else {
+ Log.d(TAG, "Photo picker was cancelled");
+ finishWithError("Photo selection was cancelled");
+ }
+ }
+
+ private void finishWithResult(Uri selectedUri) {
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(EXTRA_SELECTED_URI, selectedUri.toString());
+ setResult(RESULT_OK, resultIntent);
+ finish();
+ }
+
+ private void finishWithError(String errorMessage) {
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(EXTRA_ERROR_MESSAGE, errorMessage);
+ setResult(RESULT_CANCELED, resultIntent);
+ finish();
+ }
+}
diff --git a/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerModule.java b/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerModule.java
index 9f5bc9fbc..920c36f50 100644
--- a/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerModule.java
+++ b/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerModule.java
@@ -1,10 +1,16 @@
-// 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
+// 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.
package com.proofofpassportapp;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
@@ -14,14 +20,21 @@
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.LifecycleEventListener;
import com.blikoon.qrcodescanner.QrCodeActivity;
import android.Manifest;
+import com.proofofpassportapp.utils.QrCodeDetectorProcessor;
+import example.jllarraz.com.passportreader.mlkit.FrameMetadata;
+import java.io.InputStream;
-public class QRCodeScannerModule extends ReactContextBaseJavaModule {
+public class QRCodeScannerModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
private static final int REQUEST_CODE_QR_SCAN = 101;
+ private static final int REQUEST_CODE_PHOTO_PICK = 102;
+ private static final int REQUEST_CODE_MODERN_PHOTO_PICK = 103;
private static final int PERMISSION_REQUEST_CAMERA = 1;
private Promise scanPromise;
+ private Promise photoLibraryPromise;
private final ActivityEventListener activityEventListener = new BaseActivityEventListener() {
@Override
@@ -36,13 +49,38 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode,
}
scanPromise = null;
}
+ } else if (requestCode == REQUEST_CODE_PHOTO_PICK && photoLibraryPromise != null) {
+ // Handle legacy photo picker result for older devices
+ if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
+ processImageForQRCode(data.getData());
+ } else {
+ photoLibraryPromise.reject("PHOTO_PICKER_CANCELLED", "Photo selection was cancelled");
+ photoLibraryPromise = null;
+ }
+ } else if (requestCode == REQUEST_CODE_MODERN_PHOTO_PICK && photoLibraryPromise != null) {
+ // Handle modern photo picker result from dedicated activity
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ String uriString = data.getStringExtra(PhotoPickerActivity.EXTRA_SELECTED_URI);
+ if (uriString != null) {
+ processImageForQRCode(Uri.parse(uriString));
+ } else {
+ photoLibraryPromise.reject("PHOTO_PICKER_ERROR", "No URI returned from photo picker");
+ photoLibraryPromise = null;
+ }
+ } else {
+ String errorMessage = data != null ? data.getStringExtra(PhotoPickerActivity.EXTRA_ERROR_MESSAGE) : "Photo selection was cancelled";
+ photoLibraryPromise.reject("PHOTO_PICKER_CANCELLED", errorMessage);
+ photoLibraryPromise = null;
+ }
}
}
};
- QRCodeScannerModule(ReactApplicationContext reactContext) {
+ public QRCodeScannerModule(ReactApplicationContext reactContext) {
super(reactContext);
reactContext.addActivityEventListener(activityEventListener);
+ reactContext.addLifecycleEventListener(this);
+
}
@NonNull
@@ -71,12 +109,111 @@ public void scanQRCode(Promise promise) {
}
}
+ @ReactMethod
+ public void scanQRCodeFromPhotoLibrary(Promise promise) {
+ Activity currentActivity = getCurrentActivity();
+ if (currentActivity == null) {
+ promise.reject("ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist");
+ return;
+ }
+
+ photoLibraryPromise = promise;
+
+ // we first try with the recomended approach. This should be sufficient for most devices with play service.
+ // It fallsback to document picker if photo-picker is not available.
+ try {
+ android.util.Log.d("QRCodeScanner", "Using recommended PickVisualMedia photo picker via dedicated activity");
+ Intent intent = new Intent(currentActivity, PhotoPickerActivity.class);
+ currentActivity.startActivityForResult(intent, REQUEST_CODE_MODERN_PHOTO_PICK);
+ return;
+ } catch (Exception e) {
+ android.util.Log.d("QRCodeScanner", "Modern photo picker activity failed: " + e.getMessage());
+ }
+
+ // Fallback to intent-based photo picker for Android 13+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ try {
+ android.util.Log.d("QRCodeScanner", "Using intent-based modern photo picker (Android 13+)");
+ Intent intent = new Intent("android.provider.action.PICK_IMAGES");
+ intent.setType("image/*");
+ currentActivity.startActivityForResult(intent, REQUEST_CODE_PHOTO_PICK);
+ return;
+ } catch (Exception e) {
+ android.util.Log.d("QRCodeScanner", "Intent-based modern photo picker failed: " + e.getMessage());
+ }
+ }
+
+ // Final fallback to legacy photo picker
+ android.util.Log.d("QRCodeScanner", "Using legacy Intent.ACTION_PICK photo picker");
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setType("image/*");
+ currentActivity.startActivityForResult(intent, REQUEST_CODE_PHOTO_PICK);
+ }
+
private void startQRScanner(Activity activity) {
Intent intent = new Intent(activity, QrCodeActivity.class);
activity.startActivityForResult(intent, REQUEST_CODE_QR_SCAN);
}
- // Add this method to handle permission result
+ private void processImageForQRCode(Uri imageUri) {
+ try {
+ Activity currentActivity = getCurrentActivity();
+ if (currentActivity == null) {
+ if (photoLibraryPromise != null) {
+ photoLibraryPromise.reject("ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist");
+ photoLibraryPromise = null;
+ }
+ return;
+ }
+
+ InputStream inputStream = currentActivity.getContentResolver().openInputStream(imageUri);
+ Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+ inputStream.close();
+
+ if (bitmap == null) {
+ if (photoLibraryPromise != null) {
+ photoLibraryPromise.reject("IMAGE_LOAD_FAILED", "Failed to load selected image");
+ photoLibraryPromise = null;
+ }
+ return;
+ }
+
+ // use the exising qrcode processor we already have.
+ QrCodeDetectorProcessor processor = new QrCodeDetectorProcessor();
+ processor.detectQrCodeInBitmap(bitmap, new QrCodeDetectorProcessor.Listener() {
+ @Override
+ public void onSuccess(String results, FrameMetadata frameMetadata, long timeRequired, Bitmap bitmap) {
+ if (photoLibraryPromise != null) {
+ photoLibraryPromise.resolve(results);
+ photoLibraryPromise = null;
+ }
+ }
+
+ @Override
+ public void onFailure(Exception e, long timeRequired) {
+ if (photoLibraryPromise != null) {
+ photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image: " + e.getMessage());
+ photoLibraryPromise = null;
+ }
+ }
+
+ @Override
+ public void onCompletedFrame(long timeRequired) {
+ if (photoLibraryPromise != null) {
+ photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image");
+ photoLibraryPromise = null;
+ }
+ }
+ });
+
+ } catch (Exception e) {
+ if (photoLibraryPromise != null) {
+ photoLibraryPromise.reject("IMAGE_PROCESSING_ERROR", "Error processing image: " + e.getMessage());
+ photoLibraryPromise = null;
+ }
+ }
+ }
+
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CAMERA) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@@ -92,4 +229,19 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in
}
}
}
+
+ // Lifecycle methods
+ @Override
+ public void onHostResume() {
+ }
+
+ @Override
+ public void onHostPause() {
+ }
+
+ @Override
+ public void onHostDestroy() {
+ getReactApplicationContext().removeActivityEventListener(activityEventListener);
+ getReactApplicationContext().removeLifecycleEventListener(this);
+ }
}
diff --git a/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerPackage.java b/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerPackage.java
index 98c9bea53..c028e0ce1 100644
--- a/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerPackage.java
+++ b/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerPackage.java
@@ -21,6 +21,8 @@ public List createViewManagers(ReactApplicationContext reactContext
@Override
public List createNativeModules(ReactApplicationContext reactContext) {
- return Collections.emptyList();
+ return List.of(
+ new QRCodeScannerModule(reactContext)
+ );
}
}
diff --git a/app/android/app/src/main/java/com/proofofpassportapp/utils/QrCodeDetectorProcessor.kt b/app/android/app/src/main/java/com/proofofpassportapp/utils/QrCodeDetectorProcessor.kt
index ea066510d..f0a5e68e3 100644
--- a/app/android/app/src/main/java/com/proofofpassportapp/utils/QrCodeDetectorProcessor.kt
+++ b/app/android/app/src/main/java/com/proofofpassportapp/utils/QrCodeDetectorProcessor.kt
@@ -121,27 +121,95 @@ class QrCodeDetectorProcessor {
private fun detectInImage(bitmap: Bitmap): Result? {
val qRCodeDetectorReader = QRCodeReader()
+
+ // Try with original image first
+ var result = tryDetectInBitmap(bitmap, qRCodeDetectorReader)
+ if (result != null) return result
+
+ // If original fails, try with scaled up image (better for small QR codes)
+ val scaledBitmap = Bitmap.createScaledBitmap(bitmap, bitmap.width * 2, bitmap.height * 2, true)
+ result = tryDetectInBitmap(scaledBitmap, qRCodeDetectorReader)
+ if (result != null) return result
+
+ // If still fails, try with scaled down image (better for very large QR codes)
+ val scaledDownBitmap = Bitmap.createScaledBitmap(bitmap, bitmap.width / 2, bitmap.height / 2, true)
+ result = tryDetectInBitmap(scaledDownBitmap, qRCodeDetectorReader)
+ if (result != null) return result
+
+ return null
+ }
+
+ private fun tryDetectInBitmap(bitmap: Bitmap, qRCodeDetectorReader: QRCodeReader): Result? {
+ println("Attempting QR detection on bitmap: ${bitmap.width}x${bitmap.height}, hasAlpha: ${bitmap.hasAlpha()}")
+
val intArray = IntArray(bitmap.width * bitmap.height)
bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
val source: LuminanceSource =
RGBLuminanceSource(bitmap.width, bitmap.height, intArray)
- val binaryBitMap = BinaryBitmap(HybridBinarizer(source))
+ // Try multiple binarization strategies for better detection
+ val binarizers = listOf(
+ HybridBinarizer(source),
+ com.google.zxing.common.GlobalHistogramBinarizer(source)
+ )
+
+ for (binarizer in binarizers) {
+ val binaryBitMap = BinaryBitmap(binarizer)
- try {
- return qRCodeDetectorReader.decode(binaryBitMap)
+ try {
+ val result = qRCodeDetectorReader.decode(binaryBitMap)
+ println("QR Code detected successfully with ${binarizer.javaClass.simpleName}")
+ return result
+ } catch (e: Exception) {
+ println("Detection failed with ${binarizer.javaClass.simpleName}: ${e.message}")
+ }
}
- catch (e: Exception) {
- // noop
- println(e)
+
+ // Try with different hints for better detection
+ val hints = mapOf(
+ com.google.zxing.DecodeHintType.TRY_HARDER to true,
+ com.google.zxing.DecodeHintType.POSSIBLE_FORMATS to listOf(com.google.zxing.BarcodeFormat.QR_CODE)
+ )
+
+ for (binarizer in binarizers) {
+ val binaryBitMap = BinaryBitmap(binarizer)
+
+ try {
+ val result = qRCodeDetectorReader.decode(binaryBitMap, hints)
+ println("QR Code detected successfully with hints and ${binarizer.javaClass.simpleName}")
+ return result
+ } catch (e: Exception) {
+ println("Detection with hints failed with ${binarizer.javaClass.simpleName}: ${e.message}")
+ }
}
+
+ println("All QR code detection attempts failed for bitmap ${bitmap.width}x${bitmap.height}")
return null
}
fun stop() {
}
+ fun detectQrCodeInBitmap(
+ image: Bitmap,
+ listener: Listener
+ ): Boolean {
+ val start = System.currentTimeMillis()
+ executor.execute {
+ val result = detectInImage(image)
+ val timeRequired = System.currentTimeMillis() - start
+ println(result)
+ if (result != null) {
+ listener.onSuccess(result.text!!, null, timeRequired, null)
+ }
+ else {
+ listener.onCompletedFrame(timeRequired)
+ }
+ }
+ return true
+ }
+
interface Listener {
fun onSuccess(results: String, frameMetadata: FrameMetadata?, timeRequired: Long, bitmap: Bitmap?)
diff --git a/app/android/app/src/main/res/values/styles.xml b/app/android/app/src/main/res/values/styles.xml
index 50f41297a..1e07c584b 100644
--- a/app/android/app/src/main/res/values/styles.xml
+++ b/app/android/app/src/main/res/values/styles.xml
@@ -6,4 +6,14 @@
- #000000
-
\ No newline at end of file
+
+
+
+
diff --git a/app/ios/PhotoLibraryQRScannerViewController.swift b/app/ios/PhotoLibraryQRScannerViewController.swift
new file mode 100644
index 000000000..89c26b6cc
--- /dev/null
+++ b/app/ios/PhotoLibraryQRScannerViewController.swift
@@ -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()
+ }
+
+ 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 presentImagePicker() {
+ let imagePicker = UIImagePickerController()
+ imagePicker.delegate = self
+ imagePicker.sourceType = .photoLibrary
+ imagePicker.mediaTypes = ["public.image"]
+ present(imagePicker, animated: true, completion: nil)
+ }
+
+ 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)
+ }
+ }
+ }
+
+ 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)
+ }
+ }
+}
+
diff --git a/app/ios/QRScannerBridge.m b/app/ios/QRScannerBridge.m
index 9410cd2c7..34c943cab 100644
--- a/app/ios/QRScannerBridge.m
+++ b/app/ios/QRScannerBridge.m
@@ -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
diff --git a/app/ios/QRScannerBridge.swift b/app/ios/QRScannerBridge.swift
index 4bcf0ad63..1508fe144 100644
--- a/app/ios/QRScannerBridge.swift
+++ b/app/ios/QRScannerBridge.swift
@@ -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)
+ }
+ }
}
diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj
index d894e12ef..78a4b0457 100644
--- a/app/ios/Self.xcodeproj/project.pbxproj
+++ b/app/ios/Self.xcodeproj/project.pbxproj
@@ -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 */; };
@@ -57,6 +58,7 @@
165E76BC2B8DC4A00000FA90 /* MRZScannerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MRZScannerModule.swift; sourceTree = ""; };
165E76BE2B8DC53A0000FA90 /* MRZScannerModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MRZScannerModule.m; sourceTree = ""; };
165E76C22B8DC8370000FA90 /* ScannerHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannerHostingController.swift; sourceTree = ""; };
+ 1668A53E2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryQRScannerViewController.swift; sourceTree = ""; };
1686F0DB2C500F3800841CDE /* QRScannerBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerBridge.swift; sourceTree = ""; };
1686F0DD2C500F4F00841CDE /* QRScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerViewController.swift; sourceTree = ""; };
1686F0DF2C500FBD00841CDE /* QRScannerBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QRScannerBridge.m; sourceTree = ""; };
@@ -102,6 +104,7 @@
13B07FAE1A68108700A75B9A /* OpenPassport */ = {
isa = PBXGroup;
children = (
+ 1668A53E2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift */,
BF6F0D542E38ED81008EA85C /* SelfAnalytics.swift */,
BFBA0C782E33A01F00E82A52 /* NativeLoggerBridge.m */,
BFBA0C762E339D2B00E82A52 /* NativeLoggerBridge.swift */,
@@ -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 */,
@@ -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";
@@ -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;
diff --git a/app/src/components/NavBar/AadhaarNavBar.tsx b/app/src/components/NavBar/AadhaarNavBar.tsx
new file mode 100644
index 000000000..39ea21f60
--- /dev/null
+++ b/app/src/components/NavBar/AadhaarNavBar.tsx
@@ -0,0 +1,114 @@
+// 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, 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, slate300 } from '@/utils/colors';
+import { extraYPadding } from '@/utils/constants';
+import { dinot } from '@/utils/fonts';
+import { buttonTap } from '@/utils/haptic';
+
+export const AadhaarNavBar = (props: NativeStackHeaderProps) => {
+ const insets = useSafeAreaInsets();
+
+ const currentRouteName = props.route.name;
+ const isFirstStep = currentRouteName === 'AadhaarUpload';
+ const isSecondStep =
+ currentRouteName === 'AadhaarUploadSuccess' ||
+ currentRouteName === 'AadhaarUploadError';
+
+ 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 (
+
+
+
+
+
+ }
+ />
+
+
+ AADHAAR REGISTRATION
+
+
+
+
+
+ }
+ />
+
+
+ {/* Progress Bar - dynamic based on current step */}
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/src/components/homeScreen/idCard.tsx b/app/src/components/homeScreen/idCard.tsx
index 64fa39f36..a997124e1 100644
--- a/app/src/components/homeScreen/idCard.tsx
+++ b/app/src/components/homeScreen/idCard.tsx
@@ -6,13 +6,19 @@ import type { FC } from 'react';
import { Dimensions } from 'react-native';
import { Separator, Text, XStack, YStack } from 'tamagui';
+import {
+ AadhaarData,
+ isAadhaarDocument,
+ isMRZDocument,
+ PassportData,
+} from '@selfxyz/common';
import {
attributeToPosition,
attributeToPosition_ID,
} from '@selfxyz/common/constants';
-import { PassportData } from '@selfxyz/common/types';
import { SvgXml } from '@/components/homeScreen/SvgXmlWrapper';
+import AadhaarIcon from '@/images/icons/aadhaar.svg';
import EPassport from '@/images/icons/epassport.svg';
import LogoGray from '@/images/logo_gray.svg';
import {
@@ -33,7 +39,7 @@ const logoSvg = `