SELF-725: add iOS qrcode opener and aadhaar screen (#1038)

* add iOS qrcode opener and aadhaar screen

* format

* fix test

* add Image-picker android (#1077)

* add image-picker android

* fix validation

* feat: implement Aadhaar upload success and error screens, enhance AadhaarNavBar with dynamic progress indication

- Added AadhaarUploadedSuccessScreen and AadhaarUploadErrorScreen components for handling upload outcomes.
- Updated AadhaarNavBar to reflect current upload step with dynamic progress bar.
- Integrated new screens into navigation flow for Aadhaar upload process.
- Introduced blue check and warning SVG icons for visual feedback on success and error states.

* feat: generate mock aadhar (#1083)

* feat: generate mock aadhar

* add yarn.lock

* update yarn.lock

* update protocolStore, update types, start modifying provingMachine

* Register mock aadhar (#1093)

* Register mock aadhar

* fix ofac

* temp: generate name

* fix dob

* Add Aadhaar support to ID card component and screens

- Integrated Aadhaar icon and conditional rendering in IdCardLayout.
- Updated AadhaarUploadScreen to process QR codes and store Aadhaar data.
- Modified navigation and button text in AadhaarUploadedSuccessScreen.
- Added mock data generation for Aadhaar in the mobile SDK.
- Updated ManageDocumentsScreen to include Aadhaar document type.
- Enhanced error handling and validation for Aadhaar QR code processing.
- Added utility functions for Aadhaar data extraction and commitment processing.

* aadhaar disclose - wip (#1094)

* fix: timestamp cal of extractQRDataFields

* Feat/aadhar fixes (#1099)

* Fix - android aadhar qr scanner

* fixes

* update text

* yarn nice

* run prettier

* Add mock Aadhaar certificates for development

- Introduced hardcoded Aadhaar test certificates for development purposes.
- Moved Aadhaar mock private and public keys to a dedicated file for better organization.
- Updated the mock ID document generation utility to utilize the new Aadhaar mock certificates.

* prettier write

* add 'add-aadhaar' button (#1100)

* Update .gitleaks.toml to include path for mock certificates in the common/dist directory

* yarn nice

* Enhance Aadhaar error handling with specific error types

- Updated the AadhaarUploadErrorScreen to display different messages based on the error type (general or expired).
- Modified the AadhaarUploadScreen to pass the appropriate error type when navigating to the error screen.
- Set initial parameters for the home screen to include a default error type.

* Update passport handling in proving machine to support Aadhaar document category

- Modified the handling of country code in the useProvingStore to return 'IND' for Aadhaar documents.
- Ensured that the country code is only fetched from passport metadata for non-Aadhaar documents.

* tweak layout, text, change email to support, hide help button

* fix ci, remove aadhaar logging, add aadhaar events

* remove unused aadhaar tracking events

* update globs

* fix gitguardian config

* don't track id

---------

Co-authored-by: Justin Hernandez <justin.hernandez@self.xyz>
Co-authored-by: Seshanth.S🐺 <35675963+seshanthS@users.noreply.github.com>
Co-authored-by: vishal <vishalkoolkarni0045@gmail.com>
This commit is contained in:
turnoffthiscomputer
2025-09-20 02:36:01 +02:00
committed by GitHub
parent 664be08e12
commit 2df4dc4619
62 changed files with 3134 additions and 952 deletions

View File

@@ -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"

View File

@@ -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$''',

View File

@@ -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"
}

View File

@@ -82,5 +82,20 @@
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/notification_color"
tools:replace="android:resource" />
<activity
android:name=".PhotoPickerActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:exported="false" />
<service android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
</intent-filter>
<meta-data android:name="photopicker_activity:0:required" android:value="" />
</service>
</application>
</manifest>

View File

@@ -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<PickVisualMediaRequest> 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();
}
}

View File

@@ -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.Promise;
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 class QRCodeScannerModule extends ReactContextBaseJavaModule {
}
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 class QRCodeScannerModule extends ReactContextBaseJavaModule {
}
}
@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 class QRCodeScannerModule extends ReactContextBaseJavaModule {
}
}
}
// Lifecycle methods
@Override
public void onHostResume() {
}
@Override
public void onHostPause() {
}
@Override
public void onHostDestroy() {
getReactApplicationContext().removeActivityEventListener(activityEventListener);
getReactApplicationContext().removeLifecycleEventListener(this);
}
}

View File

@@ -21,6 +21,8 @@ public class QRCodeScannerPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
return List.of(
new QRCodeScannerModule(reactContext)
);
}
}

View File

@@ -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)
)
try {
return qRCodeDetectorReader.decode(binaryBitMap)
for (binarizer in binarizers) {
val binaryBitMap = BinaryBitmap(binarizer)
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?)

View File

@@ -6,4 +6,14 @@
<item name="android:windowBackground">#000000</item>
</style>
</resources>
<!-- Translucent theme for PhotoPickerActivity -->
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">false</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>

View File

@@ -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)
}
}
}

View File

@@ -14,4 +14,6 @@
RCT_EXTERN_METHOD(scanQRCode:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(scanQRCodeFromPhotoLibrary:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
@end

View File

@@ -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)
}
}
}

View File

@@ -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 = "<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>"; };
@@ -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;

View File

@@ -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 (
<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"
fontFamily={dinot}
>
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} opacity={0} />
</Button>
}
/>
</NavBar.Container>
{/* Progress Bar - dynamic based on current step */}
<YStack
paddingHorizontal={20}
paddingBottom={15}
backgroundColor={slate100}
>
<XStack gap={8}>
<YStack
flex={1}
height={4}
backgroundColor={isFirstStep ? '#00D4FF' : slate300}
borderRadius={2}
/>
<YStack
flex={1}
height={4}
backgroundColor={isSecondStep ? '#00D4FF' : slate300}
borderRadius={2}
/>
</XStack>
</YStack>
</YStack>
);
};

View File

@@ -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 = `<svg width="47" height="46" viewBox="0 0 47 46" fill="none" xml
</svg>`;
interface IdCardLayoutAttributes {
idDocument: PassportData;
idDocument: PassportData | AadhaarData | null;
selected: boolean;
hidden: boolean;
}
@@ -49,6 +55,11 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
selected,
hidden,
}) => {
// Early return if document is null
if (!idDocument) {
return null;
}
// Function to mask MRZ characters except '<' and spaces
const maskMrzValue = (text: string): string => {
return text.replace(/./g, 'X');
@@ -107,10 +118,17 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
{/* Header Section */}
<XStack>
<XStack alignItems="center">
<EPassport
width={fontSize.large * 3}
height={fontSize.large * 3 * 0.617}
/>
{idDocument.documentCategory === 'aadhaar' ? (
<AadhaarIcon
width={fontSize.large * 3}
height={fontSize.large * 3 * 0.617}
/>
) : (
<EPassport
width={fontSize.large * 3}
height={fontSize.large * 3 * 0.617}
/>
)}
<YStack marginLeft={imageSize.width - fontSize.large * 3}>
<Text
fontWeight="bold"
@@ -120,7 +138,9 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
>
{idDocument.documentCategory === 'passport'
? 'Passport'
: 'ID Card'}
: idDocument.documentCategory === 'aadhaar'
? 'Aadhaar'
: 'ID Card'}
</Text>
<Text
fontSize={fontSize.small}
@@ -130,7 +150,9 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
Verified{' '}
{idDocument.documentCategory === 'passport'
? 'Biometric Passport'
: ' Biometric ID Card'}
: idDocument.documentCategory === 'aadhaar'
? 'Aadhaar Document'
: 'Biometric ID Card'}
</Text>
</YStack>
</XStack>
@@ -203,12 +225,16 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
value={
idDocument.documentCategory === 'passport'
? 'PASSPORT'
: 'ID CARD'
: idDocument.documentCategory === 'aadhaar'
? 'AADHAAR'
: 'ID CARD'
}
maskValue={
idDocument.documentCategory === 'passport'
? 'PASSPORT'
: 'ID CARD'
: idDocument.documentCategory === 'aadhaar'
? 'AADHAAR'
: 'ID CARD'
}
hidden={hidden}
/>
@@ -224,68 +250,81 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
<YStack flex={1}>
<IdAttribute
name="DOC NO."
value={
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).passNoSlice
}
value={getDocumentAttributes(idDocument).passNoSlice}
maskValue="XX-XXXXXXX"
hidden={hidden}
/>
</YStack>
</XStack>
<XStack flex={1} gap={padding * 0.3}>
<YStack flex={1}>
<IdAttribute
name="SURNAME"
value={getNameAndSurname(
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).nameSlice,
).surname.join(' ')}
maskValue="XXXXXXXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="NAME"
value={getNameAndSurname(
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).nameSlice,
).names.join(' ')}
maskValue="XXXXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="SEX"
value={
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).sexSlice
}
maskValue="X"
hidden={hidden}
/>
</YStack>
{idDocument.documentCategory === 'aadhaar' ? (
// Aadhaar: Combined name field spanning two columns
<>
<YStack flex={2}>
<IdAttribute
name="NAME"
value={(() => {
const nameData = getNameAndSurname(
getDocumentAttributes(idDocument).nameSlice,
);
const fullName = [
...nameData.surname,
...nameData.names,
].join(' ');
return fullName;
})()}
maskValue="XXXXXXXXXXXXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="SEX"
value={getDocumentAttributes(idDocument).sexSlice}
maskValue="X"
hidden={hidden}
/>
</YStack>
</>
) : (
// Other documents: Separate surname and name fields
<>
<YStack flex={1}>
<IdAttribute
name="SURNAME"
value={getNameAndSurname(
getDocumentAttributes(idDocument).nameSlice,
).surname.join(' ')}
maskValue="XXXXXXXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="NAME"
value={getNameAndSurname(
getDocumentAttributes(idDocument).nameSlice,
).names.join(' ')}
maskValue="XXXXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="SEX"
value={getDocumentAttributes(idDocument).sexSlice}
maskValue="X"
hidden={hidden}
/>
</YStack>
</>
)}
</XStack>
<XStack flex={1} gap={padding * 0.3}>
<YStack flex={1}>
<IdAttribute
name="NATIONALITY"
value={
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).nationalitySlice
}
value={getDocumentAttributes(idDocument).nationalitySlice}
maskValue="XXX"
hidden={hidden}
/>
@@ -294,10 +333,7 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
<IdAttribute
name="DOB"
value={formatDateFromYYMMDD(
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).dobSlice,
getDocumentAttributes(idDocument).dobSlice,
)}
maskValue="XX/XX/XXXX"
hidden={hidden}
@@ -307,10 +343,7 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
<IdAttribute
name="EXPIRY DATE"
value={formatDateFromYYMMDD(
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).expiryDateSlice,
getDocumentAttributes(idDocument).expiryDateSlice,
true,
)}
maskValue="XX/XX/XXXX"
@@ -322,12 +355,7 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
<YStack flex={1}>
<IdAttribute
name="AUTHORITY"
value={
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).issuingStateSlice
}
value={getDocumentAttributes(idDocument).issuingStateSlice}
maskValue="XXX"
hidden={hidden}
/>
@@ -339,8 +367,8 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
</XStack>
)}
{/* Footer Section - MRZ */}
{selected && (
{/* Footer Section - MRZ or QR Data */}
{selected && isMRZDocument(idDocument) && idDocument.mrz && (
<XStack
alignItems="center"
backgroundColor={slate100}
@@ -416,6 +444,35 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
</YStack>
</XStack>
)}
{/* Footer Section - Empty placeholder for Aadhaar (no MRZ) */}
{selected && isAadhaarDocument(idDocument) && (
<XStack
alignItems="center"
backgroundColor={slate100}
borderRadius={borderRadius / 3}
paddingHorizontal={padding / 2}
paddingVertical={padding / 4}
minHeight={fontSize.xsmall * 2.5} // Maintain consistent height
>
{/* Fixed-width spacer to align content with the attributes block */}
<XStack width={contentLeftOffset} alignItems="center">
<LogoGray width={fontSize.large} height={fontSize.large} />
</XStack>
<YStack marginLeft={-padding / 2} justifyContent="center">
<Text
fontSize={fontSize.xsmall}
letterSpacing={fontSize.xsmall * 0.1}
fontFamily={plexMono}
color={slate400}
opacity={0.5}
>
{/* Empty placeholder - no MRZ for Aadhaar */}
</Text>
</YStack>
</XStack>
)}
</YStack>
</YStack>
);
@@ -466,6 +523,65 @@ const IdAttribute: FC<IdAttributeProps> = ({
export default IdCardLayout;
// Helper functions to safely extract document data
function getDocumentAttributes(document: PassportData | AadhaarData) {
if (isAadhaarDocument(document)) {
return getAadhaarAttributes(document);
} else if (isMRZDocument(document)) {
return getPassportAttributes(document.mrz, document.documentCategory);
} else {
// Fallback for unknown document types
return {
nameSlice: '',
dobSlice: '',
yobSlice: '',
issuingStateSlice: '',
nationalitySlice: '',
passNoSlice: '',
sexSlice: '',
expiryDateSlice: '',
isPassportType: false,
};
}
}
function getAadhaarAttributes(document: AadhaarData) {
const extractedFields = document.extractedFields;
// For Aadhaar, we format the name to work with the existing getNameAndSurname function
// We'll put the full name in the "surname" position and leave names empty
const fullName = extractedFields?.name || '';
const nameSliceFormatted = fullName ? `${fullName}<<` : ''; // Format like MRZ
// Format DOB to YYMMDD for consistency with passport format
let dobFormatted = '';
if (extractedFields?.dob && extractedFields?.mob && extractedFields?.yob) {
const year =
extractedFields.yob.length === 4
? extractedFields.yob.slice(-2)
: extractedFields.yob;
const month = extractedFields.mob.padStart(2, '0');
const day = extractedFields.dob.padStart(2, '0');
dobFormatted = `${year}${month}${day}`;
}
return {
nameSlice: nameSliceFormatted,
dobSlice: dobFormatted,
yobSlice: extractedFields?.yob || '',
issuingStateSlice: extractedFields?.state || '',
nationalitySlice: 'IND', // Aadhaar is always Indian
passNoSlice: extractedFields?.aadhaarLast4Digits || '',
sexSlice:
extractedFields?.gender === 'M'
? 'M'
: extractedFields?.gender === 'F'
? 'F'
: extractedFields?.gender || '',
expiryDateSlice: '', // Aadhaar doesn't expire
isPassportType: false,
};
}
function getPassportAttributes(mrz: string, documentCategory: string) {
const isPassportType = documentCategory === 'passport';
const attributePositions = isPassportType

View File

@@ -12,7 +12,7 @@ export const useMockDataForm = () => {
'sha256 rsa 65537 2048',
);
const [selectedDocumentType, setSelectedDocumentType] = useState<
'mock_passport' | 'mock_id_card'
'mock_passport' | 'mock_id_card' | 'mock_aadhaar'
>('mock_passport');
const [isInOfacList, setIsInOfacList] = useState(true);
@@ -34,7 +34,7 @@ export const useMockDataForm = () => {
};
const handleDocumentTypeSelect = (
documentType: 'mock_passport' | 'mock_id_card',
documentType: 'mock_passport' | 'mock_id_card' | 'mock_aadhaar',
) => {
setSelectedDocumentType(documentType);
};

BIN
app/src/images/512w.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@@ -0,0 +1,3 @@
<svg width="139" height="140" viewBox="0 0 139 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.36816 84.2275C2.72168 79.5413 0.378581 74.7956 0.338867 69.9902C0.299154 65.1849 2.60254 60.459 7.24902 55.8125L55.3818 7.73926C59.9886 3.09277 64.6947 0.789388 69.5 0.829102C74.3451 0.868815 79.1107 3.21191 83.7969 7.8584L131.632 55.6934C136.278 60.3796 138.621 65.1452 138.661 69.9902C138.701 74.7956 136.397 79.5215 131.751 84.168L83.6777 132.241C79.0312 136.888 74.3053 139.191 69.5 139.151C64.6947 139.112 59.9489 136.769 55.2627 132.122L7.36816 84.2275ZM62.9473 99.5371C64.1387 99.5371 65.2308 99.2591 66.2236 98.7031C67.2562 98.1074 68.1299 97.2933 68.8447 96.2607L96.6641 53.251C97.1009 52.5758 97.4583 51.861 97.7363 51.1064C98.054 50.3519 98.2129 49.5973 98.2129 48.8428C98.2129 47.1748 97.5775 45.8245 96.3066 44.792C95.0755 43.7594 93.6657 43.2432 92.0771 43.2432C89.9723 43.2432 88.2051 44.375 86.7754 46.6387L62.7686 85.002L51.748 71.1816C50.9538 70.1888 50.1595 69.4938 49.3652 69.0967C48.571 68.6598 47.6774 68.4414 46.6846 68.4414C45.0166 68.4414 43.6068 69.0371 42.4551 70.2285C41.3034 71.3802 40.7275 72.79 40.7275 74.458C40.7275 75.2523 40.8665 76.0267 41.1445 76.7812C41.4622 77.4961 41.8991 78.2308 42.4551 78.9854L56.8115 96.2607C57.6852 97.3727 58.6185 98.2067 59.6113 98.7627C60.6042 99.279 61.7161 99.5371 62.9473 99.5371Z" fill="#2563EB"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -0,0 +1,3 @@
<svg width="127" height="116" viewBox="0 0 127 116" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5117 115.273C13.9772 115.273 10.8994 114.479 8.27832 112.891C5.69694 111.342 3.69141 109.237 2.26172 106.576C0.832031 103.955 0.117188 101.056 0.117188 97.8789C0.117188 94.821 0.911458 91.902 2.5 89.1221L48.5479 8.82129C50.1761 5.9222 52.3206 3.73796 54.9814 2.26855C57.682 0.799154 60.5215 0.0644531 63.5 0.0644531C66.4388 0.0644531 69.2386 0.799154 71.8994 2.26855C74.5602 3.73796 76.7246 5.9222 78.3926 8.82129L124.44 89.1221C125.235 90.512 125.83 91.9616 126.228 93.4707C126.625 94.9401 126.823 96.4095 126.823 97.8789C126.823 101.056 126.108 103.955 124.679 106.576C123.249 109.237 121.224 111.342 118.603 112.891C116.021 114.479 112.963 115.273 109.429 115.273H17.5117ZM63.5596 73.6338C67.3721 73.6338 69.3577 71.6481 69.5166 67.6768L70.4697 39.5C70.5492 37.554 69.9137 35.9456 68.5635 34.6748C67.2529 33.3643 65.5651 32.709 63.5 32.709C61.3952 32.709 59.6875 33.3444 58.377 34.6152C57.0664 35.8861 56.4508 37.5143 56.5303 39.5L57.4238 67.6768C57.5827 71.6481 59.6279 73.6338 63.5596 73.6338ZM63.5596 94.6025C65.7041 94.6025 67.5309 93.9473 69.04 92.6367C70.5492 91.2865 71.3037 89.5589 71.3037 87.4541C71.3037 85.389 70.5492 83.6813 69.04 82.3311C67.5309 80.9808 65.7041 80.3057 63.5596 80.3057C61.3753 80.3057 59.5286 80.9808 58.0195 82.3311C56.5104 83.6813 55.7559 85.389 55.7559 87.4541C55.7559 89.5589 56.5104 91.2865 58.0195 92.6367C59.5684 93.9473 61.415 94.6025 63.5596 94.6025Z" fill="#F59E0B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -5,6 +5,10 @@
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import { HomeNavBar, IdDetailsNavBar } from '@/components/NavBar';
import { AadhaarNavBar } from '@/components/NavBar/AadhaarNavBar';
import AadhaarUploadedSuccessScreen from '@/screens/document/aadhaar/AadhaarUploadedSuccessScreen';
import AadhaarUploadErrorScreen from '@/screens/document/aadhaar/AadhaarUploadErrorScreen';
import AadhaarUploadScreen from '@/screens/document/aadhaar/AadhaarUploadScreen';
import DisclaimerScreen from '@/screens/home/DisclaimerScreen';
import HomeScreen from '@/screens/home/HomeScreen';
import IdDetailsScreen from '@/screens/home/IdDetailsScreen';
@@ -48,6 +52,33 @@ const homeScreens = {
headerBackVisible: false, // Hide default back button
},
},
AadhaarUpload: {
screen: AadhaarUploadScreen,
options: {
title: 'AADHAAR REGISTRATION',
header: AadhaarNavBar,
headerBackVisible: false,
} as NativeStackNavigationOptions,
},
AadhaarUploadSuccess: {
screen: AadhaarUploadedSuccessScreen,
options: {
title: 'AADHAAR REGISTRATION',
header: AadhaarNavBar,
headerBackVisible: false,
} as NativeStackNavigationOptions,
},
AadhaarUploadError: {
screen: AadhaarUploadErrorScreen,
options: {
title: 'AADHAAR REGISTRATION',
header: AadhaarNavBar,
headerBackVisible: false,
} as NativeStackNavigationOptions,
initialParams: {
errorType: 'general',
},
},
};
export default homeScreens;

View File

@@ -44,6 +44,7 @@ import type { PropsWithChildren } from 'react';
import React, { createContext, useCallback, useContext, useMemo } from 'react';
import Keychain from 'react-native-keychain';
import { isMRZDocument } from '@selfxyz/common';
import type {
PublicKeyDetailsECDSA,
PublicKeyDetailsRSA,
@@ -55,8 +56,10 @@ import {
parseCertificateSimple,
} from '@selfxyz/common/utils';
import type {
AadhaarData,
DocumentCatalog,
DocumentMetadata,
IDDocument,
PassportData,
} from '@selfxyz/common/utils/types';
import type { DocumentsAdapter, SelfClient } from '@selfxyz/mobile-sdk-alpha';
@@ -387,10 +390,10 @@ export async function initializeNativeModules(
// TODO: is this used?
async function loadAllPassportData(selfClient: SelfClient): Promise<{
[service: string]: PassportData;
[service: string]: IDDocument;
}> {
const allDocs = await getAllDocuments(selfClient);
const result: { [service: string]: PassportData } = {};
const result: { [service: string]: IDDocument } = {};
// Convert to legacy format for backward compatibility
Object.values(allDocs).forEach(({ data, metadata }) => {
@@ -601,7 +604,7 @@ interface IPassportContext {
data: PassportData;
} | null>;
// TODO: is this even used?
getAllData: () => Promise<{ [service: string]: PassportData }>;
getAllData: () => Promise<{ [service: string]: IDDocument }>;
getAvailableTypes: () => Promise<string[]>;
setData: (data: PassportData) => Promise<void>;
getPassportDataAndSecret: () => Promise<{
@@ -616,7 +619,7 @@ interface IPassportContext {
loadDocumentCatalog: () => Promise<DocumentCatalog>;
getAllDocuments: () => Promise<{
[documentId: string]: { data: PassportData; metadata: DocumentMetadata };
[documentId: string]: { data: IDDocument; metadata: DocumentMetadata };
}>;
setSelectedDocument: (documentId: string) => Promise<void>;
@@ -742,7 +745,7 @@ export async function setSelectedDocument(documentId: string): Promise<void> {
async function storeDocumentDirectlyToKeychain(
contentHash: string,
passportData: PassportData,
passportData: PassportData | AadhaarData,
): Promise<void> {
await Keychain.setGenericPassword(contentHash, JSON.stringify(passportData), {
service: `document-${contentHash}`,
@@ -750,7 +753,7 @@ async function storeDocumentDirectlyToKeychain(
}
export async function storeDocumentWithDeduplication(
passportData: PassportData,
passportData: PassportData | AadhaarData,
): Promise<string> {
const contentHash = calculateContentHash(passportData);
const catalog = await loadDocumentCatalogDirectlyFromKeychain();
@@ -780,8 +783,12 @@ export async function storeDocumentWithDeduplication(
documentType: passportData.documentType,
documentCategory:
passportData.documentCategory ||
inferDocumentCategory(passportData.documentType),
data: passportData.mrz || '', // Store MRZ for passports/IDs, relevant data for aadhaar
inferDocumentCategory(
(passportData as PassportData | AadhaarData).documentType,
),
data: isMRZDocument(passportData)
? (passportData as PassportData).mrz
: (passportData as AadhaarData).qrData || '', // Store MRZ for passports/IDs, relevant data for aadhaar
mock: passportData.mock || false,
isRegistered: false,
};
@@ -793,7 +800,9 @@ export async function storeDocumentWithDeduplication(
return contentHash;
}
export async function storePassportData(passportData: PassportData) {
export async function storePassportData(
passportData: PassportData | AadhaarData,
) {
await storeDocumentWithDeduplication(passportData);
}

View File

@@ -57,6 +57,7 @@ import { buttonTap, selectionChange } from '@/utils/haptic';
const documentTypes = {
mock_passport: 'Passport',
mock_id_card: 'ID Card',
mock_aadhaar: 'Aadhaar',
};
const MockDocumentTitleCard = () => {
@@ -181,6 +182,10 @@ const CreateMockScreen: React.FC = () => {
const handleGenerate = useCallback(async () => {
setIsGenerating(true);
// Allow React to update the UI state
await new Promise(resolve => setTimeout(resolve, 0));
try {
const parsedMockData = await generateMockDocument({
age,
@@ -237,27 +242,29 @@ const CreateMockScreen: React.FC = () => {
borderColor={slate200}
backgroundColor={slate100}
>
<FormSection title="Encryption Preference">
<Button
onPress={() => {
buttonTap();
setAlgorithmSheetOpen(true);
}}
paddingVertical="$5"
paddingHorizontal="$3"
backgroundColor="white"
borderColor={slate200}
borderWidth={1}
borderRadius={5}
>
<XStack justifyContent="space-between" width="100%">
<Text fontSize="$4" fontFamily={plexMono} color={black}>
{selectedAlgorithm}
</Text>
<ChevronDown size={20} color={slate500} />
</XStack>
</Button>
</FormSection>
{selectedDocumentType !== 'mock_aadhaar' && (
<FormSection title="Encryption Preference">
<Button
onPress={() => {
buttonTap();
setAlgorithmSheetOpen(true);
}}
paddingVertical="$5"
paddingHorizontal="$3"
backgroundColor="white"
borderColor={slate200}
borderWidth={1}
borderRadius={5}
>
<XStack justifyContent="space-between" width="100%">
<Text fontSize="$4" fontFamily={plexMono} color={black}>
{selectedAlgorithm}
</Text>
<ChevronDown size={20} color={slate500} />
</XStack>
</Button>
</FormSection>
)}
<FormSection title="Document Type">
<Button
@@ -290,35 +297,41 @@ const CreateMockScreen: React.FC = () => {
</Button>
</FormSection>
<FormSection title="Nationality">
<Button
onPress={() => {
buttonTap();
setCountrySheetOpen(true);
trackEvent(MockDataEvents.OPEN_COUNTRY_SELECTION);
}}
paddingVertical="$5"
paddingHorizontal="$3"
backgroundColor="white"
borderColor={slate200}
borderWidth={1}
borderRadius={5}
>
<XStack justifyContent="space-between" width="100%">
<Text
fontSize="$4"
fontFamily={plexMono}
color={black}
textTransform="uppercase"
>
{flag(getCountryISO2(selectedCountry))}
{' '}
{countryCodes[selectedCountry as keyof typeof countryCodes]}
</Text>
<ChevronDown size={20} color={slate500} />
</XStack>
</Button>
</FormSection>
{selectedDocumentType !== 'mock_aadhaar' && (
<FormSection title="Nationality">
<Button
onPress={() => {
buttonTap();
setCountrySheetOpen(true);
trackEvent(MockDataEvents.OPEN_COUNTRY_SELECTION);
}}
paddingVertical="$5"
paddingHorizontal="$3"
backgroundColor="white"
borderColor={slate200}
borderWidth={1}
borderRadius={5}
>
<XStack justifyContent="space-between" width="100%">
<Text
fontSize="$4"
fontFamily={plexMono}
color={black}
textTransform="uppercase"
>
{flag(getCountryISO2(selectedCountry))}
{' '}
{
countryCodes[
selectedCountry as keyof typeof countryCodes
]
}
</Text>
<ChevronDown size={20} color={slate500} />
</XStack>
</Button>
</FormSection>
)}
<FormSection title="Age">
<XStack
@@ -338,7 +351,7 @@ const CreateMockScreen: React.FC = () => {
setAge(age - 1);
trackEvent(MockDataEvents.DECREASE_AGE);
}}
disabled={expiryYears <= 0}
disabled={age <= 1}
>
<Minus color={slate500} />
</Button>
@@ -370,55 +383,57 @@ const CreateMockScreen: React.FC = () => {
</XStack>
</FormSection>
<FormSection title="Document Expires In">
<XStack
alignItems="center"
gap="$2"
justifyContent="space-between"
>
<Button
height="$3.5"
width="$6"
backgroundColor="white"
justifyContent="center"
borderColor={slate200}
borderWidth={1}
onPress={() => {
buttonTap();
setExpiryYears(expiryYears - 1);
trackEvent(MockDataEvents.DECREASE_EXPIRY_YEARS);
}}
disabled={expiryYears <= 0}
{selectedDocumentType !== 'mock_aadhaar' && (
<FormSection title="Document Expires In">
<XStack
alignItems="center"
gap="$2"
justifyContent="space-between"
>
<Minus color={slate500} />
</Button>
<Text
textTransform="uppercase"
textAlign="center"
color={textBlack}
fontWeight="500"
fontSize="$4"
fontFamily={plexMono}
>
{expiryYears} years
</Text>
<Button
height="$3.5"
width="$6"
backgroundColor="white"
justifyContent="center"
borderColor={slate200}
borderWidth={1}
onPress={() => {
buttonTap();
setExpiryYears(expiryYears + 1);
trackEvent(MockDataEvents.INCREASE_EXPIRY_YEARS);
}}
>
<Plus color={slate500} />
</Button>
</XStack>
</FormSection>
<Button
height="$3.5"
width="$6"
backgroundColor="white"
justifyContent="center"
borderColor={slate200}
borderWidth={1}
onPress={() => {
buttonTap();
setExpiryYears(expiryYears - 1);
trackEvent(MockDataEvents.DECREASE_EXPIRY_YEARS);
}}
disabled={age <= 0}
>
<Minus color={slate500} />
</Button>
<Text
textTransform="uppercase"
textAlign="center"
color={textBlack}
fontWeight="500"
fontSize="$4"
fontFamily={plexMono}
>
{expiryYears} years
</Text>
<Button
height="$3.5"
width="$6"
backgroundColor="white"
justifyContent="center"
borderColor={slate200}
borderWidth={1}
onPress={() => {
buttonTap();
setExpiryYears(expiryYears + 1);
trackEvent(MockDataEvents.INCREASE_EXPIRY_YEARS);
}}
>
<Plus color={slate500} />
</Button>
</XStack>
</FormSection>
)}
<FormSection title="In OFAC sanction list" endSection={true}>
<YStack flexDirection="column" gap="$2">
@@ -559,7 +574,10 @@ const CreateMockScreen: React.FC = () => {
onPress={() => {
buttonTap();
handleDocumentTypeSelect(
docType as 'mock_passport' | 'mock_id_card',
docType as
| 'mock_passport'
| 'mock_id_card'
| 'mock_aadhaar',
);
setDocumentTypeSheetOpen(false);
trackEvent(MockDataEvents.SELECT_DOCUMENT_TYPE);

View File

@@ -124,6 +124,7 @@ function ParameterSection({
const items = [
'DevSettings',
'AadhaarUpload',
'DevFeatureFlags',
'DevHapticFeedback',
'DevPrivateKey',

View File

@@ -0,0 +1,124 @@
// 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 { XStack, YStack } from 'tamagui';
import type { RouteProp } from '@react-navigation/native';
import { useNavigation, useRoute } from '@react-navigation/native';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { PrimaryButton } from '@/components/buttons/PrimaryButton';
import { SecondaryButton } from '@/components/buttons/SecondaryButton';
import { BodyText } from '@/components/typography/BodyText';
import WarningIcon from '@/images/warning.svg';
import { useSafeAreaInsets } from '@/mocks/react-native-safe-area-context';
import { black, slate100, slate200, slate500, white } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
type AadhaarUploadErrorRouteParams = {
errorType?: 'general' | 'expired';
};
type AadhaarUploadErrorRoute = RouteProp<
Record<string, AadhaarUploadErrorRouteParams>,
string
>;
const AadhaarUploadErrorScreen: React.FC = () => {
const { bottom } = useSafeAreaInsets();
const navigation = useNavigation();
const route = useRoute<AadhaarUploadErrorRoute>();
const { trackEvent } = useSelfClient();
const errorType = route.params?.errorType || 'general';
// Define error messages based on error type
const getErrorMessages = () => {
if (errorType === 'expired') {
return {
title: 'QR Code Has Expired',
description:
'You uploaded a valid Aadhaar QR code, but unfortunately it has expired. Please generate a new QR code from the mAadhaar app and try again.',
};
}
return {
title: 'There was a problem reading the code',
description:
'Please ensure the QR code is clear and well-lit, then try again. For best results, take a screenshot of the QR code instead of photographing it.',
};
};
const { title, description } = getErrorMessages();
return (
<YStack flex={1} backgroundColor={slate100}>
<YStack flex={1} paddingHorizontal={20} paddingTop={20}>
<YStack
flex={1}
justifyContent="center"
alignItems="center"
paddingVertical={20}
>
<WarningIcon width={120} height={120} />
</YStack>
</YStack>
<YStack
paddingHorizontal={20}
paddingTop={20}
alignItems="center"
paddingVertical={25}
borderBlockWidth={1}
borderBlockColor={slate200}
>
<BodyText fontSize={19} textAlign="center" color={black}>
{title}
</BodyText>
<BodyText
marginTop={6}
fontSize={17}
textAlign="center"
color={slate500}
>
{description}
</BodyText>
</YStack>
<YStack
paddingHorizontal={25}
backgroundColor={white}
paddingBottom={bottom + extraYPadding + 35}
paddingTop={25}
>
<XStack gap="$3" alignItems="stretch">
<YStack flex={1}>
<PrimaryButton
onPress={() => {
trackEvent(AadhaarEvents.RETRY_BUTTON_PRESSED, { errorType });
// Navigate back to upload screen to try again
navigation.goBack();
}}
>
Try Again
</PrimaryButton>
</YStack>
<YStack flex={1}>
<SecondaryButton
onPress={() => {
trackEvent(AadhaarEvents.HELP_BUTTON_PRESSED, { errorType });
// TODO: Implement help functionality
}}
>
Need Help?
</SecondaryButton>
</YStack>
</XStack>
</YStack>
</YStack>
);
};
export default AadhaarUploadErrorScreen;

View File

@@ -0,0 +1,330 @@
// 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, { useCallback, useEffect, useState } from 'react';
import { Linking } from 'react-native';
import { Image, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import {
extractQRDataFields,
getAadharRegistrationWindow,
} from '@selfxyz/common/utils';
import type { AadhaarData } from '@selfxyz/common/utils/types';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { PrimaryButton } from '@/components/buttons/PrimaryButton';
import { BodyText } from '@/components/typography/BodyText';
import { useModal } from '@/hooks/useModal';
import AadhaarImage from '@/images/512w.png';
import { useSafeAreaInsets } from '@/mocks/react-native-safe-area-context';
import type { RootStackParamList } from '@/navigation';
import { storePassportData } from '@/providers/passportDataProvider';
import { slate100, slate200, slate400, slate500, white } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
import {
isQRScannerPhotoLibraryAvailable,
scanQRCodeFromPhotoLibrary,
} from '@/utils/qrScanner';
const AadhaarUploadScreen: React.FC = () => {
const { bottom } = useSafeAreaInsets();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { trackEvent } = useSelfClient();
const [isProcessing, setIsProcessing] = useState(false);
const { showModal: showPermissionModal } = useModal({
titleText: 'Photo Library Access Required',
bodyText:
'To upload QR codes from your photo library, please enable photo library access in your device settings.',
buttonText: 'Open Settings',
secondaryButtonText: 'Cancel',
onButtonPress: () => {
trackEvent(AadhaarEvents.PERMISSION_SETTINGS_OPENED);
Linking.openSettings();
},
onModalDismiss: () => {
trackEvent(AadhaarEvents.PERMISSION_MODAL_DISMISSED);
},
});
// Track screen entry
useEffect(() => {
trackEvent(AadhaarEvents.UPLOAD_SCREEN_OPENED);
// Track button state based on photo library availability
if (isQRScannerPhotoLibraryAvailable()) {
trackEvent(AadhaarEvents.UPLOAD_BUTTON_ENABLED);
} else {
trackEvent(AadhaarEvents.UPLOAD_BUTTON_DISABLED);
trackEvent(AadhaarEvents.PHOTO_LIBRARY_UNAVAILABLE);
}
}, [trackEvent]);
const validateAAdhaarTimestamp = useCallback(
async (timestamp: string) => {
//timestamp is in YYYY-MM-DD HH:MM format
trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_STARTED);
const currentTimestamp = new Date().getTime();
const timestampDate = new Date(timestamp);
const timestampTimestamp = timestampDate.getTime();
const diff = currentTimestamp - timestampTimestamp;
const diffMinutes = diff / (1000 * 60);
const allowedWindow = await getAadharRegistrationWindow();
const isValid = diffMinutes <= allowedWindow;
if (isValid) {
trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_SUCCESS);
} else {
trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_FAILED);
}
return isValid;
},
[trackEvent],
);
const processAadhaarQRCode = useCallback(
async (qrCodeData: string) => {
try {
if (
!qrCodeData ||
typeof qrCodeData !== 'string' ||
qrCodeData.length < 100
) {
trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT);
throw new Error('Invalid QR code format - too short or not a string');
}
if (!/^\d+$/.test(qrCodeData)) {
trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT);
throw new Error('Invalid QR code format - not a numeric string');
}
if (qrCodeData.length < 100) {
trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT);
throw new Error(
'QR code too short - likely not a valid Aadhaar QR code',
);
}
trackEvent(AadhaarEvents.QR_DATA_EXTRACTION_STARTED);
let extractedFields;
try {
extractedFields = extractQRDataFields(qrCodeData);
trackEvent(AadhaarEvents.QR_DATA_EXTRACTION_SUCCESS);
} catch {
trackEvent(AadhaarEvents.QR_CODE_PARSE_FAILED);
throw new Error('Failed to parse Aadhaar QR code - invalid format');
}
if (
!extractedFields.name ||
!extractedFields.dob ||
!extractedFields.gender
) {
trackEvent(AadhaarEvents.QR_CODE_MISSING_FIELDS);
throw new Error('Invalid Aadhaar QR code - missing required fields');
}
if (!(await validateAAdhaarTimestamp(extractedFields.timestamp))) {
trackEvent(AadhaarEvents.QR_CODE_EXPIRED);
throw new Error('QRCODE_EXPIRED');
}
const aadhaarData: AadhaarData = {
documentType: 'aadhaar',
documentCategory: 'aadhaar',
mock: false,
qrData: qrCodeData,
extractedFields: extractedFields,
signature: [],
publicKey: '',
photoHash: '',
};
trackEvent(AadhaarEvents.DATA_STORAGE_STARTED);
await storePassportData(aadhaarData);
trackEvent(AadhaarEvents.DATA_STORAGE_SUCCESS);
trackEvent(AadhaarEvents.QR_UPLOAD_SUCCESS);
navigation.navigate('AadhaarUploadSuccess');
} catch (error) {
// Check if it's a QR code expiration error
const errorType: 'expired' | 'general' =
error instanceof Error && error.message === 'QRCODE_EXPIRED'
? 'expired'
: 'general';
trackEvent(AadhaarEvents.ERROR_SCREEN_NAVIGATED, { errorType });
(navigation.navigate as any)('AadhaarUploadError', { errorType });
}
},
[navigation, trackEvent, validateAAdhaarTimestamp],
);
const onPhotoLibraryPress = useCallback(async () => {
if (isProcessing) {
return;
}
try {
setIsProcessing(true);
trackEvent(AadhaarEvents.PROCESSING_STARTED);
const qrCodeData = await scanQRCodeFromPhotoLibrary();
await processAadhaarQRCode(qrCodeData);
} catch (error) {
trackEvent(AadhaarEvents.QR_UPLOAD_FAILED, {
error:
error instanceof Error
? error.message
: error?.toString() || 'Unknown error',
});
// Don't show error for user cancellation
if (error instanceof Error && error.message.includes('cancelled')) {
trackEvent(AadhaarEvents.USER_CANCELLED_SELECTION);
return;
}
// Handle permission errors specifically - check for exact message from native code
const errorMessage =
error instanceof Error ? error.message : String(error);
if (errorMessage.includes('Photo library access is required')) {
trackEvent(AadhaarEvents.PERMISSION_MODAL_OPENED);
showPermissionModal();
return;
}
// Also check for other permission-related error messages
if (
errorMessage.includes('permission') ||
errorMessage.includes('access') ||
errorMessage.includes('Settings') ||
errorMessage.includes('enable access')
) {
trackEvent(AadhaarEvents.PERMISSION_MODAL_OPENED);
showPermissionModal();
return;
}
// Handle QR code scanning/processing errors
if (
errorMessage.includes('No QR code found') ||
errorMessage.includes('QR code') ||
errorMessage.includes('Failed to process') ||
errorMessage.includes('Invalid')
) {
(navigation.navigate as any)('AadhaarUploadError', {
errorType: 'general' as const,
});
return;
}
// Handle any other errors by showing error screen
(navigation.navigate as any)('AadhaarUploadError', {
errorType: 'general' as const,
});
} finally {
setIsProcessing(false);
}
}, [
isProcessing,
trackEvent,
processAadhaarQRCode,
navigation,
showPermissionModal,
]);
return (
<YStack
flex={1}
backgroundColor={slate100}
paddingBottom={bottom + extraYPadding + 50}
>
<YStack flex={1} paddingHorizontal={20} paddingTop={20}>
<YStack
flex={1}
justifyContent="center"
alignItems="center"
paddingVertical={20}
>
<Image
source={AadhaarImage}
width="100%"
height="100%"
objectFit="contain"
/>
</YStack>
</YStack>
<YStack
paddingHorizontal={20}
paddingTop={20}
alignItems="center"
paddingVertical={25}
borderBlockWidth={1}
borderBlockColor={slate200}
>
<BodyText fontWeight="bold" fontSize={18} textAlign="center">
Generate a QR code from the mAadaar app
</BodyText>
<BodyText fontSize={16} textAlign="center" color={slate500}>
Save the QR code to your photo library and upload it here.
</BodyText>
<BodyText
fontSize={12}
textAlign="center"
color={slate400}
marginTop={20}
>
SELF DOES NOT STORE THIS INFORMATION.
</BodyText>
</YStack>
<YStack paddingHorizontal={25} backgroundColor={white} paddingTop={25}>
<XStack gap="$3" alignItems="stretch">
<YStack flex={1}>
<PrimaryButton
disabled={!isQRScannerPhotoLibraryAvailable() || isProcessing}
trackEvent={AadhaarEvents.QR_UPLOAD_REQUESTED}
onPress={onPhotoLibraryPress}
>
{isProcessing ? 'Processing...' : 'Upload QR code'}
</PrimaryButton>
</YStack>
{/* TODO: Implement camera-based QR scanning for Aadhaar */}
{/* <Button
aspectRatio={1}
backgroundColor={slate200}
borderRadius="$2"
justifyContent="center"
alignItems="center"
pressStyle={{
backgroundColor: slate50,
scale: 0.98,
}}
hoverStyle={{
backgroundColor: slate300,
}}
onPress={onCameraScanPress}
disabled={isProcessing}
>
<ScanIcon width={28} height={28} color={black} />
</Button> */}
</XStack>
</YStack>
</YStack>
);
};
export default AadhaarUploadScreen;

View File

@@ -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.
import React from 'react';
import { YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { PrimaryButton } from '@/components/buttons/PrimaryButton';
import { BodyText } from '@/components/typography/BodyText';
import BlueCheckIcon from '@/images/blue_check.svg';
import { useSafeAreaInsets } from '@/mocks/react-native-safe-area-context';
import { black, slate100, slate200, slate500, white } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
const AadhaarUploadedSuccessScreen: React.FC = () => {
const { bottom } = useSafeAreaInsets();
const navigation = useNavigation();
const { trackEvent } = useSelfClient();
return (
<YStack flex={1} backgroundColor={slate100}>
<YStack flex={1} paddingHorizontal={20} paddingTop={20}>
<YStack
flex={1}
justifyContent="center"
alignItems="center"
paddingVertical={20}
>
<BlueCheckIcon width={120} height={120} />
</YStack>
</YStack>
<YStack
paddingHorizontal={20}
paddingTop={20}
alignItems="center"
paddingVertical={25}
borderBlockWidth={1}
borderBlockColor={slate200}
>
<BodyText fontSize={19} textAlign="center" color={black}>
QR code upload successful
</BodyText>
<BodyText
marginTop={6}
fontSize={17}
textAlign="center"
color={slate500}
>
You are ready to register your Aadhaar card with Self.
</BodyText>
</YStack>
<YStack
paddingHorizontal={25}
backgroundColor={white}
paddingBottom={bottom + extraYPadding + 35}
paddingTop={25}
>
<PrimaryButton
onPress={() => {
trackEvent(AadhaarEvents.CONTINUE_TO_REGISTRATION_PRESSED);
navigation.navigate('ConfirmBelonging', {});
}}
>
Continue to Registration
</PrimaryButton>
</YStack>
</YStack>
);
};
export default AadhaarUploadedSuccessScreen;

View File

@@ -2,10 +2,10 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { Pressable } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button, ScrollView, styled, Text, YStack } from 'tamagui';
import { ScrollView, Text, YStack } from 'tamagui';
import {
useFocusEffect,
useNavigation,
@@ -13,48 +13,31 @@ import {
} from '@react-navigation/native';
import { PassportData } from '@selfxyz/common/types';
import { DocumentCatalog } from '@selfxyz/common/utils/types';
import { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types';
import { DocumentMetadata, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { DocumentEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { pressedStyle } from '@/components/buttons/pressedStyle';
import IdCardLayout from '@/components/homeScreen/idCard';
import { BodyText } from '@/components/typography/BodyText';
import { useAppUpdates } from '@/hooks/useAppUpdates';
import useConnectionModal from '@/hooks/useConnectionModal';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import WarnIcon from '@/images/icons/warning.svg';
import { usePassport } from '@/providers/passportDataProvider';
import { useSettingStore } from '@/stores/settingStore';
import useUserStore from '@/stores/userStore';
import { neutral700, slate50, slate800, white } from '@/utils/colors';
import { slate50 } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
const ScanButton = styled(Button, {
borderRadius: 20,
width: 90,
height: 90,
borderColor: neutral700,
borderWidth: 1,
backgroundColor: '#1D1D1D',
alignItems: 'center',
justifyContent: 'center',
});
const HomeScreen: React.FC = () => {
const selfClient = useSelfClient();
useConnectionModal();
const navigation = useNavigation();
const { setIdDetailsDocumentId } = useUserStore();
const { getAllDocuments, loadDocumentCatalog, setSelectedDocument } =
usePassport();
const { getAllDocuments, loadDocumentCatalog } = usePassport();
const [isNewVersionAvailable, showAppUpdateModal, isModalDismissed] =
useAppUpdates();
const [documentCatalog, setDocumentCatalog] = useState<DocumentCatalog>({
documents: [],
});
const [allDocuments, setAllDocuments] = useState<
Record<string, { data: PassportData; metadata: DocumentMetadata }>
Record<string, { data: IDDocument; metadata: DocumentMetadata }>
>({});
const [loading, setLoading] = useState(true);
@@ -89,22 +72,6 @@ const HomeScreen: React.FC = () => {
}
});
const handleDocumentSelection = async (documentId: string) => {
await setSelectedDocument(documentId);
// Reload catalog to update selected state
const updatedCatalog = await loadDocumentCatalog();
setDocumentCatalog(updatedCatalog);
};
const goToQRCodeViewFinder = useHapticNavigation('QRCodeViewFinder');
const onScanButtonPress = useCallback(() => {
selfClient.trackEvent(ProofEvents.QR_SCAN_REQUESTED, {
from: 'Home',
});
goToQRCodeViewFinder();
}, [goToQRCodeViewFinder, selfClient]);
// Prevents back navigation
usePreventRemove(true, () => {});
const { bottom } = useSafeAreaInsets();
@@ -153,6 +120,10 @@ const HomeScreen: React.FC = () => {
<Pressable
key={metadata.id}
onPress={() => {
selfClient.trackEvent(DocumentEvents.DOCUMENT_SELECTED, {
document_type: documentData.data.documentType,
document_category: documentData.data.documentCategory,
});
setIdDetailsDocumentId(metadata.id);
navigation.navigate('IdDetails');
}}
@@ -170,39 +141,4 @@ const HomeScreen: React.FC = () => {
);
};
const pressStyle = {
opacity: 1,
backgroundColor: 'transparent',
transform: [{ scale: 0.95 }],
} as const;
function PrivacyNote() {
const { hasPrivacyNoteBeenDismissed } = useSettingStore();
const onDisclaimerPress = useHapticNavigation('Disclaimer');
if (hasPrivacyNoteBeenDismissed) {
return null;
}
return (
<Card onPress={onDisclaimerPress} pressStyle={pressedStyle}>
<WarnIcon color={white} width={24} height={33} />
<BodyText color={white} textAlign="center" fontSize={18}>
A note on protecting your privacy
</BodyText>
</Card>
);
}
export default HomeScreen;
const Card = styled(YStack, {
width: '100%',
flexGrow: 0,
backgroundColor: slate800,
borderRadius: 8,
gap: 12,
alignItems: 'center',
padding: 20,
});

View File

@@ -9,7 +9,7 @@ import { BlurView } from '@react-native-community/blur';
import { useNavigation } from '@react-navigation/native';
import { PassportData } from '@selfxyz/common/types';
import { DocumentCatalog } from '@selfxyz/common/utils/types';
import { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types';
import IdCardLayout from '@/components/homeScreen/idCard';
import { usePassport } from '@/providers/passportDataProvider';
@@ -29,7 +29,7 @@ const IdDetailsScreen: React.FC = () => {
const documentId = idDetailsDocumentId;
const { getAllDocuments, loadDocumentCatalog, setSelectedDocument } =
usePassport();
const [document, setDocument] = useState<PassportData | null>(null);
const [document, setDocument] = useState<IDDocument | null>(null);
const [documentCatalog, setDocumentCatalog] = useState<DocumentCatalog>({
documents: [],
});

View File

@@ -8,7 +8,7 @@ import { ActivityIndicator, View } from 'react-native';
import type { StaticScreenProps } from '@react-navigation/native';
import { usePreventRemove } from '@react-navigation/native';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
PassportEvents,
ProofEvents,
@@ -46,7 +46,22 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
const isReadyToProve = currentState === 'ready_to_prove';
useEffect(() => {
notificationSuccess();
init(selfClient, 'dsc');
const initializeProving = async () => {
try {
const selectedDocument = await loadSelectedDocument(selfClient);
if (selectedDocument?.data?.documentCategory === 'aadhaar') {
init(selfClient, 'register');
} else {
init(selfClient, 'dsc');
}
} catch (error) {
console.error('Error loading selected document:', error);
init(selfClient, 'dsc');
}
};
initializeProving();
}, [init, selfClient]);
const onOkPress = async () => {
@@ -109,10 +124,10 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
>
<Title textAlign="center">Confirm your identity</Title>
<Description textAlign="center" paddingBottom={20}>
By continuing, you certify that this passport belongs to you and is
not stolen or forged. Once registered with Self, this document will
be permanently linked to your identity and can't be linked to
another one.
By continuing, you certify that this passport, biometric ID or
Aadhaar card belongs to you and is not stolen or forged. Once
registered with Self, this document will be permanently linked to
your identity and can't be linked to another one.
</Description>
<PrimaryButton
trackEvent={PassportEvents.OWNERSHIP_CONFIRMED}

View File

@@ -17,7 +17,6 @@ import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores';
import qrScanAnimation from '@/assets/animations/qr_scan.json';
import { SecondaryButton } from '@/components/buttons/SecondaryButton';
import type { QRCodeScannerViewProps } from '@/components/native/QRCodeScanner';
import { QRCodeScannerView } from '@/components/native/QRCodeScanner';
import Additional from '@/components/typography/Additional';
@@ -153,13 +152,6 @@ const QRCodeViewFinderScreen: React.FC = () => {
</View>
</XStack>
</YStack>
<SecondaryButton
trackEvent={ProofEvents.QR_SCAN_CANCELLED}
onPress={onCancelPress}
>
Cancel
</SecondaryButton>
</YStack>
</ExpandableBottomLayout.BottomSection>
</ExpandableBottomLayout.Layout>

View File

@@ -66,6 +66,9 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
return useProtocolStore.getState()[docCategory].commitment_tree;
},
getAltCSCA(docCategory) {
if (passportData.documentCategory === 'aadhaar') {
return useProtocolStore.getState().aadhaar.public_keys;
}
return useProtocolStore.getState()[docCategory].alternative_csca;
},
},

View File

@@ -45,7 +45,7 @@ const AccountVerifiedSuccessScreen: React.FC = ({}) => {
>
<Title size="large">ID Verified</Title>
<Description>
Your passport information is now protected by Self ID. Just scan a
Your document's information is now protected by Self ID. Just scan a
participating partner's QR code to prove your identity.
</Description>
</YStack>

View File

@@ -71,6 +71,9 @@ const RecoverWithPhraseScreen: React.FC = () => {
return useProtocolStore.getState()[docCategory].commitment_tree;
},
getAltCSCA(docCategory) {
if (docCategory === 'aadhaar') {
return useProtocolStore.getState()[docCategory].public_keys;
}
return useProtocolStore.getState()[docCategory].alternative_csca;
},
},

View File

@@ -114,6 +114,10 @@ const PassportDataSelector = () => {
return 'ID Card';
case 'mock_id_card':
return 'Mock ID Card';
case 'aadhaar':
return 'Aadhaar';
case 'mock_aadhaar':
return 'Mock Aadhaar';
default:
return documentType;
}
@@ -288,6 +292,12 @@ const ManageDocumentsScreen: React.FC = () => {
navigation.navigate('CreateMock');
};
const handleAddAadhaar = () => {
impactLight();
trackEvent(DocumentEvents.ADD_NEW_AADHAAR_SELECTED);
navigation.navigate('AadhaarUpload');
};
return (
<YStack
flex={1}
@@ -315,6 +325,9 @@ const ManageDocumentsScreen: React.FC = () => {
<PrimaryButton onPress={handleScanDocument}>
Scan New ID Document
</PrimaryButton>
<PrimaryButton onPress={handleAddAadhaar}>
Add Aadhaar
</PrimaryButton>
<SecondaryButton onPress={handleGenerateMock}>
Generate Mock Document
</SecondaryButton>

View File

@@ -24,6 +24,7 @@ import { advercase, dinot } from '@/utils/fonts';
const LaunchScreen: React.FC = () => {
useConnectionModal();
const onStartPress = useHapticNavigation('DocumentOnboarding');
const onAadhaarPress = useHapticNavigation('AadhaarUpload');
const createMock = useHapticNavigation('CreateMock');
const { bottom } = useSafeAreaInsets();
@@ -37,7 +38,6 @@ const LaunchScreen: React.FC = () => {
<YStack
backgroundColor={black}
flex={1}
justifyContent="space-between"
alignItems="center"
paddingHorizontal={20}
paddingBottom={bottom + extraYPadding}
@@ -53,13 +53,20 @@ const LaunchScreen: React.FC = () => {
<Text style={styles.title}>Get started</Text>
<BodyText style={styles.description}>
Register with Self using your passport or biometric ID to prove your
identity across the web without revealing your personal information.
Register with Self using your passport, biometric ID or Aadhaar card
to prove your identity across the web without revealing your
personal information.
</BodyText>
</View>
</View>
<YStack gap="$3" width="100%" alignItems="center" marginBottom={20}>
<YStack
gap="$3"
width="100%"
alignItems="center"
marginBottom={20}
marginTop={24}
>
<YStack gap="$3" width="100%">
<AbstractButton
bgColor={black}
@@ -79,7 +86,7 @@ const LaunchScreen: React.FC = () => {
</AbstractButton>
<AbstractButton
trackEvent={AppEvents.GET_STARTED}
trackEvent={AppEvents.GET_STARTED_BIOMETRIC}
onPress={onStartPress}
bgColor={white}
color={black}
@@ -87,6 +94,15 @@ const LaunchScreen: React.FC = () => {
>
I have a Passport or Biometric ID
</AbstractButton>
<AbstractButton
trackEvent={AppEvents.GET_STARTED_AADHAAR}
onPress={onAadhaarPress}
bgColor={white}
color={black}
testID="launch-get-started-button"
>
I have an Aadhaar Card
</AbstractButton>
</YStack>
<Caption style={styles.notice}>
@@ -109,15 +125,15 @@ export default LaunchScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
flex: 0,
justifyContent: 'flex-start',
alignItems: 'center',
width: '102%',
paddingTop: '25%',
paddingTop: '30%',
},
card: {
width: '100%',
marginTop: '30%',
marginTop: '20%',
borderRadius: 16,
paddingVertical: 40,
paddingHorizontal: 20,
@@ -128,7 +144,7 @@ const styles = StyleSheet.create({
shadowOpacity: 0.2,
shadowRadius: 12,
elevation: 8,
marginBottom: 20,
marginBottom: 8,
},
logoSection: {
width: 60,

View File

@@ -25,7 +25,7 @@ export const sendCountrySupportNotification = async ({
countryCode,
documentCategory,
subject = `Country Support Request: ${countryName}`,
recipient = 'team@self.xyz',
recipient = 'support@self.xyz',
}: SendCountrySupportNotificationOptions): Promise<void> => {
const deviceInfo = [
['device', `${Platform.OS}@${Platform.Version}`],
@@ -82,7 +82,7 @@ export const sendFeedbackEmail = async ({
message,
origin,
subject = 'SELF App Feedback',
recipient = 'team@self.xyz',
recipient = 'support@self.xyz',
}: SendFeedbackEmailOptions): Promise<void> => {
const deviceInfo = [
['device', `${Platform.OS}@${Platform.Version}`],

View File

@@ -41,6 +41,7 @@ import {
getPayload,
getWSDbRelayerUrl,
} from '@selfxyz/common/utils/proving';
import type { IDDocument } from '@selfxyz/common/utils/types';
import type { SelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
clearPassportData,
@@ -72,10 +73,20 @@ const getMappingKey = (
documentCategory: DocumentCategory,
): string => {
if (circuitType === 'disclose') {
return documentCategory === 'passport' ? 'DISCLOSE' : 'DISCLOSE_ID';
if (documentCategory === 'passport') return 'DISCLOSE';
if (documentCategory === 'id_card') return 'DISCLOSE_ID';
if (documentCategory === 'aadhaar') return 'DISCLOSE_AADHAAR';
throw new Error(
`Unsupported document category for disclose: ${documentCategory}`,
);
}
if (circuitType === 'register') {
return documentCategory === 'passport' ? 'REGISTER' : 'REGISTER_ID';
if (documentCategory === 'passport') return 'REGISTER';
if (documentCategory === 'id_card') return 'REGISTER_ID';
if (documentCategory === 'aadhaar') return 'REGISTER_AADHAAR';
throw new Error(
`Unsupported document category for register: ${documentCategory}`,
);
}
// circuitType === 'dsc'
return documentCategory === 'passport' ? 'DSC' : 'DSC_ID';
@@ -95,10 +106,10 @@ const resolveWebSocketUrl = (
};
// Helper functions for _generatePayload refactoring
const _generateCircuitInputs = (
const _generateCircuitInputs = async (
circuitType: 'disclose' | 'register' | 'dsc',
secret: string | undefined | null,
passportData: PassportData,
passportData: IDDocument,
env: 'prod' | 'stg',
) => {
const document: DocumentCategory = passportData.documentCategory;
@@ -114,17 +125,19 @@ const _generateCircuitInputs = (
switch (circuitType) {
case 'register':
({ inputs, circuitName, endpointType, endpoint } =
generateTEEInputsRegister(
await generateTEEInputsRegister(
secret as string,
passportData,
protocolStore[document].dsc_tree,
document === 'aadhaar'
? protocolStore[document].public_keys
: protocolStore[document].dsc_tree,
env,
));
circuitTypeWithDocumentExtension = `${circuitType}${document === 'passport' ? '' : '_id'}`;
break;
case 'dsc':
({ inputs, circuitName, endpointType, endpoint } = generateTEEInputsDSC(
passportData,
passportData as PassportData,
protocolStore[document].csca_tree as string[][],
env,
));
@@ -140,7 +153,9 @@ const _generateCircuitInputs = (
const docStore =
doc === 'passport'
? protocolStore.passport
: protocolStore.id_card;
: doc === 'aadhaar'
? protocolStore.aadhaar
: protocolStore.id_card;
switch (tree) {
case 'ofac':
return docStore.ofac_trees;
@@ -335,7 +350,7 @@ interface ProvingState {
socketConnection: Socket | null;
uuid: string | null;
userConfirmed: boolean;
passportData: PassportData | null;
passportData: IDDocument | null;
secret: string | null;
circuitType: provingMachineCircuitType | null;
error_code: string | null;
@@ -959,34 +974,49 @@ export const useProvingStore = create<ProvingState>((set, get) => {
selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED);
const startTime = Date.now();
const context = createProofContext('startFetchingData');
// passport and id card
logProofEvent('info', 'Fetching DSC data started', context);
try {
const { passportData, env } = get();
if (!passportData) {
throw new Error('PassportData is not available');
}
if (!passportData?.dsc_parsed) {
logProofEvent('error', 'Missing parsed DSC', context, {
failure: 'PROOF_FAILED_DATA_FETCH',
duration_ms: Date.now() - startTime,
});
console.error('Missing parsed DSC in passport data');
selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, {
message: 'Missing parsed DSC in passport data',
});
actor!.send({ type: 'FETCH_ERROR' });
return;
}
const document: DocumentCategory = passportData.documentCategory;
logProofEvent('info', 'Protocol store fetch', context, {
step: 'protocol_store_fetch',
document,
});
await useProtocolStore
.getState()
[
document
].fetch_all(env!, (passportData as PassportData).dsc_parsed!.authorityKeyIdentifier);
console.log('document', document);
switch (passportData.documentCategory) {
case 'passport':
case 'id_card':
if (!passportData?.dsc_parsed) {
logProofEvent('error', 'Missing parsed DSC', context, {
failure: 'PROOF_FAILED_DATA_FETCH',
duration_ms: Date.now() - startTime,
});
console.error('Missing parsed DSC in passport data');
selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, {
message: 'Missing parsed DSC in passport data',
});
actor!.send({ type: 'FETCH_ERROR' });
return;
}
logProofEvent('info', 'Protocol store fetch', context, {
step: 'protocol_store_fetch',
document,
});
await useProtocolStore
.getState()
[
document
].fetch_all(env!, (passportData as PassportData).dsc_parsed!.authorityKeyIdentifier);
break;
case 'aadhaar':
logProofEvent('info', 'Protocol store fetch', context, {
step: 'protocol_store_fetch',
document,
});
await useProtocolStore.getState()[document].fetch_all(env!);
break;
}
logProofEvent('info', 'Data fetch succeeded', context, {
duration_ms: Date.now() - startTime,
});
@@ -1084,8 +1114,10 @@ export const useProvingStore = create<ProvingState>((set, get) => {
secret as string,
{
getCommitmentTree,
getAltCSCA: docType =>
useProtocolStore.getState()[docType].alternative_csca,
getAltCSCA: (docType: DocumentCategory) =>
docType === 'aadhaar'
? useProtocolStore.getState().aadhaar.public_keys
: useProtocolStore.getState()[docType].alternative_csca,
},
);
logProofEvent(
@@ -1135,16 +1167,18 @@ export const useProvingStore = create<ProvingState>((set, get) => {
return;
}
const document: DocumentCategory = passportData.documentCategory;
const isDscRegistered = await checkIfPassportDscIsInTree(
passportData,
useProtocolStore.getState()[document].dsc_tree,
);
logProofEvent('info', 'DSC tree check', context, {
dsc_registered: isDscRegistered,
});
if (isDscRegistered) {
selfClient.trackEvent(ProofEvents.DSC_IN_TREE);
set({ circuitType: 'register' });
if (document === 'passport' || document === 'id_card') {
const isDscRegistered = await checkIfPassportDscIsInTree(
passportData,
useProtocolStore.getState()[document].dsc_tree,
);
logProofEvent('info', 'DSC tree check', context, {
dsc_registered: isDscRegistered,
});
if (isDscRegistered) {
selfClient.trackEvent(ProofEvents.DSC_IN_TREE);
set({ circuitType: 'register' });
}
}
logProofEvent('info', 'Validation succeeded', context, {
duration_ms: Date.now() - startTime,
@@ -1183,7 +1217,10 @@ export const useProvingStore = create<ProvingState>((set, get) => {
let circuitName;
if (circuitType === 'disclose') {
circuitName = 'disclose';
circuitName =
passportData.documentCategory === 'aadhaar'
? 'disclose_aadhaar'
: 'disclose';
} else {
circuitName = getCircuitNameFromPassportData(
passportData,
@@ -1196,6 +1233,7 @@ export const useProvingStore = create<ProvingState>((set, get) => {
passportData as PassportData,
circuitName,
);
logProofEvent('info', 'Circuit resolution', baseContext, {
circuit_name: circuitName,
ws_url: wsRpcUrl,
@@ -1433,7 +1471,7 @@ export const useProvingStore = create<ProvingState>((set, get) => {
endpointType,
endpoint,
circuitTypeWithDocumentExtension,
} = _generateCircuitInputs(
} = await _generateCircuitInputs(
circuitType as 'disclose' | 'register' | 'dsc',
secret,
passportData,
@@ -1499,7 +1537,11 @@ export const useProvingStore = create<ProvingState>((set, get) => {
_handlePassportNotSupported: (selfClient: SelfClient) => {
const passportData = get().passportData;
const countryCode = passportData?.passportMetadata?.countryCode;
const countryCode =
passportData?.documentCategory !== 'aadhaar'
? (passportData as PassportData)?.passportMetadata?.countryCode
: 'IND';
const documentCategory = passportData?.documentCategory;
selfClient.emit(SdkEvents.PROVING_PASSPORT_NOT_SUPPORTED, {

View File

@@ -0,0 +1,60 @@
// 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
import { NativeModules, Platform } from 'react-native';
interface QRScannerBridge {
scanQRCode: () => Promise<string>;
scanQRCodeFromPhotoLibrary: () => Promise<string>;
}
// Platform-specific QRScanner implementation
let QRScanner: QRScannerBridge | null = null;
if (Platform.OS === 'ios') {
QRScanner = NativeModules.QRScannerBridge || null;
} else if (Platform.OS === 'android') {
QRScanner = NativeModules.QRCodeScanner || null;
} else {
console.warn('QRScanner: Unsupported platform');
QRScanner = null;
}
export { QRScanner };
/**
* Check if QR scanner camera is available
*/
export const isQRScannerCameraAvailable = (): boolean => {
return QRScanner?.scanQRCode != null;
};
/**
* Check if QR scanner photo library is available
*/
export const isQRScannerPhotoLibraryAvailable = (): boolean => {
return QRScanner?.scanQRCodeFromPhotoLibrary != null;
};
/**
* Scans QR code from photo library
* @returns Promise that resolves with the QR code content
*/
export const scanQRCodeFromPhotoLibrary = async (): Promise<string> => {
if (!QRScanner?.scanQRCodeFromPhotoLibrary) {
throw new Error('QR Scanner photo library not available on this platform');
}
return await QRScanner.scanQRCodeFromPhotoLibrary();
};
/**
* Scans QR code using device camera
* @returns Promise that resolves with the QR code content
*/
export const scanQRCodeWithCamera = async (): Promise<string> => {
if (!QRScanner?.scanQRCode) {
throw new Error('QR Scanner not available on this platform');
}
return await QRScanner.scanQRCode();
};

View File

@@ -7,6 +7,7 @@ describe('navigation', () => {
const navigationScreens = require('@/navigation').navigationScreens;
const listOfScreens = Object.keys(navigationScreens).sort();
expect(listOfScreens).toEqual([
'AadhaarUpload',
'AccountRecovery',
'AccountRecoveryChoice',
'AccountVerifiedSuccess',

View File

@@ -1,5 +1,6 @@
// Type exports from constants
export type {
AadhaarData,
CertificateData,
DocumentCategory,
IdDocInput,
@@ -84,14 +85,8 @@ export {
stringToBigInt,
} from './src/utils/index.js';
export {
prepareAadhaarRegisterTestData,
prepareAadhaarDiscloseTestData,
prepareAadhaarRegisterData,
prepareAadhaarDiscloseData,
} from './src/utils/aadhaar/mockData.js';
export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js';
export { createSelector } from './src/utils/aadhaar/constants.js';
// Hash utilities
export {
customHasher,
@@ -100,3 +95,14 @@ export {
hash,
packBytesAndPoseidon,
} from './src/utils/hash.js';
export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js';
export { isAadhaarDocument, isMRZDocument } from './src/utils/index.js';
export {
prepareAadhaarDiscloseData,
prepareAadhaarDiscloseTestData,
prepareAadhaarRegisterData,
prepareAadhaarRegisterTestData,
} from './src/utils/aadhaar/mockData.js';

View File

@@ -97,6 +97,11 @@ export const IDENTITY_TREE_URL_STAGING = 'https://tree.staging.self.xyz/identity
export const IDENTITY_TREE_URL_STAGING_ID_CARD = 'https://tree.staging.self.xyz/identity-id';
export const IDENTITY_VERIFICATION_HUB_ADDRESS = '0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF';
export const IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING =
'0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74';
export const ID_CARD_ATTESTATION_ID = '2';
export const MAX_BYTES_IN_FIELD = 31;

View File

@@ -0,0 +1,56 @@
// Hardcoded Aadhaar test certificates for development purposes only
// This file contains mock private keys that should NEVER be used in production
export const AADHAAR_MOCK_PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC//2Yjq4TpEm1t
5Fm4MM/+8MhGPd9vTAZpo04L7HYFbe4YdFmXZBLXH6KbLrbK3uhMuq9dmotJiDtx
Wjch5f5iHwqLLUKsSHJl4Mr2eFZj77TTLkxTEUYEISpRm9JSIHYRg7kcFPbR+CrE
uAe9s3/qLDAD85gqDCiosj6bCovMLayHQYglqN2pbYNp8ZIFaVj1gdkoQg8wCK5O
D3jy5CJnvJirNuiWrvdRLZ48o01L7b/2B/iuBWtoBtOaCTPWZutBIcKB6oNUKBbY
zwG40NxWpQtAeY6NW0CC/apqUEZVPLdYijjsLGBUohHTtLCXB/C1KDNh0sNTfMU8
bkctLqvXAgMBAAECggEAD3zqgBS6F1RRhOyUR9VfZskepsfr9ve/ieFodNhhpuUS
Y8efyIrqmCiPPr+npp+q4DGsRTunyJbXdx8YO0EcSOcIvAE6xr7ekS68JxWLBoT6
MpG8CqfMkAQeFh1trte7UbgtN3SbeoTV6/uNqE7LRUuRbgHGM+VTzFP6OxomyW5/
BGHmhlU5j5r4gdNrztwpnfLFZvZt+4yR99kWIoYbFAvgq6sgRGflo45dHGG80TUd
o3vir1IeNAY5vkeJ9owCxUJW4JxJKarjlBibqRUprEgnjKr2ovxirjUOzOClmVEJ
tgyx4doY/F9cE8jD4JfcC7xxC79j90odfEED+5IBKQKBgQD2nCwPxr9YxMiQLQii
Z4E7x96nHdTvqXKSTPGWX2Zv6Sur9qL1Wyz30tt3COB7+b9UwtpTozDxxlVn+u4U
SnDdVWMrUpDi03dRvsLWhTDC8btN6WnYqGmHKSjst+yHytPo39cqQVZ4TY8Gqfg0
3/Pqb5hpxkJ2RRxVt3gDvgnOPwKBgQDHTuSxZbpQ956z8t6BA4tDYkFC9BFQpb/F
pSrw2w8PMZH4QckbjFj59ME2u/WLyuJ+U8GjR4YTk8ZXQ5niSrPDC5Pa6s3Ano44
8h5FgrMeAbxZ0HuANHRS1YWba8k4tbeunAdj08nIviMJEuhMcjzbqgf5rFrrGzR5
Jb86eznsaQKBgBzMLeUFu3B9QkJ7z8dPOOsnMtvnAuedrPBipc9+gnLNErl5CpyG
MiEacWBcHAK+LlaSjnY3105Ub8K9rbGW48kk4Hi9ooeqVAOquAve78vD+LBncmHH
gNM0vj+uVqOgztAh23lmuddAj1Qi4wYhpNUahPzNFxPCjEWCMDSXq4N3AoGAcqR1
tXi/WA1m8zkzNWCVfXgJ8/ox74K3sXdVIN/QZLvtq7Ajfr4W/AgGD3bEQdm8uE9z
JXlhrOcmglF3NYwkpH+HV7gSC8boJedW9EK+xvbWoY7jSxZhBridNo4kW4NjGYPU
WF6dRePggTqn9jkLuoquNbYnQe8PGtRUj84LvmkCgYEA5iu39qcCBd+JuPDmxOJx
Ah3QcOFI4i7WW9oi7+68aCqee9K7d659hyYWpewYcDXzvSLYvXJJcU9vkVuW8DLK
lQzKVNh2/5SaAN/EysYBpFQVbNZ5dA74WrjxnPsNmwRc6yv/o8I/LfgWOB9yB3fI
avCtlYniKHPvSCA/gS2h4fk=
-----END PRIVATE KEY-----
`;
export const AADHAAR_MOCK_PUBLIC_KEY_PEM = `-----BEGIN CERTIFICATE-----
MIIDjzCCAnegAwIBAgIUZA6u4qBxEjW4dxmbLaLkWnHIybowDQYJKoZIhvcNAQEL
BQAwVzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0y
NTA5MTgxMDE2NTlaFw0yNjA5MTgxMDE2NTlaMFcxCzAJBgNVBAYTAlVTMQ4wDAYD
VQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UECgwMT3JnYW5pemF0aW9u
MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC//2Yjq4TpEm1t5Fm4MM/+8MhGPd9vTAZpo04L7HYFbe4YdFmXZBLXH6Kb
LrbK3uhMuq9dmotJiDtxWjch5f5iHwqLLUKsSHJl4Mr2eFZj77TTLkxTEUYEISpR
m9JSIHYRg7kcFPbR+CrEuAe9s3/qLDAD85gqDCiosj6bCovMLayHQYglqN2pbYNp
8ZIFaVj1gdkoQg8wCK5OD3jy5CJnvJirNuiWrvdRLZ48o01L7b/2B/iuBWtoBtOa
CTPWZutBIcKB6oNUKBbYzwG40NxWpQtAeY6NW0CC/apqUEZVPLdYijjsLGBUohHT
tLCXB/C1KDNh0sNTfMU8bkctLqvXAgMBAAGjUzBRMB0GA1UdDgQWBBTGyVMLFNL2
PRJwtA8vekrtJVu2BTAfBgNVHSMEGDAWgBTGyVMLFNL2PRJwtA8vekrtJVu2BTAP
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCwcKlyaZw3jxDNtU6j
V8g9tUr77z0LyTrVe0GujxFaa4EKKKqG/lzf6wNDaHGOgyEwhPsi/ui8VU6Y8KTS
SxorUta+2zNHu8jziz1rxYTfgPWvK54B3Q3q4ycRLmYfR0CVvH2+TvTAqfEEvpEh
8tY9mpNzjYsLzlwPszkWU+WpLJjH0VPhVIiFC65EaxuArZrap8IpuK/bSa4Beqbb
7rMo/KmDfhFpVMQcOrvyQJmurtmjo12Esb0EjwZp634nDVRC9gFXEh5YuWBg3IaI
cTCvHQ+MAXTzZMOfc2dWZYdk1PaO6xLTw0YfGAtl6r3x4Csd0i5iwpDo1JXjSpZE
mESQ
-----END CERTIFICATE-----
`;

View File

@@ -1,8 +1,11 @@
import forge from 'node-forge';
import { poseidon5 } from 'poseidon-lite';
import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js';
import { formatCountriesList } from '../circuits/formatInputs.js';
import { findIndexInTree, formatInput } from '../circuits/generateInputs.js';
import { packBytesAndPoseidon } from '../hash.js';
import { shaPad } from '../shaPad.js';
import {
generateMerkleProof,
generateSMTProof,
@@ -10,20 +13,25 @@ import {
getNameYobLeafAahaar,
} from '../trees.js';
import { testQRData } from './assets/dataInput.js';
import { calculateAge, generateTestData, stringToAsciiArray, testCustomData } from './utils.js';
import { extractQRDataFields } from './utils.js';
import { AadhaarField, createSelector } from './constants.js';
import { formatCountriesList } from '../circuits/formatInputs.js';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { SMT } from '@openpassport/zk-kit-smt';
import {
calculateAge,
extractQRDataFields,
generateTestData,
stringToAsciiArray,
testCustomData,
} from './utils.js';
import {
convertBigIntToByteArray,
decompressByteArray,
extractPhoto,
splitToWords,
} from '@anon-aadhaar/core';
import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { SMT } from '@openpassport/zk-kit-smt';
import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js';
import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js';
// Helper function to compute padded name
function computePaddedName(name: string): number[] {
@@ -41,22 +49,18 @@ function computeUppercasePaddedName(name: string): number[] {
.map((char) => char.charCodeAt(0));
}
// Helper function to compute nullifier
function nullifierHash(extractedFields: ReturnType<typeof extractQRDataFields>): bigint {
const genderAscii = stringToAsciiArray(extractedFields.gender)[0];
const personalInfoHashArgs = [
genderAscii,
...stringToAsciiArray(extractedFields.yob),
...stringToAsciiArray(extractedFields.mob),
...stringToAsciiArray(extractedFields.dob),
...stringToAsciiArray(extractedFields.name.toUpperCase().padEnd(62, '\0')),
...stringToAsciiArray(extractedFields.aadhaarLast4Digits),
];
return BigInt(packBytesAndPoseidon(personalInfoHashArgs));
export function convertByteArrayToBigInt(byteArray: Uint8Array | number[]): bigint {
let result = 0n;
for (let i = 0; i < byteArray.length; i++) {
result = result * 256n + BigInt(byteArray[i]);
}
return result;
}
// Helper function to compute packed commitment
function computePackedCommitment(extractedFields: ReturnType<typeof extractQRDataFields>): bigint {
export function computePackedCommitment(
extractedFields: ReturnType<typeof extractQRDataFields>
): bigint {
const packedCommitmentArgs = [
3,
...stringToAsciiArray(extractedFields.pincode),
@@ -68,7 +72,7 @@ function computePackedCommitment(extractedFields: ReturnType<typeof extractQRDat
}
// Helper function to compute final commitment
function computeCommitment(
export function computeCommitment(
secret: bigint,
qrHash: bigint,
nullifier: bigint,
@@ -90,48 +94,26 @@ interface SharedQRData {
photoHash: bigint;
}
function processQRData(
privKeyPem: string,
name?: string,
dateOfBirth?: string,
gender?: string,
pincode?: string,
state?: string,
timestamp?: string
): SharedQRData {
const finalName = name ?? 'Sumit Kumar';
const finalDateOfBirth = dateOfBirth ?? '01-01-1984';
const finalGender = gender ?? 'M';
const finalPincode = pincode ?? '110051';
const finalState = state ?? 'Delhi';
let QRData: string;
if (name || dateOfBirth || gender || pincode || state) {
const newTestData = generateTestData({
privKeyPem,
data: testCustomData,
name: finalName,
dob: finalDateOfBirth,
gender: finalGender,
pincode: finalPincode,
state: finalState,
timestamp: timestamp,
});
QRData = newTestData.testQRData;
} else {
QRData = testQRData.testQRData;
}
return processQRDataSimple(QRData);
// Helper function to compute nullifier
export function nullifierHash(extractedFields: ReturnType<typeof extractQRDataFields>): bigint {
const genderAscii = stringToAsciiArray(extractedFields.gender)[0];
const personalInfoHashArgs = [
genderAscii,
...stringToAsciiArray(extractedFields.yob),
...stringToAsciiArray(extractedFields.mob),
...stringToAsciiArray(extractedFields.dob),
...stringToAsciiArray(extractedFields.name.toUpperCase().padEnd(62, '\0')),
...stringToAsciiArray(extractedFields.aadhaarLast4Digits),
];
return BigInt(packBytesAndPoseidon(personalInfoHashArgs));
}
function processQRDataSimple(qrData: string) {
export function processQRDataSimple(qrData: string) {
const qrDataBytes = convertBigIntToByteArray(BigInt(qrData));
const decodedData = decompressByteArray(qrDataBytes);
const signedData = decodedData.slice(0, decodedData.length - 256);
const [qrDataPadded, qrDataPaddedLen] = sha256Pad(signedData, 512 * 3);
const [qrDataPaddedNumber, qrDataPaddedLen] = shaPad(signedData, 512 * 3);
const qrDataPadded = new Uint8Array(qrDataPaddedNumber);
let photoEOI = 0;
for (let i = 0; i < qrDataPadded.length - 1; i++) {
if (qrDataPadded[i + 1] === 217 && qrDataPadded[i] === 255) {
@@ -163,139 +145,34 @@ function processQRDataSimple(qrData: string) {
};
}
export function prepareAadhaarRegisterTestData(
privKeyPem: string,
pubkeyPem: string,
export function prepareAadhaarDiscloseData(
qrData: string,
identityTree: LeanIMT,
nameAndDob_smt: SMT,
nameAndYob_smt: SMT,
scope: string,
secret: string,
name?: string,
dateOfBirth?: string,
gender?: string,
pincode?: string,
state?: string,
timestamp?: string
user_identifier: string,
discloseAttributes: {
dateOfBirth?: boolean;
name?: boolean;
gender?: boolean;
idNumber?: boolean;
issuingState?: boolean;
minimumAge?: number;
forbiddenCountriesListPacked?: string[];
ofac?: boolean;
}
) {
const sharedData = processQRData(
privKeyPem,
name,
dateOfBirth,
gender,
pincode,
state,
timestamp
);
const delimiterIndices: number[] = [];
for (let i = 0; i < sharedData.qrDataPadded.length; i++) {
if (sharedData.qrDataPadded[i] === 255) {
delimiterIndices.push(i);
}
if (delimiterIndices.length === 18) {
break;
}
}
let photoEOI = 0;
for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) {
if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) {
photoEOI = i + 1;
}
}
if (photoEOI === 0) {
throw new Error('Photo EOI not found');
}
const signatureBytes = sharedData.decodedData.slice(
sharedData.decodedData.length - 256,
sharedData.decodedData.length
);
const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString());
const publicKey = forge.pki.publicKeyFromPem(pubkeyPem);
const modulusHex = publicKey.n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
const inputs = {
qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded),
qrDataPaddedLength: sharedData.qrDataPaddedLen,
delimiterIndices: delimiterIndices,
signature: splitToWords(signature, BigInt(121), BigInt(17)),
pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}
export async function prepareAadhaarRegisterData(qrData: string, secret: string, certs: string[]) {
const sharedData = processQRDataSimple(qrData);
const delimiterIndices: number[] = [];
for (let i = 0; i < sharedData.qrDataPadded.length; i++) {
if (sharedData.qrDataPadded[i] === 255) {
delimiterIndices.push(i);
}
if (delimiterIndices.length === 18) {
break;
}
}
let photoEOI = 0;
for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) {
if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) {
photoEOI = i + 1;
}
}
if (photoEOI === 0) {
throw new Error('Photo EOI not found');
}
const signatureBytes = sharedData.decodedData.slice(
sharedData.decodedData.length - 256,
sharedData.decodedData.length
);
const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString());
//do promise.all for all certs and pick the one that is valid
const certificates = await Promise.all(
certs.map(async (cert) => {
const certificate = forge.pki.certificateFromPem(cert);
const publicKey = certificate.publicKey as forge.pki.rsa.PublicKey;
try {
const md = forge.md.sha256.create();
md.update(forge.util.binary.raw.encode(sharedData.signedData));
const isValid = publicKey.verify(md.digest().getBytes(), signatureBytes);
return isValid;
} catch (error) {
return false;
}
})
const { currentYear, currentMonth, currentDay } = calculateAge(
sharedData.extractedFields.dob,
sharedData.extractedFields.mob,
sharedData.extractedFields.yob
);
//find the valid cert
const validCert = certificates.indexOf(true);
if (validCert === -1) {
throw new Error('No valid certificate found');
}
const certPem = certs[validCert];
const cert = forge.pki.certificateFromPem(certPem);
const modulusHex = (cert.publicKey as forge.pki.rsa.PublicKey).n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0];
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
@@ -306,14 +183,99 @@ export async function prepareAadhaarRegisterData(qrData: string, secret: string,
BigInt(sharedData.photoHash)
);
const paddedName = computePaddedName(sharedData.extractedFields.name);
const index = findIndexInTree(identityTree, BigInt(commitment));
const {
siblings,
path: merkle_path,
leaf_depth,
} = generateMerkleProof(identityTree, index, COMMITMENT_TREE_DEPTH);
const namedob_leaf = getNameDobLeafAadhaar(
sharedData.extractedFields.name,
sharedData.extractedFields.yob,
sharedData.extractedFields.mob,
sharedData.extractedFields.dob
);
const nameyob_leaf = getNameYobLeafAahaar(
sharedData.extractedFields.name,
sharedData.extractedFields.yob
);
const {
root: ofac_name_dob_smt_root,
closestleaf: ofac_name_dob_smt_leaf_key,
siblings: ofac_name_dob_smt_siblings,
} = generateSMTProof(nameAndDob_smt, namedob_leaf);
const {
root: ofac_name_yob_smt_root,
closestleaf: ofac_name_yob_smt_leaf_key,
siblings: ofac_name_yob_smt_siblings,
} = generateSMTProof(nameAndYob_smt, nameyob_leaf);
const selectorArr: AadhaarField[] = [];
if (discloseAttributes.dateOfBirth) {
selectorArr.push('YEAR_OF_BIRTH');
selectorArr.push('MONTH_OF_BIRTH');
selectorArr.push('DAY_OF_BIRTH');
}
if (discloseAttributes.name) {
selectorArr.push('NAME');
}
if (discloseAttributes.gender) {
selectorArr.push('GENDER');
}
if (discloseAttributes.idNumber) {
selectorArr.push('AADHAAR_LAST_4_DIGITS');
}
if (discloseAttributes.issuingState) {
selectorArr.push('STATE');
}
if (discloseAttributes.ofac) {
selectorArr.push('OFAC_NAME_DOB_CHECK');
selectorArr.push('OFAC_NAME_YOB_CHECK');
}
const selector = createSelector(selectorArr);
const inputs = {
qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded),
qrDataPaddedLength: sharedData.qrDataPaddedLen,
delimiterIndices: delimiterIndices,
signature: splitToWords(signature, BigInt(121), BigInt(17)),
pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)),
secret: secret,
photoEOI: photoEOI,
attestation_id: '3',
secret,
qrDataHash: formatInput(BigInt(sharedData.qrHash)),
gender: formatInput(genderAscii),
// qrDataHash: BigInt(sharedData.qrHash).toString(),
// gender: genderAscii.toString(),
yob: stringToAsciiArray(sharedData.extractedFields.yob),
mob: stringToAsciiArray(sharedData.extractedFields.mob),
dob: stringToAsciiArray(sharedData.extractedFields.dob),
name: formatInput(paddedName),
aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits),
pincode: stringToAsciiArray(sharedData.extractedFields.pincode),
state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')),
ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits),
photoHash: formatInput(BigInt(sharedData.photoHash)),
merkle_root: formatInput(BigInt(identityTree.root)),
leaf_depth: formatInput(leaf_depth),
path: formatInput(merkle_path),
siblings: formatInput(siblings),
ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)),
ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)),
ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings),
ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)),
ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)),
ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings),
selector: formatInput(selector),
minimumAge: formatInput(discloseAttributes.minimumAge ?? 0),
currentYear: formatInput(currentYear),
currentMonth: formatInput(currentMonth),
currentDay: formatInput(currentDay),
scope: formatInput(BigInt(scope)),
user_identifier: formatInput(BigInt(user_identifier)),
forbidden_countries_list: discloseAttributes.forbiddenCountriesListPacked
? formatInput(formatCountriesList(discloseAttributes.forbiddenCountriesListPacked))
: formatInput([...Array(120)].map((_) => '0')),
};
return inputs;
@@ -440,34 +402,61 @@ export function prepareAadhaarDiscloseTestData(
};
}
export function prepareAadhaarDiscloseData(
qrData: string,
identityTree: LeanIMT,
nameAndDob_smt: SMT,
nameAndYob_smt: SMT,
scope: string,
secret: string,
user_identifier: string,
discloseAttributes: {
dateOfBirth?: boolean;
name?: boolean;
gender?: boolean;
idNumber?: boolean;
issuingState?: boolean;
minimumAge?: number;
forbiddenCountriesListPacked?: string[];
ofac?: boolean;
}
) {
export async function prepareAadhaarRegisterData(qrData: string, secret: string, certs: string[]) {
const sharedData = processQRDataSimple(qrData);
const delimiterIndices: number[] = [];
for (let i = 0; i < sharedData.qrDataPadded.length; i++) {
if (sharedData.qrDataPadded[i] === 255) {
delimiterIndices.push(i);
}
if (delimiterIndices.length === 18) {
break;
}
}
let photoEOI = 0;
for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) {
if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) {
photoEOI = i + 1;
}
}
if (photoEOI === 0) {
throw new Error('Photo EOI not found');
}
const { currentYear, currentMonth, currentDay } = calculateAge(
sharedData.extractedFields.dob,
sharedData.extractedFields.mob,
sharedData.extractedFields.yob
const signatureBytes = sharedData.decodedData.slice(
sharedData.decodedData.length - 256,
sharedData.decodedData.length
);
const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString());
//do promise.all for all certs and pick the one that is valid
const certificates = await Promise.all(
certs.map(async (cert) => {
const certificate = forge.pki.certificateFromPem(cert);
const publicKey = certificate.publicKey as forge.pki.rsa.PublicKey;
try {
const md = forge.md.sha256.create();
md.update(forge.util.binary.raw.encode(sharedData.signedData));
const isValid = publicKey.verify(md.digest().getBytes(), signatureBytes);
return isValid;
} catch (error) {
return false;
}
})
);
const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0];
//find the valid cert
const validCert = certificates.indexOf(true);
if (validCert === -1) {
throw new Error('No valid certificate found');
}
const certPem = certs[validCert];
const cert = forge.pki.certificateFromPem(certPem);
const modulusHex = (cert.publicKey as forge.pki.rsa.PublicKey).n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
@@ -478,98 +467,129 @@ export function prepareAadhaarDiscloseData(
BigInt(sharedData.photoHash)
);
const paddedName = computePaddedName(sharedData.extractedFields.name);
const index = findIndexInTree(identityTree, BigInt(commitment));
const {
siblings,
path: merkle_path,
leaf_depth,
} = generateMerkleProof(identityTree, index, COMMITMENT_TREE_DEPTH);
const namedob_leaf = getNameDobLeafAadhaar(
sharedData.extractedFields.name,
sharedData.extractedFields.yob,
sharedData.extractedFields.mob,
sharedData.extractedFields.dob
);
const nameyob_leaf = getNameYobLeafAahaar(
sharedData.extractedFields.name,
sharedData.extractedFields.yob
);
const {
root: ofac_name_dob_smt_root,
closestleaf: ofac_name_dob_smt_leaf_key,
siblings: ofac_name_dob_smt_siblings,
} = generateSMTProof(nameAndDob_smt, namedob_leaf);
const {
root: ofac_name_yob_smt_root,
closestleaf: ofac_name_yob_smt_leaf_key,
siblings: ofac_name_yob_smt_siblings,
} = generateSMTProof(nameAndYob_smt, nameyob_leaf);
const selectorArr: AadhaarField[] = [];
if (discloseAttributes.dateOfBirth) {
selectorArr.push('YEAR_OF_BIRTH');
selectorArr.push('MONTH_OF_BIRTH');
selectorArr.push('DAY_OF_BIRTH');
}
if (discloseAttributes.name) {
selectorArr.push('NAME');
}
if (discloseAttributes.gender) {
selectorArr.push('GENDER');
}
if (discloseAttributes.idNumber) {
selectorArr.push('AADHAAR_LAST_4_DIGITS');
}
if (discloseAttributes.issuingState) {
selectorArr.push('STATE');
}
if (discloseAttributes.ofac) {
selectorArr.push('OFAC_NAME_DOB_CHECK');
selectorArr.push('OFAC_NAME_YOB_CHECK');
}
const selector = createSelector(selectorArr);
const inputs = {
attestation_id: '3',
secret,
qrDataHash: BigInt(sharedData.qrHash).toString(),
gender: genderAscii.toString(),
yob: stringToAsciiArray(sharedData.extractedFields.yob),
mob: stringToAsciiArray(sharedData.extractedFields.mob),
dob: stringToAsciiArray(sharedData.extractedFields.dob),
name: formatInput(paddedName),
aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits),
pincode: stringToAsciiArray(sharedData.extractedFields.pincode),
state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')),
ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits),
photoHash: formatInput(BigInt(sharedData.photoHash)),
merkle_root: formatInput(BigInt(identityTree.root)),
leaf_depth: formatInput(leaf_depth),
path: formatInput(merkle_path),
siblings: formatInput(siblings),
ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)),
ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)),
ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings),
ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)),
ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)),
ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings),
selector,
minimumAge: formatInput(discloseAttributes.minimumAge ?? 0),
currentYear: formatInput(currentYear),
currentMonth: formatInput(currentMonth),
currentDay: formatInput(currentDay),
scope: formatInput(BigInt(scope)),
user_identifier: formatInput(BigInt(user_identifier)),
forbidden_countries_list: discloseAttributes.forbiddenCountriesListPacked
? formatInput(formatCountriesList(discloseAttributes.forbiddenCountriesListPacked))
: formatInput([...Array(120)].map((_) => '0')),
qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded),
qrDataPaddedLength: sharedData.qrDataPaddedLen,
delimiterIndices: delimiterIndices,
signature: splitToWords(signature, BigInt(121), BigInt(17)),
pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)),
secret: secret,
photoEOI: photoEOI,
};
return inputs;
}
export function prepareAadhaarRegisterTestData(
privKeyPem: string,
pubkeyPem: string,
secret: string,
name?: string,
dateOfBirth?: string,
gender?: string,
pincode?: string,
state?: string,
timestamp?: string
) {
const sharedData = processQRData(
privKeyPem,
name,
dateOfBirth,
gender,
pincode,
state,
timestamp
);
const delimiterIndices: number[] = [];
for (let i = 0; i < sharedData.qrDataPadded.length; i++) {
if (sharedData.qrDataPadded[i] === 255) {
delimiterIndices.push(i);
}
if (delimiterIndices.length === 18) {
break;
}
}
let photoEOI = 0;
for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) {
if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) {
photoEOI = i + 1;
}
}
if (photoEOI === 0) {
throw new Error('Photo EOI not found');
}
const signatureBytes = sharedData.decodedData.slice(
sharedData.decodedData.length - 256,
sharedData.decodedData.length
);
const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString());
const publicKey = forge.pki.publicKeyFromPem(pubkeyPem);
const modulusHex = publicKey.n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
const inputs = {
qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded),
qrDataPaddedLength: sharedData.qrDataPaddedLen,
delimiterIndices: delimiterIndices,
signature: splitToWords(signature, BigInt(121), BigInt(17)),
pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}
export function processQRData(
privKeyPem: string,
name?: string,
dateOfBirth?: string,
gender?: string,
pincode?: string,
state?: string,
timestamp?: string
): SharedQRData {
const finalName = name ?? 'Sumit Kumar';
const finalDateOfBirth = dateOfBirth ?? '01-01-1984';
const finalGender = gender ?? 'M';
const finalPincode = pincode ?? '110051';
const finalState = state ?? 'Delhi';
let QRData: string;
if (name || dateOfBirth || gender || pincode || state) {
const newTestData = generateTestData({
privKeyPem,
data: testCustomData,
name: finalName,
dob: finalDateOfBirth,
gender: finalGender,
pincode: finalPincode,
state: finalState,
timestamp: timestamp,
});
QRData = newTestData.testQRData;
} else {
QRData = testQRData.testQRData;
// console.log('testQRData:', testQRData);
}
return processQRDataSimple(QRData);
}

View File

@@ -1,5 +1,8 @@
import { ethers } from 'ethers';
import forge from 'node-forge';
import { IDENTITY_VERIFICATION_HUB_ADDRESS, RPC_URL } from '../../constants/constants.js';
import {
convertBigIntToByteArray,
decompressByteArray,
@@ -94,6 +97,7 @@ export const createCustomV2TestData = ({
photo,
name,
timestamp,
aadhaarLast4Digits,
}: {
signedData: Uint8Array;
dob?: string;
@@ -103,6 +107,7 @@ export const createCustomV2TestData = ({
photo?: boolean;
name?: string;
timestamp?: string;
aadhaarLast4Digits?: string;
}) => {
const allDataParsed: number[][] = [];
const delimiterIndices: number[] = [];
@@ -123,6 +128,18 @@ export const createCustomV2TestData = ({
}
}
console.log('createCustomV2TestData', {
signedData,
dob,
pincode,
gender,
state,
photo,
name,
timestamp,
aadhaarLast4Digits,
});
// Set new timestamp to the time of the signature
const newDateString = returnNewDateString(timestamp);
const newTimestamp = new TextEncoder().encode(newDateString);
@@ -175,6 +192,12 @@ export const createCustomV2TestData = ({
);
}
if (!aadhaarLast4Digits) {
for (let i = 2; i < 6; i++) {
modifiedSignedData[i] = Math.floor(Math.random() * 10) + 48;
}
}
if (name) {
const newName = new TextEncoder().encode(name);
modifiedSignedData = replaceBytesBetween(
@@ -275,31 +298,16 @@ export function extractQRDataFields(qrData: string | Uint8Array): ExtractedQRDat
const phoneData = extractFieldData(signedData, delimiterIndices, FIELD_POSITIONS.PHONE_NO);
const phoneNoLast4Digits = asciiArrayToString(phoneData.slice(phoneData.length - 4));
// Extract timestamp (from position after first delimiter)
// Timestamp format: YYYYMMDDHHMM (similar to circom implementation)
const timestampStartIndex = delimiterIndices[0] + 1;
const timestampYear = asciiArrayToString([
signedData[timestampStartIndex + 8],
signedData[timestampStartIndex + 9],
signedData[timestampStartIndex + 10],
signedData[timestampStartIndex + 11],
]);
const timestampMonth = asciiArrayToString([
signedData[timestampStartIndex + 12],
signedData[timestampStartIndex + 13],
]);
const timestampDay = asciiArrayToString([
signedData[timestampStartIndex + 14],
signedData[timestampStartIndex + 15],
]);
const timestampHour = asciiArrayToString([
signedData[timestampStartIndex + 16],
signedData[timestampStartIndex + 17],
]);
const timestampMinute = asciiArrayToString([
signedData[timestampStartIndex + 18],
signedData[timestampStartIndex + 19],
signedData[9],
signedData[10],
signedData[11],
signedData[12],
]);
const timestampMonth = asciiArrayToString([signedData[13], signedData[14]]);
const timestampDay = asciiArrayToString([signedData[15], signedData[16]]);
const timestampHour = asciiArrayToString([signedData[17], signedData[18]]);
const timestampMinute = asciiArrayToString([signedData[19], signedData[20]]);
const timestamp = `${timestampYear}-${timestampMonth}-${timestampDay} ${timestampHour}:${timestampMinute}`;
@@ -371,6 +379,24 @@ export const generateTestData = ({
return newQrData;
};
export async function getAadharRegistrationWindow() {
try {
const provider = new ethers.JsonRpcProvider(RPC_URL);
const identityVerificationHub = new ethers.Contract(
IDENTITY_VERIFICATION_HUB_ADDRESS,
['function AADHAAR_REGISTRATION_WINDOW() view returns (uint256)'],
provider
);
const aadharRegistrationWindow = await identityVerificationHub.AADHAAR_REGISTRATION_WINDOW();
return aadharRegistrationWindow;
} catch (error) {
console.warn('Failed to get aadhar registration window:', error);
return 120;
}
}
export function returnNewDateString(timestamp?: string): string {
const newDate = timestamp ? new Date(+timestamp) : new Date();

View File

@@ -1,7 +1,7 @@
import type { PassportData } from '../types.js';
import type { IDDocument, PassportData } from '../types.js';
export function getCircuitNameFromPassportData(
passportData: PassportData,
passportData: IDDocument,
circuitType: 'register' | 'dsc'
) {
if (circuitType === 'register') {
@@ -11,9 +11,13 @@ export function getCircuitNameFromPassportData(
}
}
function getDSCircuitNameFromPassportData(passportData: PassportData) {
function getDSCircuitNameFromPassportData(passportData: IDDocument) {
console.log('Getting DSC circuit name from passport data...');
if (passportData.documentCategory === 'aadhaar') {
throw new Error('Aadhaar does not have a DSC circuit');
}
if (!passportData.passportMetadata) {
console.error('Passport metadata is missing');
throw new Error('Passport data are not parsed');
@@ -76,9 +80,13 @@ function getDSCircuitNameFromPassportData(passportData: PassportData) {
}
}
function getRegisterNameFromPassportData(passportData: PassportData) {
function getRegisterNameFromPassportData(passportData: IDDocument) {
console.log('Getting register circuit name from passport data...');
if (passportData.documentCategory === 'aadhaar') {
return 'register_aadhaar';
}
if (!passportData.passportMetadata) {
console.error('Passport metadata is missing');
throw new Error('Passport data are not parsed');

View File

@@ -1,6 +1,7 @@
import { poseidon2 } from 'poseidon-lite';
import {
AADHAAR_ATTESTATION_ID,
attributeToPosition,
attributeToPosition_ID,
DEFAULT_MAJORITY,
@@ -17,13 +18,94 @@ import {
getCircuitNameFromPassportData,
hashEndpointWithScope,
} from '../../utils/index.js';
import type { OfacTree } from '../../utils/types.js';
import type { AadhaarData, IDDocument, OfacTree } from '../../utils/types.js';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { SMT } from '@openpassport/zk-kit-smt';
export { generateCircuitInputsRegister } from './generateInputs.js';
export function generateTEEInputsAadhaarDisclose(
secret: string,
aadhaarData: AadhaarData,
selfApp: SelfApp,
getTree: <T extends 'ofac' | 'commitment'>(
doc: DocumentCategory,
tree: T
) => T extends 'ofac' ? OfacTree : any
) {
const { prepareAadhaarDiscloseData } = require('../aadhaar/mockData.js');
const { scope, disclosures, endpoint, userId, userDefinedData, chainID } = selfApp;
const userIdentifierHash = calculateUserIdentifierHash(chainID, userId, userDefinedData);
const scope_hash = hashEndpointWithScope(endpoint, scope);
const ofac_trees = getTree('aadhaar', 'ofac');
if (!ofac_trees) {
throw new Error('OFAC trees not loaded');
}
if (!ofac_trees.nameAndDob || !ofac_trees.nameAndYob) {
throw new Error('Invalid OFAC tree structure: missing required fields');
}
const nameAndDobSMT = new SMT(poseidon2, true);
const nameAndYobSMT = new SMT(poseidon2, true);
nameAndDobSMT.import(ofac_trees.nameAndDob);
nameAndYobSMT.import(ofac_trees.nameAndYob);
const serialized_tree = getTree('aadhaar', 'commitment');
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serialized_tree);
const inputs = prepareAadhaarDiscloseData(
aadhaarData.qrData,
tree,
nameAndDobSMT,
nameAndYobSMT,
scope_hash,
secret,
userIdentifierHash.toString(),
{
dateOfBirth: disclosures.date_of_birth,
name: disclosures.name,
gender: disclosures.gender,
idNumber: disclosures.passport_number,
issuingState: disclosures.issuing_state,
minimumAge: disclosures.minimumAge,
forbiddenCountriesListPacked: disclosures.excludedCountries,
ofac: disclosures.ofac,
}
);
return {
inputs,
circuitName: 'vc_and_disclose_aadhaar',
endpointType: selfApp.endpointType,
endpoint: selfApp.endpoint,
};
}
export async function generateTEEInputsAadhaarRegister(
secret: string,
aadhaarData: AadhaarData,
publicKeys: string[],
env: 'prod' | 'stg'
) {
const { prepareAadhaarRegisterData } = require('../aadhaar/mockData.js');
console.log(
'publicKeys-aadhaar',
publicKeys,
'secret-aadhaar',
secret,
'aadhaarData-aadhaar',
aadhaarData
);
const inputs = await prepareAadhaarRegisterData(aadhaarData.qrData, secret, publicKeys);
const circuitName = 'register_aadhaar';
const endpointType = env === 'stg' ? 'staging_celo' : 'celo';
const endpoint = 'https://self.xyz';
return { inputs, circuitName, endpointType, endpoint };
}
export function generateTEEInputsDSC(
passportData: PassportData,
cscaTree: string[][],
@@ -36,15 +118,63 @@ export function generateTEEInputsDSC(
return { inputs, circuitName, endpointType, endpoint };
}
/*** DISCLOSURE ***/
function getSelectorDg1(document: DocumentCategory, disclosures: SelfAppDisclosureConfig) {
switch (document) {
case 'passport':
return getSelectorDg1Passport(disclosures);
case 'id_card':
return getSelectorDg1IdCard(disclosures);
}
}
function getSelectorDg1Passport(disclosures: SelfAppDisclosureConfig) {
const selector_dg1 = Array(88).fill('0');
Object.entries(disclosures).forEach(([attribute, reveal]) => {
if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) {
return;
}
if (reveal) {
const [start, end] = attributeToPosition[attribute as keyof typeof attributeToPosition];
selector_dg1.fill('1', start, end + 1);
}
});
return selector_dg1;
}
function getSelectorDg1IdCard(disclosures: SelfAppDisclosureConfig) {
const selector_dg1 = Array(90).fill('0');
Object.entries(disclosures).forEach(([attribute, reveal]) => {
if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) {
return;
}
if (reveal) {
const [start, end] = attributeToPosition_ID[attribute as keyof typeof attributeToPosition_ID];
selector_dg1.fill('1', start, end + 1);
}
});
return selector_dg1;
}
export function generateTEEInputsDiscloseStateless(
secret: string,
passportData: PassportData,
passportData: IDDocument,
selfApp: SelfApp,
getTree: <T extends 'ofac' | 'commitment'>(
doc: DocumentCategory,
tree: T
) => T extends 'ofac' ? OfacTree : any
) {
if (passportData.documentCategory === 'aadhaar') {
const { inputs, circuitName, endpointType, endpoint } = generateTEEInputsAadhaarDisclose(
secret,
passportData,
selfApp,
getTree
);
return { inputs, circuitName, endpointType, endpoint };
}
const { scope, disclosures, endpoint, userId, userDefinedData, chainID } = selfApp;
const userIdentifierHash = calculateUserIdentifierHash(chainID, userId, userDefinedData);
const scope_hash = hashEndpointWithScope(endpoint, scope);
@@ -107,54 +237,29 @@ export function generateTEEInputsDiscloseStateless(
};
}
export function generateTEEInputsRegister(
export async function generateTEEInputsRegister(
secret: string,
passportData: PassportData,
dscTree: string,
passportData: IDDocument,
dscTree: string | string[],
env: 'prod' | 'stg'
) {
const inputs = generateCircuitInputsRegister(secret, passportData, dscTree);
if (passportData.documentCategory === 'aadhaar') {
const { inputs, circuitName, endpointType, endpoint } = await generateTEEInputsAadhaarRegister(
secret,
passportData,
dscTree as string[],
env
);
console.log('inputs-aadhaar', inputs);
console.log('circuitName-aadhaar', circuitName);
console.log('endpointType-aadhaar', endpointType);
console.log('endpoint-aadhaar', endpoint);
return { inputs, circuitName, endpointType, endpoint };
}
const inputs = generateCircuitInputsRegister(secret, passportData, dscTree as string);
const circuitName = getCircuitNameFromPassportData(passportData, 'register');
const endpointType = env === 'stg' ? 'staging_celo' : 'celo';
const endpoint = 'https://self.xyz';
return { inputs, circuitName, endpointType, endpoint };
}
/*** DISCLOSURE ***/
function getSelectorDg1(document: DocumentCategory, disclosures: SelfAppDisclosureConfig) {
switch (document) {
case 'passport':
return getSelectorDg1Passport(disclosures);
case 'id_card':
return getSelectorDg1IdCard(disclosures);
}
}
function getSelectorDg1Passport(disclosures: SelfAppDisclosureConfig) {
const selector_dg1 = Array(88).fill('0');
Object.entries(disclosures).forEach(([attribute, reveal]) => {
if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) {
return;
}
if (reveal) {
const [start, end] = attributeToPosition[attribute as keyof typeof attributeToPosition];
selector_dg1.fill('1', start, end + 1);
}
});
return selector_dg1;
}
function getSelectorDg1IdCard(disclosures: SelfAppDisclosureConfig) {
const selector_dg1 = Array(90).fill('0');
Object.entries(disclosures).forEach(([attribute, reveal]) => {
if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) {
return;
}
if (reveal) {
const [start, end] = attributeToPosition_ID[attribute as keyof typeof attributeToPosition_ID];
selector_dg1.fill('1', start, end + 1);
}
});
return selector_dg1;
}

View File

@@ -1,9 +1,9 @@
export type { AadhaarData, DocumentCategory, PassportData } from './types.js';
export type {
CertificateData,
PublicKeyDetailsECDSA,
PublicKeyDetailsRSA,
} from './certificate_parsing/dataStructure.js';
export type { DocumentCategory, PassportData } from './types.js';
export type { IdDocInput } from './passports/genMockIdDoc.js';
export type { PassportMetadata } from './passports/passport_parsing/parsePassportData.js';
export type { TEEPayload, TEEPayloadBase, TEEPayloadDisclose } from './proving.js';
@@ -19,6 +19,15 @@ export {
export { bigIntToString, formatEndpoint, hashEndpointWithScope, stringToBigInt } from './scope.js';
export { brutforceSignatureAlgorithmDsc } from './passports/passport_parsing/brutForceDscSignature.js';
export { buildSMT, getLeafCscaTree, getLeafDscTree } from './trees.js';
export {
calculateContentHash,
findStartPubKeyIndex,
generateCommitment,
generateNullifier,
inferDocumentCategory,
initPassportDataParsing,
} from './passports/passport.js';
export { isAadhaarDocument, isMRZDocument } from './types.js';
export {
calculateUserIdentifierHash,
customHasher,
@@ -36,14 +45,7 @@ export {
getPayload,
getWSDbRelayerUrl,
} from './proving.js';
export {
findStartPubKeyIndex,
generateCommitment,
generateNullifier,
initPassportDataParsing,
calculateContentHash,
inferDocumentCategory,
} from './passports/passport.js';
export { extractQRDataFields, getAadharRegistrationWindow } from './aadhaar/utils.js';
export { formatMrz } from './passports/format.js';
export { genAndInitMockPassportData } from './passports/genMockPassportData.js';
export {
@@ -57,6 +59,10 @@ export {
generateCircuitInputsRegisterForTests,
generateCircuitInputsVCandDisclose,
} from './circuits/generateInputs.js';
export {
generateTEEInputsAadhaarDisclose,
generateTEEInputsAadhaarRegister,
} from './circuits/registerInputs.js';
export { getCircuitNameFromPassportData } from './circuits/circuitsName.js';
export { getSKIPEM } from './csca.js';
export { initElliptic } from './certificate_parsing/elliptic.js';

View File

@@ -7,6 +7,8 @@ import forge from 'node-forge';
import type { hashAlgosTypes } from '../../constants/constants.js';
import { API_URL_STAGING } from '../../constants/constants.js';
import { countries } from '../../constants/countries.js';
import { convertByteArrayToBigInt, processQRData } from '../aadhaar/mockData.js';
import { extractQRDataFields } from '../aadhaar/utils.js';
import { getCurveForElliptic } from '../certificate_parsing/curves.js';
import type {
PublicKeyDetailsECDSA,
@@ -14,14 +16,18 @@ import type {
} from '../certificate_parsing/dataStructure.js';
import { parseCertificateSimple } from '../certificate_parsing/parseCertificateSimple.js';
import { getHashLen, hash } from '../hash.js';
import type { DocumentType, PassportData, SignatureAlgorithm } from '../types.js';
import type { AadhaarData, DocumentType, PassportData, SignatureAlgorithm } from '../types.js';
import { genDG1 } from './dg1.js';
import { formatAndConcatenateDataHashes, formatMrz, generateSignedAttr } from './format.js';
import { getMockDSC } from './getMockDSC.js';
import { initPassportDataParsing } from './passport.js';
import {
AADHAAR_MOCK_PRIVATE_KEY_PEM,
AADHAAR_MOCK_PUBLIC_KEY_PEM,
} from '../../mock_certificates/aadhaar/mockAadhaarCert.js';
export interface IdDocInput {
idType: 'mock_passport' | 'mock_id_card';
idType: 'mock_passport' | 'mock_id_card' | 'mock_aadhaar';
dgHashAlgo?: hashAlgosTypes;
eContentHashAlgo?: hashAlgosTypes;
signatureType?: SignatureAlgorithm;
@@ -32,6 +38,9 @@ export interface IdDocInput {
lastName?: string;
firstName?: string;
sex?: 'M' | 'F';
// Aadhaar-specific fields
pincode?: string; // - not disclosing this so not getting it in CreateMockScreen
state?: string;
}
const defaultIdDocInput: IdDocInput = {
@@ -43,19 +52,79 @@ const defaultIdDocInput: IdDocInput = {
birthDate: '900101',
expiryDate: '300101',
passportNumber: '123456789',
lastName: 'DOE',
firstName: 'JOHN',
lastName: undefined,
firstName: undefined,
sex: 'M',
// Aadhaar defaults
pincode: '110051',
state: 'Delhi',
};
// Generate mock Aadhaar document
function genMockAadhaarDoc(input: IdDocInput): AadhaarData {
const name = input.firstName
? `${input.firstName} ${input.lastName || ''}`.trim()
: generateRandomName();
const gender = input.sex === 'F' ? 'F' : 'M';
const pincode = input.pincode ?? '110051';
const state = input.state ?? 'Delhi';
const dateOfBirth = input.birthDate ?? '01-01-1990';
console.log('genMockAadhaarDoc', input);
console.log('dateOfBirth', dateOfBirth);
// Generate Aadhaar QR data using processQRData
const qrData = processQRData(
AADHAAR_MOCK_PRIVATE_KEY_PEM,
name,
dateOfBirth,
gender,
pincode,
state,
new Date().getTime().toString()
);
// Convert QR data to string format
const qrDataString = convertByteArrayToBigInt(qrData.qrDataBytes).toString();
console.log('qrDataString', qrDataString);
// Extract signature from the decoded data
const signatureBytes = qrData.decodedData.slice(
qrData.decodedData.length - 256,
qrData.decodedData.length
);
const signature = Array.from(signatureBytes);
console.log('qrData.extractedFields', qrData.extractedFields);
return {
documentType: input.idType as DocumentType,
documentCategory: 'aadhaar',
mock: true,
qrData: qrDataString,
extractedFields: qrData.extractedFields,
signature,
publicKey: AADHAAR_MOCK_PUBLIC_KEY_PEM,
photoHash: qrData.photoHash.toString(),
};
}
export function genMockIdDoc(
userInput: Partial<IdDocInput> = {},
mockDSC?: { dsc: string; privateKeyPem: string }
): PassportData {
): PassportData | AadhaarData {
if (userInput.idType === 'mock_aadhaar') {
return genMockAadhaarDoc(userInput as IdDocInput);
}
const mergedInput: IdDocInput = {
...defaultIdDocInput,
...userInput,
};
mergedInput.lastName = mergedInput.lastName ?? 'DOE';
mergedInput.firstName = mergedInput.firstName ?? 'JOHN';
let privateKeyPem: string, dsc: string;
if (mockDSC) {
dsc = mockDSC.dsc;
@@ -91,7 +160,7 @@ export function genMockIdDoc(
export function genMockIdDocAndInitDataParsing(userInput: Partial<IdDocInput> = {}) {
return initPassportDataParsing({
...genMockIdDoc(userInput),
...(genMockIdDoc(userInput) as PassportData),
});
}
@@ -116,6 +185,21 @@ export async function generateMockDSC(
return { privateKeyPem: data.data.privateKeyPem, dsc: data.data.dsc };
}
function generateRandomName(): string {
// Generate random letter combinations for first and last name
const generateRandomLetters = (length: number): string => {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return Array.from({ length }, () => letters[Math.floor(Math.random() * letters.length)]).join(
''
);
};
const firstName = generateRandomLetters(4 + Math.floor(Math.random() * 4)); // 4-7 letters
const lastName = generateRandomLetters(5 + Math.floor(Math.random() * 5)); // 5-9 letters
return `${firstName} ${lastName}`;
}
function generateRandomBytes(length: number): number[] {
// Generate numbers between -128 and 127 to match the existing signed byte format
return Array.from({ length }, () => Math.floor(Math.random() * 256) - 128);

View File

@@ -1,3 +1,4 @@
import { sha256 } from 'js-sha256';
import forge from 'node-forge';
import { poseidon5 } from 'poseidon-lite';
@@ -14,6 +15,7 @@ import {
n_dsc_4096,
n_dsc_ecdsa,
} from '../../constants/constants.js';
import { nullifierHash } from '../aadhaar/mockData.js';
import { bytesToBigDecimal, hexToDecimal, splitToWords } from '../bytes.js';
import type {
CertificateData,
@@ -29,10 +31,30 @@ import { findStartIndex, findStartIndexEC } from '../csca.js';
import { hash, packBytesAndPoseidon } from '../hash.js';
import { sha384_512Pad, shaPad } from '../shaPad.js';
import { getLeafDscTree } from '../trees.js';
import type { DocumentCategory, PassportData, SignatureAlgorithm } from '../types.js';
import type { DocumentCategory, IDDocument, PassportData, SignatureAlgorithm } from '../types.js';
import { AadhaarData, isAadhaarDocument, isMRZDocument } from '../types.js';
import { formatMrz } from './format.js';
import { parsePassportData } from './passport_parsing/parsePassportData.js';
import { sha256 } from 'js-sha256';
export function calculateContentHash(passportData: PassportData | AadhaarData): string {
if (isMRZDocument(passportData) && passportData.eContent) {
// eContent is likely a buffer or array, convert to string properly
const eContentStr =
typeof passportData.eContent === 'string'
? passportData.eContent
: JSON.stringify(passportData.eContent);
return sha256(eContentStr);
}
// For MRZ documents without eContent, hash core stable fields
const stableData = {
documentType: passportData.documentType,
data: isMRZDocument(passportData) ? passportData.mrz : passportData.qrData || '',
documentCategory: passportData.documentCategory,
};
return sha256(JSON.stringify(stableData));
}
export function extractRSFromSignature(signatureBytes: number[]): { r: string; s: string } {
const derSignature = Buffer.from(signatureBytes).toString('binary');
@@ -152,6 +174,10 @@ export function generateCommitment(
}
function getPassportSignature(passportData: PassportData, n: number, k: number): any {
// if (isAadhaarDocument(passportData)) {
// return splitToWords(BigInt(bytesToBigDecimal(passportData.signature)), n, k);
// }
const { signatureAlgorithm } = passportData.dsc_parsed;
if (signatureAlgorithm === 'ecdsa') {
const { r, s } = extractRSFromSignature(passportData.encryptedDigest);
@@ -163,7 +189,11 @@ function getPassportSignature(passportData: PassportData, n: number, k: number):
}
}
export function generateNullifier(passportData: PassportData) {
export function generateNullifier(passportData: IDDocument) {
if (isAadhaarDocument(passportData)) {
return nullifierHash(passportData.extractedFields);
}
const signedAttr_shaBytes = hash(
passportData.passportMetadata.signedAttrHashFunction,
Array.from(passportData.signedAttr),
@@ -285,6 +315,17 @@ export function getSignatureAlgorithmFullName(
}
}
export function inferDocumentCategory(documentType: string): DocumentCategory {
if (documentType.includes('passport')) {
return 'passport' as DocumentCategory;
} else if (documentType.includes('id')) {
return 'id_card' as DocumentCategory;
} else if (documentType.includes('aadhaar')) {
return 'aadhaar' as DocumentCategory;
}
return 'passport' as DocumentCategory; // fallback
}
/// @dev will bruteforce passport and dsc signature
export function initPassportDataParsing(passportData: PassportData, skiPem: any = null) {
const passportMetadata = parsePassportData(passportData, skiPem);
@@ -307,34 +348,3 @@ export function pad(hashFunction: (typeof hashAlgos)[number]) {
export function padWithZeroes(bytes: number[], length: number) {
return bytes.concat(new Array(length - bytes.length).fill(0));
}
export function calculateContentHash(passportData: PassportData): string {
if (passportData.eContent) {
// eContent is likely a buffer or array, convert to string properly
const eContentStr =
typeof passportData.eContent === 'string'
? passportData.eContent
: JSON.stringify(passportData.eContent);
return sha256(eContentStr);
}
// For documents without eContent (like aadhaar), hash core stable fields
const stableData = {
documentType: passportData.documentType,
data: passportData.mrz || '', // Use mrz for passports/IDs, could be other data for aadhaar
documentCategory: passportData.documentCategory,
};
return sha256(JSON.stringify(stableData));
}
export function inferDocumentCategory(documentType: string): DocumentCategory {
if (documentType.includes('passport')) {
return 'passport' as DocumentCategory;
} else if (documentType.includes('id')) {
return 'id_card' as DocumentCategory;
} else if (documentType.includes('aadhaar')) {
return 'aadhaar' as DocumentCategory;
}
return 'passport' as DocumentCategory; // fallback
}

View File

@@ -17,9 +17,17 @@ import { hash } from '../../utils/hash/sha.js';
import { formatMrz } from '../../utils/passports/format.js';
import { getLeafDscTree } from '../../utils/trees.js';
import {
computeCommitment,
computePackedCommitment,
nullifierHash,
processQRDataSimple,
} from '../aadhaar/mockData.js';
import {
AadhaarData,
AttestationIdHex,
type DeployedCircuits,
type DocumentCategory,
IDDocument,
type PassportData,
} from '../types.js';
import { generateCommitment, generateNullifier } from './passport.js';
@@ -33,8 +41,37 @@ export type PassportSupportStatus =
| 'dsc_circuit_not_supported'
| 'passport_supported';
export async function checkDocumentSupported(
function validateRegistrationCircuit(
passportData: IDDocument,
deployedCircuits: DeployedCircuits
): { isValid: boolean; circuitName: string | null } {
let circuitNameRegister = getCircuitNameFromPassportData(
passportData as PassportData,
'register'
);
const isValid =
circuitNameRegister &&
(deployedCircuits.REGISTER.includes(circuitNameRegister) ||
deployedCircuits.REGISTER_ID.includes(circuitNameRegister) ||
deployedCircuits.REGISTER_AADHAAR.includes(circuitNameRegister));
return { isValid: !!isValid, circuitName: circuitNameRegister };
}
function validateDscCircuit(
passportData: PassportData,
deployedCircuits: DeployedCircuits
): { isValid: boolean; circuitName: string | null } {
const circuitNameDsc = getCircuitNameFromPassportData(passportData, 'dsc');
const isValid =
circuitNameDsc &&
(deployedCircuits.DSC.includes(circuitNameDsc) ||
deployedCircuits.DSC_ID.includes(circuitNameDsc));
return { isValid: !!isValid, circuitName: circuitNameDsc };
}
export async function checkDocumentSupported(
passportData: IDDocument,
opts: {
getDeployedCircuits: (docCategory: DocumentCategory) => DeployedCircuits;
}
@@ -42,8 +79,20 @@ export async function checkDocumentSupported(
status: PassportSupportStatus;
details: string;
}> {
const deployedCircuits = opts.getDeployedCircuits(passportData.documentCategory);
if (passportData.documentCategory === 'aadhaar') {
const { isValid, circuitName } = validateRegistrationCircuit(passportData, deployedCircuits);
if (!isValid) {
return {
status: 'registration_circuit_not_supported',
details: circuitName,
};
}
return { status: 'passport_supported', details: circuitName };
}
const passportMetadata = passportData.passportMetadata;
const document: DocumentCategory = passportData.documentCategory;
if (!passportMetadata) {
console.warn('Passport metadata is null');
return { status: 'passport_metadata_missing', details: passportData.dsc };
@@ -52,36 +101,30 @@ export async function checkDocumentSupported(
console.warn('CSCA not found');
return { status: 'csca_not_found', details: passportData.dsc };
}
const circuitNameRegister = getCircuitNameFromPassportData(passportData, 'register');
const deployedCircuits = opts.getDeployedCircuits(passportData.documentCategory);
if (
!circuitNameRegister ||
!(
deployedCircuits.REGISTER.includes(circuitNameRegister) ||
deployedCircuits.REGISTER_ID.includes(circuitNameRegister)
)
) {
const { isValid: isRegisterValid, circuitName: registerCircuitName } =
validateRegistrationCircuit(passportData, deployedCircuits);
if (!isRegisterValid) {
return {
status: 'registration_circuit_not_supported',
details: circuitNameRegister,
details: registerCircuitName,
};
}
const circuitNameDsc = getCircuitNameFromPassportData(passportData, 'dsc');
if (
!circuitNameDsc ||
!(
deployedCircuits.DSC.includes(circuitNameDsc) ||
deployedCircuits.DSC_ID.includes(circuitNameDsc)
)
) {
console.warn('DSC circuit not supported:', circuitNameDsc);
return { status: 'dsc_circuit_not_supported', details: circuitNameDsc };
const { isValid: isDscValid, circuitName: dscCircuitName } = validateDscCircuit(
passportData as PassportData,
deployedCircuits
);
if (!isDscValid) {
console.warn('DSC circuit not supported:', dscCircuitName);
return { status: 'dsc_circuit_not_supported', details: dscCircuitName };
}
return { status: 'passport_supported', details: 'null' };
return { status: 'passport_supported', details: dscCircuitName };
}
export async function checkIfPassportDscIsInTree(
passportData: PassportData,
passportData: IDDocument,
dscTree: string
): Promise<boolean> {
const hashFunction = (a: bigint, b: bigint) => poseidon2([a, b]);
@@ -146,13 +189,58 @@ export function generateCommitmentInApp(
return { commitment_list, csca_list };
}
export async function isDocumentNullified(passportData: PassportData) {
export function generateCommitmentInAppAadhaar(
secret: string,
attestation_id: string,
passportData: AadhaarData,
alternativePublicKeys: Record<string, string>
) {
const nullifier = nullifierHash(passportData.extractedFields);
const packedCommitment = computePackedCommitment(passportData.extractedFields);
const { qrHash, photoHash } = processQRDataSimple(passportData.qrData);
const publicKey_list: string[] = [];
const commitment_list: string[] = [];
// For Aadhaar, we can also use the document's own public key
const allPublicKeys = {
document_public_key: passportData.publicKey,
...alternativePublicKeys,
};
for (const [keyName, publicKeyValue] of Object.entries(allPublicKeys)) {
try {
const commitment = computeCommitment(
BigInt(secret),
BigInt(qrHash),
nullifier,
packedCommitment,
photoHash
).toString();
publicKey_list.push(publicKeyValue);
commitment_list.push(commitment);
} catch (error) {
console.warn(`Failed to process public key for ${keyName}:`, error);
}
}
if (commitment_list.length === 0) {
console.error('No valid public keys found for Aadhaar');
}
return { commitment_list, publicKey_list };
}
export async function isDocumentNullified(passportData: IDDocument) {
const nullifier = generateNullifier(passportData);
const nullifierHex = `0x${BigInt(nullifier).toString(16)}`;
const attestationId =
passportData.documentCategory === 'passport'
? AttestationIdHex.passport
: AttestationIdHex.id_card;
: passportData.documentCategory === 'aadhaar'
? AttestationIdHex.aadhaar
: AttestationIdHex.id_card;
console.log('checking for nullifier', nullifierHex, attestationId);
const baseUrl = passportData.mock === false ? API_URL : API_URL_STAGING;
const controller = new AbortController();
@@ -181,17 +269,38 @@ export async function isDocumentNullified(passportData: PassportData) {
}
export async function isUserRegistered(
passportData: PassportData,
documentData: PassportData | AadhaarData,
secret: string,
getCommitmentTree: (docCategory: DocumentCategory) => string
) {
if (!passportData) {
if (!documentData) {
return false;
}
const attestationId =
passportData.documentCategory === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID;
const commitment = generateCommitment(secret, attestationId, passportData);
const document: DocumentCategory = passportData.documentCategory;
const document: DocumentCategory = documentData.documentCategory;
let commitment: string;
if (document === 'aadhaar') {
const aadhaarData = documentData as AadhaarData;
const nullifier = nullifierHash(aadhaarData.extractedFields);
const packedCommitment = computePackedCommitment(aadhaarData.extractedFields);
const { qrHash, photoHash } = processQRDataSimple(aadhaarData.qrData);
commitment = computeCommitment(
BigInt(secret),
BigInt(qrHash),
nullifier,
packedCommitment,
photoHash
).toString();
console.log('commitment', commitment);
} else {
const attestationId =
document === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID;
commitment = generateCommitment(secret, attestationId, documentData as PassportData);
}
const serializedTree = getCommitmentTree(document);
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree);
const index = tree.indexOf(BigInt(commitment));
@@ -199,7 +308,7 @@ export async function isUserRegistered(
}
export async function isUserRegisteredWithAlternativeCSCA(
passportData: PassportData,
passportData: IDDocument,
secret: string,
{
getCommitmentTree,
@@ -214,21 +323,56 @@ export async function isUserRegisteredWithAlternativeCSCA(
return { isRegistered: false, csca: null };
}
const document: DocumentCategory = passportData.documentCategory;
const alternativeCSCA = getAltCSCA(document);
const { commitment_list, csca_list } = generateCommitmentInApp(
secret,
document === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID,
passportData,
alternativeCSCA
);
let commitment_list: string[];
let csca_list: string[];
if (document === 'aadhaar') {
// For Aadhaar, use public keys from protocol store instead of CSCA
const publicKeys = getAltCSCA(document);
if (!publicKeys || Object.keys(publicKeys).length === 0) {
console.error('No public keys available for Aadhaar');
return { isRegistered: false, csca: null };
}
// Create alternative public keys object from protocol store
const alternativePublicKeys: Record<string, string> = {};
Object.entries(publicKeys).forEach(([key, value], index) => {
alternativePublicKeys[`public_key_${index}`] = value;
});
const result = generateCommitmentInAppAadhaar(
secret,
AttestationIdHex.aadhaar,
passportData as AadhaarData,
alternativePublicKeys
);
commitment_list = result.commitment_list;
csca_list = result.publicKey_list;
} else {
// For passport/id_card, use CSCA certificates
const alternativeCSCA = getAltCSCA(document);
const result = generateCommitmentInApp(
secret,
document === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID,
passportData as PassportData,
alternativeCSCA
);
commitment_list = result.commitment_list;
csca_list = result.csca_list;
}
if (commitment_list.length === 0) {
console.error('No valid CSCA certificates could be parsed from alternativeCSCA');
const errorMsg =
document === 'aadhaar'
? 'No valid public keys could be processed for Aadhaar'
: 'No valid CSCA certificates could be parsed from alternativeCSCA';
console.error(errorMsg);
return { isRegistered: false, csca: null };
}
const serializedTree = getCommitmentTree(document);
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree);
for (let i = 0; i < commitment_list.length; i++) {
const commitment = commitment_list[i];
const index = tree.indexOf(BigInt(commitment));
@@ -236,7 +380,12 @@ export async function isUserRegisteredWithAlternativeCSCA(
return { isRegistered: true, csca: csca_list[i] };
}
}
console.warn('None of the following CSCA correspond to the commitment:', csca_list);
const warnMsg =
document === 'aadhaar'
? `None of the following public keys correspond to the commitment for Aadhaar: ${csca_list}`
: `None of the following CSCA correspond to the commitment: ${csca_list}`;
console.warn(warnMsg);
return { isRegistered: false, csca: null };
}

View File

@@ -32,9 +32,9 @@ export const ec = new EC('p256');
// eslint-disable-next-line -- clientKey is created from ec so must be second
export const clientKey = ec.genKeyPair();
type RegisterSuffixes = '' | '_id';
type RegisterSuffixes = '' | '_id' | '_aadhaar';
type DscSuffixes = '' | '_id';
type DiscloseSuffixes = '' | '_id';
type DiscloseSuffixes = '' | '_id' | '_aadhaar';
type ProofTypes = 'register' | 'dsc' | 'disclose';
type RegisterProofType = `${Extract<ProofTypes, 'register'>}${RegisterSuffixes}`;
type DscProofType = `${Extract<ProofTypes, 'dsc'>}${DscSuffixes}`;
@@ -67,8 +67,14 @@ export function getPayload(
userDefinedData: string = ''
) {
if (circuitType === 'disclose') {
const type =
circuitName === 'vc_and_disclose'
? 'disclose'
: circuitName === 'vc_and_disclose_aadhaar'
? 'disclose_aadhaar'
: 'disclose_id';
const payload: TEEPayloadDisclose = {
type: circuitName === 'vc_and_disclose' ? 'disclose' : 'disclose_id',
type,
endpointType: endpointType,
endpoint: endpoint,
onchain: endpointType === 'celo' ? true : false,
@@ -81,8 +87,9 @@ export function getPayload(
};
return payload;
} else {
const type = circuitName === 'register_aadhaar' ? 'register_aadhaar' : circuitType;
const payload: TEEPayload = {
type: circuitType as RegisterProofType | DscProofType,
type: type as RegisterProofType | DscProofType,
onchain: true,
endpointType: endpointType,
circuit: {

View File

@@ -1,9 +1,30 @@
import type { ExtractedQRData } from './aadhaar/utils.js';
import type { CertificateData } from './certificate_parsing/dataStructure.js';
import type { PassportMetadata } from './passports/passport_parsing/parsePassportData.js';
// Base interface for common fields
interface BaseIDData {
documentType: DocumentType;
documentCategory: DocumentCategory;
mock: boolean;
dsc_parsed?: CertificateData;
csca_parsed?: CertificateData;
}
// Aadhaar document data
export interface AadhaarData extends BaseIDData {
documentCategory: 'aadhaar';
qrData: string;
extractedFields: ExtractedQRData; // All parsed Aadhaar fields
signature: number[];
publicKey: string;
photoHash?: string;
}
export type DeployedCircuits = {
REGISTER: string[];
REGISTER_ID: string[];
REGISTER_AADHAAR: string[];
DSC: string[];
DSC_ID: string[];
};
@@ -13,7 +34,7 @@ export interface DocumentCatalog {
selectedDocumentId?: string; // This is now a contentHash
}
export type DocumentCategory = 'passport' | 'id_card';
export type DocumentCategory = 'passport' | 'id_card' | 'aadhaar';
export interface DocumentMetadata {
id: string; // contentHash as ID for deduplication
@@ -24,7 +45,15 @@ export interface DocumentMetadata {
isRegistered?: boolean; // whether the document is registered onChain
}
export type DocumentType = 'passport' | 'id_card' | 'mock_passport' | 'mock_id_card';
export type DocumentType =
| 'passport'
| 'id_card'
| 'aadhaar'
| 'mock_passport'
| 'mock_id_card'
| 'mock_aadhaar';
export type IDDocument = AadhaarData | PassportData;
export type OfacTree = {
passportNoAndNationality: any;
@@ -32,7 +61,9 @@ export type OfacTree = {
nameAndYob: any;
};
export type PassportData = {
// Define the signature algorithm in "algorithm_hashfunction_domainPapameter_keyLength"
export interface PassportData extends BaseIDData {
documentCategory: 'passport' | 'id_card';
mrz: string;
dg1Hash?: number[];
dg2Hash?: number[];
@@ -42,12 +73,7 @@ export type PassportData = {
signedAttr: number[];
encryptedDigest: number[];
passportMetadata?: PassportMetadata;
dsc_parsed?: CertificateData;
csca_parsed?: CertificateData;
documentType: DocumentType;
documentCategory: DocumentCategory;
mock: boolean;
};
}
export type Proof = {
proof: {
@@ -119,6 +145,7 @@ export enum AttestationIdHex {
invalid = '0x0000000000000000000000000000000000000000000000000000000000000000',
passport = '0x0000000000000000000000000000000000000000000000000000000000000001',
id_card = '0x0000000000000000000000000000000000000000000000000000000000000002',
aadhaar = '0x0000000000000000000000000000000000000000000000000000000000000003',
}
export function castCSCAProof(proof: any): Proof {
@@ -131,3 +158,17 @@ export function castCSCAProof(proof: any): Proof {
pub_signals: proof.pub_signals,
};
}
export function isAadhaarDocument(
passportData: PassportData | AadhaarData
): passportData is AadhaarData {
return passportData.documentCategory === 'aadhaar';
}
export function isMRZDocument(
passportData: PassportData | AadhaarData
): passportData is PassportData {
return (
passportData.documentCategory === 'passport' || passportData.documentCategory === 'id_card'
);
}

View File

@@ -20,6 +20,8 @@ const entry = {
'src/constants/sampleDataHashes': 'src/constants/sampleDataHashes.ts',
// Granular utils exports
'src/utils/aadhaar/constants': 'src/utils/aadhaar/constants.ts',
'src/utils/aadhaar/utils': 'src/utils/aadhaar/utils.ts',
'src/utils/aadhaar/mockData': 'src/utils/aadhaar/mockData.ts',
'src/utils/attest': 'src/utils/attest.ts',
'src/utils/hash': 'src/utils/hash.ts',
'src/utils/bytes': 'src/utils/bytes.ts',

View File

@@ -1,10 +1,17 @@
[
{
"name": "REGISTERED_COMMITMENT",
"signature": "REGISTERED_COMMITMENT()",
"selector": "0x034acfcc",
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
"line": 134
},
{
"name": "REGISTERED_COMMITMENT",
"signature": "REGISTERED_COMMITMENT()",
"selector": "0x034acfcc",
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
"line": 141
"line": 142
},
{
"name": "REGISTERED_COMMITMENT",
@@ -18,42 +25,42 @@
"signature": "InvalidProof()",
"selector": "0x09bde339",
"file": "contracts/example/Airdrop.sol",
"line": 54
"line": 57
},
{
"name": "NoVerifierSet",
"signature": "NoVerifierSet()",
"selector": "0x0ee78d58",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 110
"line": 136
},
{
"name": "InvalidAttestationId",
"signature": "InvalidAttestationId()",
"selector": "0x12ec75fe",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 142
"line": 168
},
{
"name": "InvalidAttestationId",
"signature": "InvalidAttestationId()",
"selector": "0x12ec75fe",
"file": "contracts/libraries/CustomVerifier.sol",
"line": 11
"line": 10
},
{
"name": "RegistrationNotOpen",
"signature": "RegistrationNotOpen()",
"selector": "0x153745d3",
"file": "contracts/example/Airdrop.sol",
"line": 63
"line": 66
},
{
"name": "InvalidDscProof",
"signature": "InvalidDscProof()",
"selector": "0x1644e049",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 122
"line": 148
},
{
"name": "InvalidYearRange",
@@ -76,12 +83,19 @@
"file": "contracts/IdentityVerificationHubImplV1.sol",
"line": 166
},
{
"name": "HUB_ADDRESS_ZERO",
"signature": "HUB_ADDRESS_ZERO()",
"selector": "0x22697ffa",
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
"line": 138
},
{
"name": "RegisteredNullifier",
"signature": "RegisteredNullifier()",
"selector": "0x22cbc6a2",
"file": "contracts/example/Airdrop.sol",
"line": 78
"line": 81
},
{
"name": "InvalidMonthRange",
@@ -95,7 +109,7 @@
"signature": "UserIdentifierAlreadyRegistered()",
"selector": "0x29393238",
"file": "contracts/example/Airdrop.sol",
"line": 75
"line": 78
},
{
"name": "InvalidFieldElement",
@@ -104,26 +118,40 @@
"file": "contracts/libraries/Formatter.sol",
"line": 13
},
{
"name": "InvalidPubkey",
"signature": "InvalidPubkey()",
"selector": "0x422cc3b7",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 196
},
{
"name": "InvalidOlderThan",
"signature": "InvalidOlderThan()",
"selector": "0x49aecbc2",
"file": "contracts/libraries/CustomVerifier.sol",
"line": 14
"line": 13
},
{
"name": "InvalidDscCommitmentRoot",
"signature": "InvalidDscCommitmentRoot()",
"selector": "0x4cb305bb",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 134
"line": 160
},
{
"name": "HUB_NOT_SET",
"signature": "HUB_NOT_SET()",
"selector": "0x4ffa9998",
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
"line": 130
},
{
"name": "HUB_NOT_SET",
"signature": "HUB_NOT_SET()",
"selector": "0x4ffa9998",
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
"line": 137
"line": 138
},
{
"name": "HUB_NOT_SET",
@@ -151,6 +179,13 @@
"signature": "UserIdentifierAlreadyMinted()",
"selector": "0x5dd09265",
"file": "contracts/example/SelfIdentityERC721.sol",
"line": 51
},
{
"name": "UserIdentifierAlreadyMinted",
"signature": "UserIdentifierAlreadyMinted()",
"selector": "0x5dd09265",
"file": "contracts/example/SelfPassportERC721.sol",
"line": 48
},
{
@@ -158,49 +193,49 @@
"signature": "InvalidOfacCheck()",
"selector": "0x5fb542f4",
"file": "contracts/libraries/CustomVerifier.sol",
"line": 12
"line": 11
},
{
"name": "CrossChainIsNotSupportedYet",
"signature": "CrossChainIsNotSupportedYet()",
"selector": "0x61296fbb",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 150
"line": 176
},
{
"name": "AlreadyClaimed",
"signature": "AlreadyClaimed()",
"selector": "0x646cf558",
"file": "contracts/example/Airdrop.sol",
"line": 57
"line": 60
},
{
"name": "AlreadyClaimed",
"signature": "AlreadyClaimed()",
"selector": "0x646cf558",
"file": "contracts/example/HappyBirthday.sol",
"line": 64
"line": 67
},
{
"name": "InputTooShort",
"signature": "InputTooShort()",
"selector": "0x65ec0cf1",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 154
"line": 180
},
{
"name": "InvalidRegisterProof",
"signature": "InvalidRegisterProof()",
"selector": "0x67b61dc7",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 118
"line": 144
},
{
"name": "RegistrationNotClosed",
"signature": "RegistrationNotClosed()",
"selector": "0x697e379b",
"file": "contracts/example/Airdrop.sol",
"line": 66
"line": 69
},
{
"name": "INVALID_DSC_PROOF",
@@ -214,7 +249,7 @@
"signature": "ClaimNotOpen()",
"selector": "0x6b687806",
"file": "contracts/example/Airdrop.sol",
"line": 69
"line": 72
},
{
"name": "INVALID_OFAC",
@@ -223,12 +258,19 @@
"file": "contracts/IdentityVerificationHubImplV1.sol",
"line": 146
},
{
"name": "InvalidUidaiTimestamp",
"signature": "InvalidUidaiTimestamp()",
"selector": "0x72b3dac6",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 200
},
{
"name": "InvalidForbiddenCountries",
"signature": "InvalidForbiddenCountries()",
"selector": "0x82cba848",
"file": "contracts/libraries/CustomVerifier.sol",
"line": 13
"line": 12
},
{
"name": "InsufficientCharcodeLen",
@@ -242,7 +284,7 @@
"signature": "InsufficientCharcodeLen()",
"selector": "0x86d41225",
"file": "contracts/libraries/CircuitAttributeHandlerV2.sol",
"line": 17
"line": 16
},
{
"name": "InsufficientCharcodeLen",
@@ -277,7 +319,7 @@
"signature": "InvalidCscaRoot()",
"selector": "0x8f1b44c7",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 138
"line": 164
},
{
"name": "INVALID_REGISTER_PROOF",
@@ -291,14 +333,14 @@
"signature": "UserContextDataTooShort()",
"selector": "0x94ec3503",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 158
"line": 184
},
{
"name": "NotWithinBirthdayWindow",
"signature": "NotWithinBirthdayWindow()",
"selector": "0x9b7983d7",
"file": "contracts/example/HappyBirthday.sol",
"line": 63
"line": 66
},
{
"name": "INVALID_CSCA_ROOT",
@@ -319,7 +361,7 @@
"signature": "ConfigNotSet()",
"selector": "0xace124bc",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 166
"line": 192
},
{
"name": "InvalidDateLength",
@@ -328,12 +370,19 @@
"file": "contracts/libraries/Formatter.sol",
"line": 9
},
{
"name": "ONLY_HUB_CAN_ACCESS",
"signature": "ONLY_HUB_CAN_ACCESS()",
"selector": "0xba0318cb",
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
"line": 132
},
{
"name": "ONLY_HUB_CAN_ACCESS",
"signature": "ONLY_HUB_CAN_ACCESS()",
"selector": "0xba0318cb",
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
"line": 139
"line": 140
},
{
"name": "ONLY_HUB_CAN_ACCESS",
@@ -354,14 +403,28 @@
"signature": "NotRegistered(address)",
"selector": "0xbfc6c337",
"file": "contracts/example/Airdrop.sol",
"line": 60
"line": 63
},
{
"name": "InvalidOfacRoots",
"signature": "InvalidOfacRoots()",
"selector": "0xc67a44d2",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 208
},
{
"name": "EXPIRY_IN_PAST",
"signature": "EXPIRY_IN_PAST()",
"selector": "0xca5d75dd",
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
"line": 136
},
{
"name": "CurrentDateNotInValidRange",
"signature": "CurrentDateNotInValidRange()",
"selector": "0xcf46551c",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 114
"line": 140
},
{
"name": "INVALID_VC_AND_DISCLOSE_PROOF",
@@ -370,12 +433,19 @@
"file": "contracts/IdentityVerificationHubImplV1.sol",
"line": 158
},
{
"name": "AttestationIdMismatch",
"signature": "AttestationIdMismatch()",
"selector": "0xd7ca437d",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 204
},
{
"name": "InvalidVcAndDiscloseProof",
"signature": "InvalidVcAndDiscloseProof()",
"selector": "0xda7bd3a6",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 126
"line": 152
},
{
"name": "INVALID_REVEALED_DATA_TYPE",
@@ -389,14 +459,14 @@
"signature": "ScopeMismatch()",
"selector": "0xe7bee380",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 146
"line": 172
},
{
"name": "InvalidUserIdentifierInProof",
"signature": "InvalidUserIdentifierInProof()",
"selector": "0xebbcc178",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 162
"line": 188
},
{
"name": "CURRENT_DATE_NOT_IN_VALID_RANGE",
@@ -410,13 +480,20 @@
"signature": "InvalidUserIdentifier()",
"selector": "0xf0c426db",
"file": "contracts/example/Airdrop.sol",
"line": 72
"line": 75
},
{
"name": "InvalidUserIdentifier",
"signature": "InvalidUserIdentifier()",
"selector": "0xf0c426db",
"file": "contracts/example/SelfIdentityERC721.sol",
"line": 52
},
{
"name": "InvalidUserIdentifier",
"signature": "InvalidUserIdentifier()",
"selector": "0xf0c426db",
"file": "contracts/example/SelfPassportERC721.sol",
"line": 49
},
{
@@ -431,13 +508,13 @@
"signature": "InvalidIdentityCommitmentRoot()",
"selector": "0xf53393a7",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 130
"line": 156
},
{
"name": "LengthMismatch",
"signature": "LengthMismatch()",
"selector": "0xff633a38",
"file": "contracts/IdentityVerificationHubImplV2.sol",
"line": 106
"line": 132
}
]

View File

@@ -1,26 +1,26 @@
require 'json'
require "json"
# Handle both local development and published package scenarios
package_json_path = File.join(__dir__, '..', 'package.json')
package_json_path = File.join(__dir__, "..", "package.json")
if File.exist?(package_json_path)
package = JSON.parse(File.read(package_json_path))
else
# Fallback for when package.json is not found
package = {
'version' => '0.1.0',
'description' => 'Self Mobile SDK Alpha'
"version" => "0.1.0",
"description" => "Self Mobile SDK Alpha",
}
end
Pod::Spec.new do |s|
s.name = "mobile-sdk-alpha"
s.version = package['version']
s.summary = package['description']
s.homepage = "https://github.com/selfxyz/self"
s.license = "BUSL-1.1"
s.author = { "Self" => "team@self.xyz" }
s.platform = :ios, "13.0"
s.source = { :path => "." }
s.name = "mobile-sdk-alpha"
s.version = package["version"]
s.summary = package["description"]
s.homepage = "https://github.com/selfxyz/self"
s.license = "BUSL-1.1"
s.author = { "Self" => "support@self.xyz" }
s.platform = :ios, "13.0"
s.source = { :path => "." }
s.source_files = "ios/**/*.{h,m,mm,swift}"
s.public_header_files = "ios/**/*.h"
@@ -29,13 +29,12 @@ Pod::Spec.new do |s|
s.dependency "NFCPassportReader"
s.pod_target_xcconfig = {
'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/Headers/Public/React-Core"',
'DEFINES_MODULE' => 'YES',
'SWIFT_INCLUDE_PATHS' => '$(PODS_ROOT)/mobile-sdk-alpha/ios'
"HEADER_SEARCH_PATHS" => '"$(PODS_ROOT)/Headers/Public/React-Core"',
"DEFINES_MODULE" => "YES",
"SWIFT_INCLUDE_PATHS" => "$(PODS_ROOT)/mobile-sdk-alpha/ios",
}
# Ensure iOS files are properly linked
s.platform = :ios, "13.0"
s.requires_arc = true
end

View File

@@ -2,9 +2,47 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
export const AadhaarEvents = {
UPLOAD_SCREEN_OPENED: 'Aadhaar: Upload Screen Opened',
QR_UPLOAD_REQUESTED: 'Aadhaar: QR Upload Requested',
QR_UPLOAD_SUCCESS: 'Aadhaar: QR Upload Success',
QR_UPLOAD_FAILED: 'Aadhaar: QR Upload Failed',
PERMISSION_MODAL_OPENED: 'Aadhaar: Permission Modal Opened',
PERMISSION_MODAL_DISMISSED: 'Aadhaar: Permission Modal Dismissed',
PERMISSION_SETTINGS_OPENED: 'Aadhaar: Permission Settings Opened',
PROCESSING_STARTED: 'Aadhaar: Processing Started',
// Error-specific events
QR_CODE_EXPIRED: 'Aadhaar: QR Code Expired',
QR_CODE_INVALID_FORMAT: 'Aadhaar: QR Code Invalid Format',
QR_CODE_MISSING_FIELDS: 'Aadhaar: QR Code Missing Required Fields',
QR_CODE_PARSE_FAILED: 'Aadhaar: QR Code Parse Failed',
PHOTO_LIBRARY_UNAVAILABLE: 'Aadhaar: Photo Library Unavailable',
USER_CANCELLED_SELECTION: 'Aadhaar: User Cancelled Photo Selection',
// Validation events
TIMESTAMP_VALIDATION_STARTED: 'Aadhaar: Timestamp Validation Started',
TIMESTAMP_VALIDATION_FAILED: 'Aadhaar: Timestamp Validation Failed',
TIMESTAMP_VALIDATION_SUCCESS: 'Aadhaar: Timestamp Validation Success',
// Data processing events
QR_DATA_EXTRACTION_STARTED: 'Aadhaar: QR Data Extraction Started',
QR_DATA_EXTRACTION_SUCCESS: 'Aadhaar: QR Data Extraction Success',
DATA_STORAGE_STARTED: 'Aadhaar: Data Storage Started',
DATA_STORAGE_SUCCESS: 'Aadhaar: Data Storage Success',
// Screen interaction events
UPLOAD_BUTTON_DISABLED: 'Aadhaar: Upload Button Disabled',
UPLOAD_BUTTON_ENABLED: 'Aadhaar: Upload Button Enabled',
// Error recovery events
ERROR_SCREEN_NAVIGATED: 'Aadhaar: Error Screen Navigated',
RETRY_BUTTON_PRESSED: 'Aadhaar: Retry Button Pressed',
HELP_BUTTON_PRESSED: 'Aadhaar: Help Button Pressed',
// Success screen events
CONTINUE_TO_REGISTRATION_PRESSED: 'Aadhaar: Continue to Registration Pressed',
};
export const AppEvents = {
DISMISS_PRIVACY_DISCLAIMER: 'App: Dismiss Privacy Disclaimer',
GET_STARTED: 'App: Get Started',
GET_STARTED_BIOMETRIC: 'App: Get Started - Biometric ID',
GET_STARTED_AADHAAR: 'App: Get Started - Aadhaar',
SUPPORTED_BIOMETRIC_IDS: 'App: Supported Biometric IDs',
UPDATE_MODAL_CLOSED: 'App: Update Modal Closed',
UPDATE_MODAL_OPENED: 'App: Update Modal Opened',
@@ -47,6 +85,7 @@ export const BackupEvents = {
export const DocumentEvents = {
ADD_NEW_MOCK_SELECTED: 'Document: Add New Document via Mock',
ADD_NEW_SCAN_SELECTED: 'Document: Add New Document via Scan',
ADD_NEW_AADHAAR_SELECTED: 'Document: Add Aadhaar',
DOCUMENT_DELETED: 'Document: Document Deleted',
DOCUMENT_SELECTED: 'Document: Document Selected',
DOCUMENTS_FETCHED: 'Document: Documents Fetched',

View File

@@ -3,13 +3,15 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import {
AadhaarData,
brutforceSignatureAlgorithmDsc,
isMRZDocument,
parseCertificateSimple,
PublicKeyDetailsECDSA,
PublicKeyDetailsRSA,
} from '@selfxyz/common';
import { calculateContentHash, inferDocumentCategory } from '@selfxyz/common/utils';
import { DocumentMetadata, PassportData } from '@selfxyz/common/utils/types';
import { DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
import { SelfClient } from '../types/public';
@@ -38,11 +40,11 @@ export async function clearPassportData(selfClient: SelfClient) {
export const getAllDocuments = async (
selfClient: SelfClient,
): Promise<{
[documentId: string]: { data: PassportData; metadata: DocumentMetadata };
[documentId: string]: { data: IDDocument; metadata: DocumentMetadata };
}> => {
const catalog = await selfClient.loadDocumentCatalog();
const allDocs: {
[documentId: string]: { data: PassportData; metadata: DocumentMetadata };
[documentId: string]: { data: IDDocument; metadata: DocumentMetadata };
} = {};
for (const metadata of catalog.documents) {
@@ -77,7 +79,7 @@ export const hasAnyValidRegisteredDocument = async (client: SelfClient): Promise
export const loadSelectedDocument = async (
selfClient: SelfClient,
): Promise<{
data: PassportData;
data: IDDocument;
metadata: DocumentMetadata;
} | null> => {
const catalog = await selfClient.loadDocumentCatalog();
@@ -122,11 +124,10 @@ export async function markCurrentDocumentAsRegistered(selfClient: SelfClient): P
}
}
export async function reStorePassportDataWithRightCSCA(
selfClient: SelfClient,
passportData: PassportData,
csca: string,
) {
export async function reStorePassportDataWithRightCSCA(selfClient: SelfClient, passportData: IDDocument, csca: string) {
if (passportData.documentCategory === 'aadhaar') {
return;
}
const cscaInCurrentPassporData = passportData.passportMetadata?.csca;
if (!(csca === cscaInCurrentPassporData)) {
const cscaParsed = parseCertificateSimple(csca);
@@ -156,7 +157,7 @@ export async function reStorePassportDataWithRightCSCA(
export async function storeDocumentWithDeduplication(
selfClient: SelfClient,
passportData: PassportData,
passportData: IDDocument,
): Promise<string> {
const contentHash = calculateContentHash(passportData);
const catalog = await selfClient.loadDocumentCatalog();
@@ -181,11 +182,12 @@ export async function storeDocumentWithDeduplication(
await selfClient.saveDocument(contentHash, passportData);
// Add to catalog
const docType = passportData.documentType;
const metadata: DocumentMetadata = {
id: contentHash,
documentType: passportData.documentType,
documentCategory: passportData.documentCategory || inferDocumentCategory(passportData.documentType),
data: passportData.mrz || '', // Store MRZ for passports/IDs, relevant data for aadhaar
documentType: docType,
documentCategory: passportData.documentCategory || inferDocumentCategory(docType),
data: isMRZDocument(passportData) ? passportData.mrz : (passportData as AadhaarData).qrData || '',
mock: passportData.mock || false,
isRegistered: false,
};
@@ -198,7 +200,7 @@ export async function storeDocumentWithDeduplication(
return contentHash;
}
export async function storePassportData(selfClient: SelfClient, passportData: PassportData) {
export async function storePassportData(selfClient: SelfClient, passportData: IDDocument) {
await storeDocumentWithDeduplication(selfClient, passportData);
}

View File

@@ -108,7 +108,7 @@ export { reactNativeScannerAdapter } from './adapters/react-native/scanner';
export { scanQRProof } from './qr';
export { useProtocolStore } from './stores/protocolStore';
export { useProtocolStore, useSelfAppStore } from './stores';
// Error handling
export { webScannerShim } from './adapters/web/shims';

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { IdDocInput } from '@selfxyz/common/utils';
import type { IdDocInput, PassportData } from '@selfxyz/common/utils';
import { getSKIPEM } from '@selfxyz/common/utils/csca';
import { generateMockDSC, genMockIdDoc, initPassportDataParsing } from '@selfxyz/common/utils/passports';
@@ -12,17 +12,28 @@ export interface GenerateMockDocumentOptions {
isInOfacList: boolean;
selectedAlgorithm: string;
selectedCountry: string;
selectedDocumentType: 'mock_passport' | 'mock_id_card';
selectedDocumentType: 'mock_passport' | 'mock_id_card' | 'mock_aadhaar';
}
const formatDateToYYMMDD = (date: Date): string => {
return (date.toISOString().slice(2, 4) + date.toISOString().slice(5, 7) + date.toISOString().slice(8, 10)).toString();
};
const getBirthDateFromAge = (age: number): string => {
// for aadhar
const formatDateToDDMMYYYY = (date: Date): string => {
return (
date.toISOString().slice(8, 10) +
'-' +
date.toISOString().slice(5, 7) +
'-' +
date.toISOString().slice(0, 4)
).toString();
};
const getBirthDateFromAge = (age: number, format: 'YYMMDD' | 'DDMMYYYY' = 'YYMMDD'): string => {
const date = new Date();
date.setFullYear(date.getFullYear() - age);
return formatDateToYYMMDD(date);
return format === 'YYMMDD' ? formatDateToYYMMDD(date) : formatDateToDDMMYYYY(date);
};
const getExpiryDateFromYears = (years: number): string => {
@@ -59,6 +70,23 @@ export async function generateMockDocument({
passportNumber: randomPassportNumber,
};
if (selectedDocumentType === 'mock_aadhaar') {
idDocInput.birthDate = getBirthDateFromAge(age, 'DDMMYYYY');
if (isInOfacList) {
idDocInput.lastName = 'HENAO MONTOYA';
idDocInput.firstName = 'ARCANGEL DE JESUS';
idDocInput.birthDate = '07-10-1954';
}
const result = genMockIdDoc(idDocInput);
if ('qrData' in result) {
console.log('Generated Aadhaar qrData:', result.qrData);
console.log('Generated Aadhaar extractedFields:', result.extractedFields);
}
return result;
}
let dobForGeneration: string;
if (isInOfacList) {
dobForGeneration = '541007';
@@ -78,7 +106,7 @@ export async function generateMockDocument({
rawMockData = genMockIdDoc(idDocInput);
}
const skiPem = await getSKIPEM('staging');
return initPassportDataParsing(rawMockData, skiPem);
return initPassportDataParsing(rawMockData as PassportData, skiPem);
}
export const signatureAlgorithmToStrictSignatureAlgorithm = {

View File

@@ -19,6 +19,8 @@ import {
IDENTITY_TREE_URL_ID_CARD,
IDENTITY_TREE_URL_STAGING,
IDENTITY_TREE_URL_STAGING_ID_CARD,
TREE_URL,
TREE_URL_STAGING,
} from '@selfxyz/common/constants';
import { fetchOfacTrees } from '@selfxyz/common/utils/ofac';
import type { DeployedCircuits, OfacTree } from '@selfxyz/common/utils/types';
@@ -58,6 +60,19 @@ interface ProtocolState {
fetch_all: (environment: 'prod' | 'stg', ski: string) => Promise<void>;
fetch_ofac_trees: (environment: 'prod' | 'stg') => Promise<void>;
};
aadhaar: {
commitment_tree: any;
public_keys: string[] | null;
deployed_circuits: DeployedCircuits | null;
circuits_dns_mapping: any;
ofac_trees: OfacTree | null;
fetch_deployed_circuits: (environment: 'prod' | 'stg') => Promise<void>;
fetch_circuits_dns_mapping: (environment: 'prod' | 'stg') => Promise<void>;
fetch_public_keys: (environment: 'prod' | 'stg') => Promise<void>;
fetch_identity_tree: (environment: 'prod' | 'stg') => Promise<void>;
fetch_all: (environment: 'prod' | 'stg') => Promise<void>;
fetch_ofac_trees: (environment: 'prod' | 'stg') => Promise<void>;
};
}
export const useProtocolStore = create<ProtocolState>((set, get) => ({
@@ -318,4 +333,111 @@ export const useProtocolStore = create<ProtocolState>((set, get) => ({
}
},
},
aadhaar: {
commitment_tree: null,
public_keys: null,
deployed_circuits: null,
circuits_dns_mapping: null,
ofac_trees: null,
fetch_all: async (environment: 'prod' | 'stg') => {
try {
await Promise.all([
get().aadhaar.fetch_deployed_circuits(environment),
get().aadhaar.fetch_circuits_dns_mapping(environment),
get().aadhaar.fetch_public_keys(environment),
get().aadhaar.fetch_identity_tree(environment),
get().aadhaar.fetch_ofac_trees(environment),
]);
} catch (error) {
console.error(`Failed fetching Aadhaar data for ${environment}:`, error);
throw error; // Re-throw to let proving machine handle it
}
},
fetch_deployed_circuits: async (environment: 'prod' | 'stg') => {
const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/deployed-circuits`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error fetching ${url}! status: ${response.status}`);
}
const responseText = await response.text();
const data = JSON.parse(responseText);
set({ aadhaar: { ...get().aadhaar, deployed_circuits: data.data } });
},
fetch_circuits_dns_mapping: async (environment: 'prod' | 'stg') => {
const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/circuit-dns-mapping-gcp`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error fetching ${url}! status: ${response.status}`);
}
const responseText = await response.text();
const data = JSON.parse(responseText);
set({
aadhaar: { ...get().aadhaar, circuits_dns_mapping: data.data },
});
},
fetch_public_keys: async (environment: 'prod' | 'stg') => {
const url = environment === 'prod' ? `${TREE_URL}/aadhaar-pubkeys` : `${TREE_URL_STAGING}/aadhaar-pubkeys`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error fetching ${url}! status: ${response.status}`);
}
const responseText = await response.text();
const data = JSON.parse(responseText);
set({ aadhaar: { ...get().aadhaar, public_keys: data.data } });
},
fetch_identity_tree: async (environment: 'prod' | 'stg') => {
const url = `${environment === 'prod' ? TREE_URL : TREE_URL_STAGING}/identity-aadhaar`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error fetching ${url}! status: ${response.status}`);
}
const responseText = await response.text();
const data = JSON.parse(responseText);
set({ aadhaar: { ...get().aadhaar, commitment_tree: data.data } });
} catch (error) {
console.error(`Failed fetching Aadhaar identity tree from ${url}:`, error);
}
},
fetch_ofac_trees: async (environment: 'prod' | 'stg') => {
const baseUrl = environment === 'prod' ? TREE_URL : TREE_URL_STAGING;
const nameDobUrl = `${baseUrl}/ofac/name-dob-aadhaar`;
const nameYobUrl = `${baseUrl}/ofac/name-yob-aadhaar`;
try {
const fetchTree = async (url: string): Promise<any> => {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`HTTP error fetching ${url}! status: ${res.status}`);
}
const responseData = await res.json();
if (responseData && typeof responseData === 'object' && 'status' in responseData) {
if (responseData.status !== 'success' || !responseData.data) {
throw new Error(`Failed to fetch tree from ${url}: ${responseData.message || 'Invalid response format'}`);
}
return responseData.data;
}
return responseData;
};
const [nameDobData, nameYobData] = await Promise.all([fetchTree(nameDobUrl), fetchTree(nameYobUrl)]);
set({
aadhaar: {
...get().aadhaar,
ofac_trees: {
passportNoAndNationality: null,
nameAndDob: nameDobData,
nameAndYob: nameYobData,
},
},
});
} catch (error) {
console.error('Failed fetching Aadhaar OFAC trees:', error);
set({ aadhaar: { ...get().aadhaar, ofac_trees: null } });
}
},
},
}));

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { DocumentCatalog, PassportData } from '@selfxyz/common/utils/types';
import type { DocumentCatalog, IDDocument, PassportData } from '@selfxyz/common/utils/types';
import { SDKEvent, SDKEventMap } from './events';
@@ -153,8 +153,8 @@ export interface DocumentsAdapter {
loadDocumentCatalog(): Promise<DocumentCatalog>;
saveDocumentCatalog(catalog: DocumentCatalog): Promise<void>;
loadDocumentById(id: string): Promise<PassportData | null>;
saveDocument(id: string, passportData: PassportData): Promise<void>;
loadDocumentById(id: string): Promise<IDDocument | null>;
saveDocument(id: string, passportData: IDDocument): Promise<void>;
deleteDocument(id: string): Promise<void>;
}
@@ -171,8 +171,8 @@ export interface SelfClient {
loadDocumentCatalog(): Promise<DocumentCatalog>;
saveDocumentCatalog(catalog: DocumentCatalog): Promise<void>;
loadDocumentById(id: string): Promise<PassportData | null>;
saveDocument(id: string, passportData: PassportData): Promise<void>;
loadDocumentById(id: string): Promise<IDDocument | null>;
saveDocument(id: string, passportData: IDDocument): Promise<void>;
deleteDocument(id: string): Promise<void>;
}