mirror of
https://github.com/selfxyz/self.git
synced 2026-01-08 22:28:11 -05:00
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:
committed by
GitHub
parent
664be08e12
commit
2df4dc4619
@@ -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"
|
||||
|
||||
@@ -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$''',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?)
|
||||
|
||||
@@ -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>
|
||||
|
||||
152
app/ios/PhotoLibraryQRScannerViewController.swift
Normal file
152
app/ios/PhotoLibraryQRScannerViewController.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,6 @@
|
||||
|
||||
RCT_EXTERN_METHOD(scanQRCode:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
RCT_EXTERN_METHOD(scanQRCodeFromPhotoLibrary:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
@end
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
114
app/src/components/NavBar/AadhaarNavBar.tsx
Normal file
114
app/src/components/NavBar/AadhaarNavBar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
BIN
app/src/images/512w.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
3
app/src/images/blue_check.svg
Normal file
3
app/src/images/blue_check.svg
Normal 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 |
9
app/src/images/icons/aadhaar.svg
Normal file
9
app/src/images/icons/aadhaar.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 92 KiB |
3
app/src/images/warning.svg
Normal file
3
app/src/images/warning.svg
Normal 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 |
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -124,6 +124,7 @@ function ParameterSection({
|
||||
|
||||
const items = [
|
||||
'DevSettings',
|
||||
'AadhaarUpload',
|
||||
'DevFeatureFlags',
|
||||
'DevHapticFeedback',
|
||||
'DevPrivateKey',
|
||||
|
||||
124
app/src/screens/document/aadhaar/AadhaarUploadErrorScreen.tsx
Normal file
124
app/src/screens/document/aadhaar/AadhaarUploadErrorScreen.tsx
Normal 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;
|
||||
330
app/src/screens/document/aadhaar/AadhaarUploadScreen.tsx
Normal file
330
app/src/screens/document/aadhaar/AadhaarUploadScreen.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`],
|
||||
|
||||
@@ -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, {
|
||||
|
||||
60
app/src/utils/qrScanner.ts
Normal file
60
app/src/utils/qrScanner.ts
Normal 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();
|
||||
};
|
||||
@@ -7,6 +7,7 @@ describe('navigation', () => {
|
||||
const navigationScreens = require('@/navigation').navigationScreens;
|
||||
const listOfScreens = Object.keys(navigationScreens).sort();
|
||||
expect(listOfScreens).toEqual([
|
||||
'AadhaarUpload',
|
||||
'AccountRecovery',
|
||||
'AccountRecoveryChoice',
|
||||
'AccountVerifiedSuccess',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
56
common/src/mock_certificates/aadhaar/mockAadhaarCert.ts
Normal file
56
common/src/mock_certificates/aadhaar/mockAadhaarCert.ts
Normal 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-----
|
||||
`;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user