mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
Merge pull request #1646 from selfxyz/release/staging-2026-01-23
Release to Staging - 2026-01-23
This commit is contained in:
1
.github/workflows/circuits-build.yml
vendored
1
.github/workflows/circuits-build.yml
vendored
@@ -208,6 +208,7 @@ jobs:
|
||||
./circuits/scripts/build/build_cpp.sh register
|
||||
./circuits/scripts/build/build_cpp.sh register_id
|
||||
./circuits/scripts/build/build_cpp.sh register_aadhaar
|
||||
./circuits/scripts/build/build_cpp.sh register_kyc
|
||||
./circuits/scripts/build/build_cpp.sh disclose
|
||||
./circuits/scripts/build/build_cpp.sh dsc
|
||||
fi
|
||||
|
||||
326
.github/workflows/mobile-e2e.yml
vendored
326
.github/workflows/mobile-e2e.yml
vendored
@@ -15,6 +15,9 @@ env:
|
||||
# Disable Maestro analytics in CI
|
||||
MAESTRO_CLI_NO_ANALYTICS: true
|
||||
MAESTRO_VERSION: 1.41.0
|
||||
MAESTRO_CACHE_VERSION: v1 # Bump this to clear Maestro cache
|
||||
# Disable Maestro recording/artifacts (keep for debugging - set to "true" to re-enable)
|
||||
ENABLE_MAESTRO_RECORDING: false
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -333,9 +336,35 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.maestro
|
||||
key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}
|
||||
key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}-${{ env.MAESTRO_CACHE_VERSION }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}-
|
||||
- name: Validate Maestro cache
|
||||
if: steps.cache-maestro.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
echo "✅ Maestro restored from cache"
|
||||
echo "Validating cached Maestro installation..."
|
||||
|
||||
if [ ! -f "$HOME/.maestro/bin/maestro" ]; then
|
||||
echo "❌ Maestro binary not found in cache - cache is corrupted"
|
||||
echo "Clearing corrupted cache..."
|
||||
rm -rf ~/.maestro
|
||||
echo "MAESTRO_CACHE_VALID=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "✅ Maestro binary found in cache"
|
||||
# Test if Maestro is executable
|
||||
if "$HOME/.maestro/bin/maestro" --version &>/dev/null; then
|
||||
echo "✅ Maestro cache is valid"
|
||||
echo "MAESTRO_CACHE_VALID=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "❌ Maestro binary is not executable - cache is corrupted"
|
||||
echo "Clearing corrupted cache..."
|
||||
rm -rf ~/.maestro
|
||||
echo "MAESTRO_CACHE_VALID=false" >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
- name: Install Maestro
|
||||
if: steps.cache-maestro.outputs.cache-hit != 'true'
|
||||
if: steps.cache-maestro.outputs.cache-hit != 'true' || env.MAESTRO_CACHE_VALID == 'false'
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 5
|
||||
@@ -344,6 +373,20 @@ jobs:
|
||||
command: curl -Ls "https://get.maestro.mobile.dev" | bash
|
||||
- name: Add Maestro to path
|
||||
run: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
|
||||
- name: Verify Maestro installation
|
||||
run: |
|
||||
echo "🔍 Verifying Maestro installation..."
|
||||
echo "Maestro path: $(which maestro)"
|
||||
echo "Maestro version:"
|
||||
maestro --version || {
|
||||
echo "❌ Maestro installation verification failed"
|
||||
echo "Maestro binary location:"
|
||||
find ~/.maestro -name maestro -type f 2>/dev/null || echo "No maestro binary found"
|
||||
echo ""
|
||||
echo "To fix: Bump MAESTRO_CACHE_VERSION in workflow file"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Maestro installation verified"
|
||||
- name: Set up Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
@@ -417,6 +460,7 @@ jobs:
|
||||
BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh
|
||||
env:
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
E2E_TESTING: 1
|
||||
- name: Setup iOS Simulator
|
||||
run: |
|
||||
echo "Setting up iOS Simulator..."
|
||||
@@ -504,6 +548,8 @@ jobs:
|
||||
echo "WORKSPACE_PATH=$WORKSPACE_PATH" >> "$GITHUB_ENV"
|
||||
echo "Resolved workspace: $WORKSPACE_PATH"
|
||||
- name: Build iOS App
|
||||
env:
|
||||
E2E_TESTING: 1
|
||||
run: |
|
||||
echo "Building iOS app..."
|
||||
echo "Project: ${{ env.IOS_PROJECT_NAME }}, Scheme: ${{ env.IOS_PROJECT_SCHEME }}"
|
||||
@@ -538,10 +584,13 @@ jobs:
|
||||
# Use cached derived data and enable parallel builds for faster compilation
|
||||
# Additional flags disable indexing, restrict architecture, and use whole-module Swift compilation
|
||||
# Use the simulator that was set up earlier in the workflow
|
||||
# E2E_TESTING compilation condition excludes NFCPassportReader which isn't available on simulator
|
||||
FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \
|
||||
xcodebuild -workspace "$WORKSPACE_PATH" -scheme ${{ env.IOS_PROJECT_SCHEME }} -configuration Debug -destination "id=${{ env.IOS_SIMULATOR_ID }}" -derivedDataPath app/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet COMPILER_INDEX_STORE_ENABLE=NO ONLY_ACTIVE_ARCH=YES SWIFT_COMPILATION_MODE=wholemodule || { echo "❌ iOS build failed"; exit 1; }
|
||||
xcodebuild -workspace "$WORKSPACE_PATH" -scheme ${{ env.IOS_PROJECT_SCHEME }} -configuration Debug -destination "id=${{ env.IOS_SIMULATOR_ID }}" -derivedDataPath app/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet COMPILER_INDEX_STORE_ENABLE=NO ONLY_ACTIVE_ARCH=YES SWIFT_COMPILATION_MODE=wholemodule 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) E2E_TESTING' || { echo "❌ iOS build failed"; exit 1; }
|
||||
echo "✅ iOS build succeeded"
|
||||
- name: Install and Test on iOS
|
||||
continue-on-error: true
|
||||
id: maestro-test
|
||||
run: |
|
||||
echo "Installing app on simulator..."
|
||||
APP_PATH=$(find app/ios/build/Build/Products/Debug-iphonesimulator -name "*.app" | head -1)
|
||||
@@ -589,20 +638,213 @@ jobs:
|
||||
# Final readiness check (suppress errors to avoid annotations)
|
||||
xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app >/dev/null 2>&1 || sleep 5
|
||||
|
||||
echo "🎭 Running Maestro tests..."
|
||||
echo "Starting test execution..."
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🔍 PRE-TEST DIAGNOSTICS ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Verify Maestro test file exists
|
||||
if [ ! -f "app/tests/e2e/launch.ios.flow.yaml" ]; then
|
||||
echo "❌ Maestro test file not found: app/tests/e2e/launch.ios.flow.yaml"
|
||||
# Create directory for test artifacts
|
||||
mkdir -p app/test-artifacts
|
||||
|
||||
# Check Maestro version and health
|
||||
echo "📦 Maestro installation details:"
|
||||
echo "Binary location: $(which maestro)"
|
||||
echo "Maestro version:"
|
||||
maestro --version || {
|
||||
echo "❌ Failed to get Maestro version"
|
||||
echo "This is a critical error - Maestro is not working"
|
||||
exit 1
|
||||
}
|
||||
echo ""
|
||||
|
||||
# Check Java (required by Maestro)
|
||||
echo "☕ Java status (required by Maestro):"
|
||||
if command -v java &>/dev/null; then
|
||||
java -version 2>&1 | head -3
|
||||
echo "✅ Java is available"
|
||||
else
|
||||
echo "⚠️ Java not found - this may cause Maestro to fail"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check simulator state
|
||||
echo "📱 Simulator state:"
|
||||
xcrun simctl list devices | grep -A 2 "$SIMULATOR_ID" || echo "⚠️ Simulator not found"
|
||||
echo ""
|
||||
|
||||
# Check if app is installed
|
||||
echo "📲 Checking app installation:"
|
||||
xcrun simctl listapps "$SIMULATOR_ID" | grep -i "$IOS_BUNDLE_ID" && echo "✅ App is installed" || echo "⚠️ App not found in installed apps"
|
||||
echo ""
|
||||
|
||||
# Check if app is running
|
||||
echo "🏃 Checking if app is running:"
|
||||
APP_PID=$(xcrun simctl spawn "$SIMULATOR_ID" launchctl list | grep "$IOS_BUNDLE_ID" | awk '{print $1}' || echo "")
|
||||
if [ -n "$APP_PID" ]; then
|
||||
echo "✅ App process found (PID: $APP_PID)"
|
||||
else
|
||||
echo "ℹ️ App not currently running (will be launched by Maestro)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Verify test file
|
||||
echo "📄 Maestro test file:"
|
||||
if [ -f "app/tests/e2e/launch.ios.flow.yaml" ]; then
|
||||
echo "✅ Found: app/tests/e2e/launch.ios.flow.yaml"
|
||||
echo "Contents:"
|
||||
cat app/tests/e2e/launch.ios.flow.yaml
|
||||
else
|
||||
echo "❌ Test file not found"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Note: Maestro 1.41.0 doesn't have daemon or devices commands
|
||||
# The test command handles daemon management internally
|
||||
echo "🔧 Maestro will manage its daemon automatically during test execution"
|
||||
echo ""
|
||||
|
||||
# Take a screenshot before running tests (if recording enabled)
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
|
||||
echo "📸 Taking pre-test screenshot..."
|
||||
xcrun simctl io "$SIMULATOR_ID" screenshot app/test-artifacts/pre-test-screenshot.png || echo "⚠️ Screenshot failed"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Run Maestro with error handling for cleanup issues
|
||||
# Note: Maestro may show NSPOSIXErrorDomain code=3 errors during cleanup when
|
||||
# terminating the test runner app that's already terminated. This is harmless.
|
||||
MAESTRO_OUTPUT=$(maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml 2>&1)
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🎭 STARTING MAESTRO TEST ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Start native simulator recording BEFORE running tests (if recording enabled)
|
||||
# This ensures we capture everything even if Maestro fails immediately
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
|
||||
echo "📹 Starting native simulator recording..."
|
||||
xcrun simctl io "$SIMULATOR_ID" recordVideo --codec=h264 app/test-artifacts/simulator-recording.mp4 &
|
||||
RECORDING_PID=$!
|
||||
echo "✅ Recording started (PID: $RECORDING_PID)"
|
||||
|
||||
# Give recording a moment to initialize
|
||||
sleep 2
|
||||
else
|
||||
echo "ℹ️ Maestro recording disabled (ENABLE_MAESTRO_RECORDING=false)"
|
||||
fi
|
||||
|
||||
# Run Maestro with verbose output and capture all output (including errors)
|
||||
echo "🎭 Starting Maestro test with verbose logging..."
|
||||
echo "Command: maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml"
|
||||
echo "Environment variables:"
|
||||
echo " MAESTRO_DEVICE_ID: $SIMULATOR_ID"
|
||||
echo " MAESTRO_DRIVER_STARTUP_TIMEOUT: 180000"
|
||||
echo " MAESTRO_CLI_NO_ANALYTICS: $MAESTRO_CLI_NO_ANALYTICS"
|
||||
echo " Working directory: $(pwd)"
|
||||
echo ""
|
||||
|
||||
set +e # Don't exit on error
|
||||
# Run with explicit device ID, increased timeout, and verbose output
|
||||
# Note: exit code 2 typically means Maestro couldn't connect to device or daemon
|
||||
MAESTRO_OUTPUT=$(MAESTRO_DEVICE_ID="$SIMULATOR_ID" MAESTRO_DRIVER_STARTUP_TIMEOUT=180000 maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml 2>&1)
|
||||
MAESTRO_EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
echo "Maestro command completed with exit code: $MAESTRO_EXIT_CODE"
|
||||
echo ""
|
||||
|
||||
# Stop the simulator recording (if recording was started)
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ] && [ -n "${RECORDING_PID:-}" ]; then
|
||||
echo ""
|
||||
echo "🛑 Stopping recording..."
|
||||
kill -SIGINT $RECORDING_PID 2>/dev/null || true
|
||||
wait $RECORDING_PID 2>/dev/null || true
|
||||
sleep 2 # Give time for video to finalize
|
||||
fi
|
||||
|
||||
# Take a screenshot after test (if recording enabled)
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
|
||||
echo "📸 Taking post-test screenshot..."
|
||||
xcrun simctl io "$SIMULATOR_ID" screenshot app/test-artifacts/post-test-screenshot.png || echo "⚠️ Screenshot failed"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Save Maestro output to file for debugging
|
||||
echo "$MAESTRO_OUTPUT" > app/test-artifacts/maestro-output.log
|
||||
echo "📝 Maestro output saved to maestro-output.log"
|
||||
|
||||
# Show Maestro output
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 📋 MAESTRO OUTPUT ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo "$MAESTRO_OUTPUT"
|
||||
echo ""
|
||||
|
||||
# Post-test diagnostics
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🔍 POST-TEST DIAGNOSTICS ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check if app is still running
|
||||
echo "🏃 App status after test:"
|
||||
APP_PID_AFTER=$(xcrun simctl spawn "$SIMULATOR_ID" launchctl list | grep "$IOS_BUNDLE_ID" | awk '{print $1}' || echo "")
|
||||
if [ -n "$APP_PID_AFTER" ]; then
|
||||
echo "✅ App still running (PID: $APP_PID_AFTER)"
|
||||
else
|
||||
echo "⚠️ App not running"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check simulator logs for any crashes
|
||||
echo "🔍 Checking for crash logs..."
|
||||
CRASH_LOGS=$(find ~/Library/Logs/DiagnosticReports -name "Self*.crash" -o -name "OpenPassport*.crash" -mmin -5 2>/dev/null | head -5)
|
||||
if [ -n "$CRASH_LOGS" ]; then
|
||||
echo "⚠️ Recent crash logs found:"
|
||||
echo "$CRASH_LOGS"
|
||||
# Copy crash logs to artifacts
|
||||
for log in $CRASH_LOGS; do
|
||||
cp "$log" app/test-artifacts/ 2>/dev/null || true
|
||||
done
|
||||
else
|
||||
echo "✅ No recent crash logs"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check system log for relevant messages
|
||||
echo "📋 Recent simulator system logs (last 50 lines):"
|
||||
xcrun simctl spawn "$SIMULATOR_ID" log show --predicate 'process == "Self" OR process == "OpenPassport"' --last 5m --style compact 2>/dev/null | tail -50 > app/test-artifacts/simulator-system.log || echo "⚠️ Could not retrieve system logs"
|
||||
if [ -f app/test-artifacts/simulator-system.log ]; then
|
||||
echo "✅ System logs saved to simulator-system.log"
|
||||
echo "Last 20 lines:"
|
||||
tail -20 app/test-artifacts/simulator-system.log
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check if video was created (if recording was enabled)
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
|
||||
if [ -f "app/test-artifacts/simulator-recording.mp4" ]; then
|
||||
VIDEO_SIZE=$(ls -lh app/test-artifacts/simulator-recording.mp4 | awk '{print $5}')
|
||||
echo "✅ Video recording saved: simulator-recording.mp4 ($VIDEO_SIZE)"
|
||||
echo ""
|
||||
echo "📹 ===================================================="
|
||||
echo "📹 VIDEO RECORDING AVAILABLE"
|
||||
echo "📹 ===================================================="
|
||||
echo "📹 To view the test recording:"
|
||||
echo "📹 1. Go to: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
echo "📹 2. Scroll to the 'Artifacts' section at the bottom"
|
||||
echo "📹 3. Download 'maestro-artifacts-ios'"
|
||||
echo "📹 4. Extract and open 'simulator-recording.mp4'"
|
||||
echo "📹 ===================================================="
|
||||
echo ""
|
||||
else
|
||||
echo "⚠️ Video recording not created"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ Video recording disabled (ENABLE_MAESTRO_RECORDING=false)"
|
||||
fi
|
||||
|
||||
# Analyze test results
|
||||
echo "🔍 Analyzing test results..."
|
||||
echo "Maestro exit code: $MAESTRO_EXIT_CODE"
|
||||
|
||||
# Check if tests actually passed (ignore cleanup errors)
|
||||
if echo "$MAESTRO_OUTPUT" | grep -q "Flow Passed"; then
|
||||
@@ -616,20 +858,76 @@ jobs:
|
||||
fi
|
||||
elif echo "$MAESTRO_OUTPUT" | grep -q "Flow Failed"; then
|
||||
echo "❌ Maestro tests failed"
|
||||
echo "Check the video recording and maestro-output.log for details"
|
||||
exit 1
|
||||
elif [ $MAESTRO_EXIT_CODE -ne 0 ]; then
|
||||
# Check results file if exit code is non-zero
|
||||
if [ -f "app/maestro-results.xml" ] && ! grep -q "<failure" app/maestro-results.xml; then
|
||||
echo "✅ Tests passed (cleanup error caused non-zero exit)"
|
||||
else
|
||||
echo "❌ Maestro test failed"
|
||||
echo "❌ Maestro test failed with exit code: $MAESTRO_EXIT_CODE"
|
||||
echo "Check the video recording and maestro-output.log for details"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
if: always() && env.ENABLE_MAESTRO_RECORDING == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maestro-results-ios
|
||||
path: app/maestro-results.xml
|
||||
if-no-files-found: warn
|
||||
- name: Upload test artifacts (video and screenshots)
|
||||
if: always() && env.ENABLE_MAESTRO_RECORDING == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maestro-artifacts-ios
|
||||
path: app/test-artifacts/
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
- name: Artifact download instructions
|
||||
if: always() && env.ENABLE_MAESTRO_RECORDING == 'true'
|
||||
run: |
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 📹 TEST ARTIFACTS UPLOADED ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "📦 Artifact name: maestro-artifacts-ios"
|
||||
echo "🎬 Contains:"
|
||||
echo " 📹 simulator-recording.mp4 - Full video of simulator during test"
|
||||
echo " 📝 maestro-output.log - Complete Maestro command output"
|
||||
echo " 📋 simulator-system.log - iOS simulator system logs"
|
||||
echo " 📸 pre-test-screenshot.png - Simulator state before test"
|
||||
echo " 📸 post-test-screenshot.png - Simulator state after test"
|
||||
echo " 📊 maestro-results.xml - Test results (if generated)"
|
||||
echo " 💥 *.crash - Crash logs (if any crashes occurred)"
|
||||
echo "⏰ Retention: 7 days"
|
||||
echo ""
|
||||
echo "🔗 Direct link to this run:"
|
||||
echo " https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
echo ""
|
||||
echo "💡 How to download:"
|
||||
echo " 1. Click the link above (or find this run in the Actions tab)"
|
||||
echo " 2. Scroll down to the 'Artifacts' section"
|
||||
echo " 3. Click 'maestro-artifacts-ios' to download the ZIP"
|
||||
echo " 4. Extract and review:"
|
||||
echo " 🎬 Play simulator-recording.mp4 to see what happened"
|
||||
echo " 📝 Read maestro-output.log for Maestro errors"
|
||||
echo " 📋 Check simulator-system.log for iOS system errors"
|
||||
echo " 📸 Compare pre/post screenshots to see UI state"
|
||||
echo " 💥 Review crash logs if app crashed"
|
||||
echo ""
|
||||
echo "📝 Alternative - Using GitHub CLI:"
|
||||
echo " gh run download ${{ github.run_id }} -n maestro-artifacts-ios"
|
||||
echo ""
|
||||
|
||||
# List what files were actually created
|
||||
echo "📂 Files in test-artifacts directory:"
|
||||
ls -lh app/test-artifacts/ 2>/dev/null || echo " (No artifacts directory found)"
|
||||
echo ""
|
||||
- name: Fail job if tests failed
|
||||
if: steps.maestro-test.outcome == 'failure'
|
||||
run: |
|
||||
echo "❌ Maestro tests failed - failing job after artifact upload"
|
||||
exit 1
|
||||
|
||||
36
.github/workflows/release-calendar.yml
vendored
36
.github/workflows/release-calendar.yml
vendored
@@ -153,12 +153,28 @@ jobs:
|
||||
echo "✓ Successfully pushed branch ${BRANCH_NAME}"
|
||||
fi
|
||||
|
||||
- name: Read app version
|
||||
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr == '' }}
|
||||
id: app_version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python - <<PY >> "$GITHUB_OUTPUT"
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
package_json = pathlib.Path("app/package.json")
|
||||
version = json.loads(package_json.read_text())["version"]
|
||||
print(f"app_version={version}")
|
||||
PY
|
||||
|
||||
- name: Create dev to staging release PR
|
||||
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr == '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_DATE: ${{ steps.check_dev_staging.outputs.date }}
|
||||
BRANCH_NAME: ${{ steps.check_dev_staging.outputs.branch_name }}
|
||||
APP_VERSION: ${{ steps.app_version.outputs.app_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -200,7 +216,7 @@ jobs:
|
||||
"""))
|
||||
PY
|
||||
|
||||
TITLE="Release to Staging - ${PR_DATE}"
|
||||
TITLE="Release to Staging v${APP_VERSION} - ${PR_DATE}"
|
||||
echo "Creating PR with title: ${TITLE} from branch ${BRANCH_NAME}"
|
||||
|
||||
if ! gh pr create \
|
||||
@@ -319,12 +335,28 @@ jobs:
|
||||
gh label create "${LABEL}" --color BFD4F2 --force || true
|
||||
done
|
||||
|
||||
- name: Read app version
|
||||
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.production_status.outputs.staging_not_ahead != 'true' && steps.production_status.outputs.existing_pr == '' }}
|
||||
id: app_version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python - <<PY >> "$GITHUB_OUTPUT"
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
package_json = pathlib.Path("app/package.json")
|
||||
version = json.loads(package_json.read_text())["version"]
|
||||
print(f"app_version={version}")
|
||||
PY
|
||||
|
||||
- name: Create staging to main release PR
|
||||
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.production_status.outputs.staging_not_ahead != 'true' && steps.production_status.outputs.existing_pr == '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_DATE: ${{ steps.production_status.outputs.date }}
|
||||
COMMITS_AHEAD: ${{ steps.production_status.outputs.commits }}
|
||||
APP_VERSION: ${{ steps.app_version.outputs.app_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -367,7 +399,7 @@ jobs:
|
||||
"""))
|
||||
PY
|
||||
|
||||
TITLE="Release to Production - ${PR_DATE}"
|
||||
TITLE="Release to Production v${APP_VERSION} - ${PR_DATE}"
|
||||
echo "Creating PR with title: ${TITLE} from staging with ${COMMITS_AHEAD} commits ahead."
|
||||
|
||||
gh pr create \
|
||||
|
||||
@@ -101,4 +101,3 @@ We are actively looking for contributors. Please check the [open issues](https:/
|
||||
Thanks [Rémi](https://github.com/remicolin), [Florent](https://github.com/0xturboblitz), [Ayman](https://github.com/Nesopie), [Justin](https://github.com/transphorm), [Seshanth](https://github.com/seshanthS), [Nico](https://github.com/motemotech) and all other contributors for building Self.
|
||||
|
||||
Thanks [Aayush](https://twitter.com/yush_g), [Vivek](https://twitter.com/viv_boop), [Andy](https://twitter.com/AndyGuzmanEth) and [Vitalik](https://github.com/vbuterin) for contributing ideas and inspiring us to build this technology, and [PSE](https://pse.dev/) for supporting the initial work through grants!
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1204.0)
|
||||
aws-sdk-core (3.241.3)
|
||||
aws-partitions (1.1209.0)
|
||||
aws-sdk-core (3.241.4)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -32,11 +32,11 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.120.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-kms (1.121.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.211.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-s3 (1.212.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:usesCleartextTraffic"
|
||||
/>
|
||||
tools:replace="android:usesCleartextTraffic">
|
||||
|
||||
<!-- Override conflicting ML Kit dependencies from passportreader (ocr) and Sumsub SDK (face) -->
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="ocr,face"
|
||||
tools:replace="android:value" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -106,5 +106,11 @@
|
||||
</intent-filter>
|
||||
<meta-data android:name="photopicker_activity:0:required" android:value="" />
|
||||
</service>
|
||||
|
||||
<!-- Override conflicting ML Kit dependencies from passportreader (ocr) and Sumsub SDK (face) -->
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="ocr,face"
|
||||
tools:replace="android:value" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -41,6 +41,7 @@ allprojects {
|
||||
url("$rootDir/../../node_modules/jsc-android/dist")
|
||||
}
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url "https://maven.sumsub.com/repository/maven-public/" }
|
||||
}
|
||||
configurations.configureEach {
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
|
||||
@@ -8,4 +8,5 @@ IS_TEST_BUILD=
|
||||
MIXPANEL_NFC_PROJECT_TOKEN=
|
||||
SEGMENT_KEY=
|
||||
SENTRY_DSN=
|
||||
SUMSUB_TEE_URL=
|
||||
IS_TEST_BUILD=
|
||||
|
||||
@@ -28,6 +28,9 @@ export const IS_TEST_BUILD = process.env.IS_TEST_BUILD === 'true';
|
||||
export const MIXPANEL_NFC_PROJECT_TOKEN = undefined;
|
||||
export const SEGMENT_KEY = process.env.SEGMENT_KEY;
|
||||
export const SENTRY_DSN = process.env.SENTRY_DSN;
|
||||
export const SUMSUB_TEE_URL =
|
||||
process.env.SUMSUB_TEE_URL || 'http://localhost:8080';
|
||||
export const SUMSUB_TEST_TOKEN = process.env.SUMSUB_TEST_TOKEN;
|
||||
|
||||
export const TURNKEY_AUTH_PROXY_CONFIG_ID =
|
||||
process.env.TURNKEY_AUTH_PROXY_CONFIG_ID;
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
source "https://cdn.cocoapods.org/"
|
||||
|
||||
# Skip Sumsub configuration for E2E testing
|
||||
unless ENV["E2E_TESTING"] == "1"
|
||||
source "https://github.com/SumSubstance/Specs.git"
|
||||
|
||||
# Enable Fisherman (Device Intelligence) module
|
||||
ENV["IDENSIC_WITH_FISHERMAN"] = "true"
|
||||
|
||||
# Enable VideoIdent module
|
||||
ENV["IDENSIC_WITH_VIDEOIDENT"] = "true"
|
||||
end
|
||||
|
||||
use_frameworks!
|
||||
require "tmpdir"
|
||||
|
||||
@@ -39,6 +51,7 @@ def using_https_git_auth?
|
||||
end
|
||||
|
||||
target "Self" do
|
||||
# Native module exclusion for E2E testing is handled in react-native.config.cjs
|
||||
config = use_native_modules!
|
||||
|
||||
use_frameworks!
|
||||
|
||||
@@ -11,6 +11,7 @@ PODS:
|
||||
- DoubleConversion (1.1.6)
|
||||
- fast_float (6.1.4)
|
||||
- FBLazyVector (0.76.9)
|
||||
- FingerprintPro (2.12.0)
|
||||
- Firebase (10.24.0):
|
||||
- Firebase/Core (= 10.24.0)
|
||||
- Firebase/Core (10.24.0):
|
||||
@@ -149,6 +150,19 @@ PODS:
|
||||
- hermes-engine (0.76.9):
|
||||
- hermes-engine/Pre-built (= 0.76.9)
|
||||
- hermes-engine/Pre-built (0.76.9)
|
||||
- IdensicMobileSDK (1.40.2):
|
||||
- IdensicMobileSDK/Default (= 1.40.2)
|
||||
- IdensicMobileSDK/Core (1.40.2)
|
||||
- IdensicMobileSDK/Default (1.40.2):
|
||||
- IdensicMobileSDK/Core
|
||||
- IdensicMobileSDK/Fisherman (1.40.2):
|
||||
- FingerprintPro (~> 2.11)
|
||||
- IdensicMobileSDK/Core
|
||||
- IdensicMobileSDK/VideoIdent (1.40.2):
|
||||
- IdensicMobileSDK/VideoIdent-latest
|
||||
- IdensicMobileSDK/VideoIdent-latest (1.40.2):
|
||||
- IdensicMobileSDK/Core
|
||||
- TwilioVideo (>= 5.8.2)
|
||||
- lottie-ios (4.5.0)
|
||||
- lottie-react-native (7.2.2):
|
||||
- DoubleConversion
|
||||
@@ -1537,6 +1551,11 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-get-random-values (1.11.0):
|
||||
- React-Core
|
||||
- react-native-mobilesdk-module (1.40.2):
|
||||
- IdensicMobileSDK (= 1.40.2)
|
||||
- IdensicMobileSDK/Fisherman (= 1.40.2)
|
||||
- IdensicMobileSDK/VideoIdent (= 1.40.2)
|
||||
- React-Core
|
||||
- react-native-netinfo (11.4.1):
|
||||
- React-Core
|
||||
- react-native-nfc-manager (3.16.3):
|
||||
@@ -2133,7 +2152,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- Sentry/HybridSDK (= 8.53.2)
|
||||
- Yoga
|
||||
- RNSVG (15.14.0):
|
||||
- RNSVG (15.15.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2153,9 +2172,9 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNSVG/common (= 15.14.0)
|
||||
- RNSVG/common (= 15.15.0)
|
||||
- Yoga
|
||||
- RNSVG/common (15.14.0):
|
||||
- RNSVG/common (15.15.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2185,6 +2204,7 @@ PODS:
|
||||
- React-Core
|
||||
- SwiftQRScanner (1.1.6)
|
||||
- SwiftyTesseract (3.1.3)
|
||||
- TwilioVideo (5.11.1)
|
||||
- Yoga (0.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -2243,6 +2263,7 @@ DEPENDENCIES:
|
||||
- react-native-cloud-storage (from `../node_modules/react-native-cloud-storage`)
|
||||
- "react-native-compat (from `../node_modules/@walletconnect/react-native-compat`)"
|
||||
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
|
||||
- "react-native-mobilesdk-module (from `../node_modules/@sumsub/react-native-mobilesdk-module`)"
|
||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||
- react-native-nfc-manager (from `../node_modules/react-native-nfc-manager`)
|
||||
- react-native-passkey (from `../node_modules/react-native-passkey`)
|
||||
@@ -2296,8 +2317,11 @@ DEPENDENCIES:
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/SumSubstance/Specs.git:
|
||||
- IdensicMobileSDK
|
||||
trunk:
|
||||
- AppAuth
|
||||
- FingerprintPro
|
||||
- Firebase
|
||||
- FirebaseABTesting
|
||||
- FirebaseAnalytics
|
||||
@@ -2322,6 +2346,7 @@ SPEC REPOS:
|
||||
- Sentry
|
||||
- SocketRocket
|
||||
- SwiftyTesseract
|
||||
- TwilioVideo
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
boost:
|
||||
@@ -2416,6 +2441,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/@walletconnect/react-native-compat"
|
||||
react-native-get-random-values:
|
||||
:path: "../node_modules/react-native-get-random-values"
|
||||
react-native-mobilesdk-module:
|
||||
:path: "../node_modules/@sumsub/react-native-mobilesdk-module"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-nfc-manager:
|
||||
@@ -2534,6 +2561,7 @@ SPEC CHECKSUMS:
|
||||
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45
|
||||
FingerprintPro: 035517a1b4e3e4fc073486b53b9956509010f8db
|
||||
Firebase: 91fefd38712feb9186ea8996af6cbdef41473442
|
||||
FirebaseABTesting: d87f56707159bae64e269757a6e963d490f2eebe
|
||||
FirebaseAnalytics: b5efc493eb0f40ec560b04a472e3e1a15d39ca13
|
||||
@@ -2551,6 +2579,7 @@ SPEC CHECKSUMS:
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
|
||||
IdensicMobileSDK: 00b13320e1b1e0574e68475bd0fbc7cd30fce26e
|
||||
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||
lottie-react-native: 7bb65bc88d3f9996ea2f646a96694285405df2f9
|
||||
Mixpanel-swift: e9bef28a9648faff384d5ba6f48ecc2787eb24c0
|
||||
@@ -2595,6 +2624,7 @@ SPEC CHECKSUMS:
|
||||
react-native-cloud-storage: 8d89f2bc574cf11068dfd90933905974087fb9e9
|
||||
react-native-compat: b80530ebcd3d574be5dd99cb27b984a17c119abc
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
react-native-mobilesdk-module: 4770cb45fdd19dc4eed04615f0fcdab013b3dfe2
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-nfc-manager: 66a00e5ddab9704efebe19d605b1b8afb0bb1bd7
|
||||
react-native-passkey: 8853c3c635164864da68a6dbbcec7148506c3bcf
|
||||
@@ -2641,15 +2671,16 @@ SPEC CHECKSUMS:
|
||||
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
|
||||
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
|
||||
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
|
||||
RNSVG: e1cf5a9a5aa12c69f2ec47031defbd87ae7fb697
|
||||
RNSVG: 39476f26bbbe72ffe6194c6fc8f6acd588087957
|
||||
segment-analytics-react-native: 0eae155b0e9fa560fa6b17d78941df64537c35b7
|
||||
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594
|
||||
SwiftQRScanner: e85a25f9b843e9231dab89a96e441472fe54a724
|
||||
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
|
||||
TwilioVideo: 9f51085d4e4fb3aff8e168b8215b31cb0f486a2f
|
||||
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
|
||||
|
||||
PODFILE CHECKSUM: 0aa47f53692543349c43673cda7380fa23049eba
|
||||
PODFILE CHECKSUM: f03c12b5d96fb6e22afe20fba517840fef44e76f
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// 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 Foundation
|
||||
import NFCPassportReader
|
||||
import Mixpanel
|
||||
|
||||
#if !E2E_TESTING
|
||||
import NFCPassportReader
|
||||
|
||||
public class SelfAnalytics: Analytics {
|
||||
private let enableDebugLogs: Bool
|
||||
|
||||
@@ -67,3 +69,13 @@ public class SelfAnalytics: Analytics {
|
||||
Mixpanel.mainInstance().flush()
|
||||
}
|
||||
}
|
||||
#else
|
||||
// E2E Testing stub - SelfAnalytics is not used when NFCPassportReader is excluded
|
||||
public class SelfAnalytics {
|
||||
public init(token: String, enableDebugLogs: Bool = false, trackAutomaticEvents: Bool = false) {}
|
||||
public func trackEvent(_ name: String, properties: [String: Any]? = nil) {}
|
||||
public func trackDebugEvent(_ name: String, properties: [String: Any]? = nil) {}
|
||||
public func trackError(_ error: Error, context: String) {}
|
||||
public func flush() {}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -16,7 +16,7 @@ module.exports = {
|
||||
'node',
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags|react-native-blur-effect)/)',
|
||||
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags|react-native-blur-effect|@sumsub)/)',
|
||||
],
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
testMatch: [
|
||||
|
||||
@@ -1237,3 +1237,29 @@ jest.mock('react-native/Libraries/AppState/AppState', () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock @sumsub/react-native-mobilesdk-module
|
||||
jest.mock('@sumsub/react-native-mobilesdk-module', () => {
|
||||
const createBuilder = () => ({
|
||||
withHandlers: jest.fn().mockReturnThis(),
|
||||
withDebug: jest.fn().mockReturnThis(),
|
||||
withLocale: jest.fn().mockReturnThis(),
|
||||
withAnalyticsEnabled: jest.fn().mockReturnThis(),
|
||||
build: jest.fn().mockReturnValue({
|
||||
launch: jest.fn().mockResolvedValue({ success: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
const MockSNSMobileSDK = {
|
||||
init: jest
|
||||
.fn()
|
||||
.mockImplementation((accessToken, tokenExpirationHandler) =>
|
||||
createBuilder(),
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
default: MockSNSMobileSDK,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
"test:ci": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
|
||||
"test:coverage": "yarn jest:run --coverage --passWithNoTests",
|
||||
"test:coverage:ci": "yarn jest:run --coverage --passWithNoTests --ci --coverageReporters=lcov --coverageReporters=text --coverageReporters=json",
|
||||
"test:e2e:android": "./scripts/mobile-ci-build-android.sh && maestro test tests/e2e/launch.android.flow.yaml",
|
||||
"test:e2e:ios": "xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build && maestro test tests/e2e/launch.ios.flow.yaml",
|
||||
"test:e2e:android": "./scripts/test-e2e-local.sh android --ci-match",
|
||||
"test:e2e:ios": "./scripts/test-e2e-local.sh ios --ci-match",
|
||||
"test:fastlane": "bundle exec ruby -Itest fastlane/test/helpers_test.rb",
|
||||
"test:tree-shaking": "node ./scripts/test-tree-shaking.cjs",
|
||||
"test:web-build": "yarn jest:run tests/web-build-render.test.ts --testTimeout=180000",
|
||||
@@ -110,6 +110,7 @@
|
||||
"@sentry/react": "^9.32.0",
|
||||
"@sentry/react-native": "7.0.1",
|
||||
"@stablelib/cbor": "^2.0.1",
|
||||
"@sumsub/react-native-mobilesdk-module": "1.40.2",
|
||||
"@tamagui/animations-css": "1.126.14",
|
||||
"@tamagui/animations-react-native": "1.126.14",
|
||||
"@tamagui/config": "1.126.14",
|
||||
@@ -163,13 +164,13 @@
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
"react-native-screens": "4.15.3",
|
||||
"react-native-sqlite-storage": "^6.0.1",
|
||||
"react-native-svg": "15.14.0",
|
||||
"react-native-svg": "15.15.0",
|
||||
"react-native-svg-web": "1.0.9",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "^0.19.0",
|
||||
"react-native-webview": "^13.16.0",
|
||||
"react-qr-barcode-scanner": "^2.1.8",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"tamagui": "1.126.14",
|
||||
"uuid": "^11.1.0",
|
||||
"xstate": "^5.20.2",
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
const dependencies = {
|
||||
'@selfxyz/mobile-sdk-alpha': { platforms: { android: null, ios: null } },
|
||||
};
|
||||
|
||||
// Disable Sumsub SDK autolinking during E2E testing to avoid build issues
|
||||
if (process.env.E2E_TESTING === '1') {
|
||||
dependencies['@sumsub/react-native-mobilesdk-module'] = {
|
||||
platforms: { android: null, ios: null },
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
project: { ios: {}, android: {} },
|
||||
dependencies: {
|
||||
'@selfxyz/mobile-sdk-alpha': { platforms: { android: null, ios: null } },
|
||||
},
|
||||
dependencies,
|
||||
assets: ['../src/assets/fonts'],
|
||||
};
|
||||
|
||||
@@ -18,20 +18,22 @@ NC='\033[0m' # No Color
|
||||
|
||||
print_usage() {
|
||||
echo "🎭 Local E2E Testing"
|
||||
echo "Usage: $0 [ios|android] [--workflow-match]"
|
||||
echo "Usage: $0 [ios|android] [--ci-match|--workflow-match]"
|
||||
echo ""
|
||||
echo "Modes:"
|
||||
echo " (default) - Debug builds, requires Metro server running"
|
||||
echo " --ci-match - Debug builds with bundled JS (matches GitHub CI exactly)"
|
||||
echo " --workflow-match - Release builds (legacy, for quick local testing)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 ios - Run iOS e2e tests locally"
|
||||
echo " $0 android - Run Android e2e tests locally"
|
||||
echo " $0 android --workflow-match - Run Android tests matching GitHub Actions workflow"
|
||||
echo " $0 ios - Run iOS e2e tests locally (requires Metro)"
|
||||
echo " $0 ios --ci-match - Run iOS tests matching GitHub CI exactly"
|
||||
echo " $0 android - Run Android e2e tests locally (requires Metro)"
|
||||
echo " $0 android --ci-match - Run Android tests matching GitHub CI exactly"
|
||||
echo ""
|
||||
echo "Prerequisites:"
|
||||
echo " iOS: Xcode, iOS Simulator, CocoaPods"
|
||||
echo " Android: Android SDK, running emulator"
|
||||
echo ""
|
||||
echo "Workflow Match Mode:"
|
||||
echo " --workflow-match - Use Release builds and exact workflow steps"
|
||||
echo " (No Metro dependency, matches CI environment)"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
@@ -249,18 +251,39 @@ build_ios_app() {
|
||||
# Set environment variable for e2e testing to enable OpenSSL fixes
|
||||
export E2E_TESTING=1
|
||||
|
||||
# Set build configuration based on workflow match
|
||||
if [ "$WORKFLOW_MATCH" = "true" ]; then
|
||||
if [ "$CI_MATCH" = "true" ]; then
|
||||
log_info "Using Debug configuration with bundled JS (matches CI)"
|
||||
BUILD_CONFIG="Debug"
|
||||
# Match CI xcodebuild flags exactly with FORCE_BUNDLING and RCT_NO_LAUNCH_PACKAGER
|
||||
if ! FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \
|
||||
xcodebuild -workspace ios/OpenPassport.xcworkspace \
|
||||
-scheme OpenPassport \
|
||||
-configuration Debug \
|
||||
-sdk iphonesimulator \
|
||||
-derivedDataPath ios/build \
|
||||
-jobs "$(sysctl -n hw.ncpu)" \
|
||||
-parallelizeTargets \
|
||||
COMPILER_INDEX_STORE_ENABLE=NO \
|
||||
ONLY_ACTIVE_ARCH=YES \
|
||||
SWIFT_COMPILATION_MODE=wholemodule \
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS="E2E_TESTING"; then
|
||||
log_error "iOS build failed"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$WORKFLOW_MATCH" = "true" ]; then
|
||||
log_info "Using Release configuration for workflow match"
|
||||
BUILD_CONFIG="Release"
|
||||
if ! xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration "$BUILD_CONFIG" -sdk iphonesimulator -derivedDataPath ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets SWIFT_ACTIVE_COMPILATION_CONDITIONS="E2E_TESTING"; then
|
||||
log_error "iOS build failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_info "Using Debug configuration for local development"
|
||||
BUILD_CONFIG="Debug"
|
||||
fi
|
||||
|
||||
if ! xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration "$BUILD_CONFIG" -sdk iphonesimulator -derivedDataPath ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets SWIFT_ACTIVE_COMPILATION_CONDITIONS="E2E_TESTING"; then
|
||||
log_error "iOS build failed"
|
||||
exit 1
|
||||
if ! xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration "$BUILD_CONFIG" -sdk iphonesimulator -derivedDataPath ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets SWIFT_ACTIVE_COMPILATION_CONDITIONS="E2E_TESTING"; then
|
||||
log_error "iOS build failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
log_success "iOS build succeeded"
|
||||
}
|
||||
@@ -436,17 +459,32 @@ setup_android_environment() {
|
||||
|
||||
build_android_app() {
|
||||
log_info "🔨 Building Android APK..."
|
||||
# Note: Using Release builds to avoid Metro dependency in CI
|
||||
# Debug builds require Metro server, Release builds have JS bundled
|
||||
# Run the build inside the android directory so gradlew is available
|
||||
echo "Current working directory: $(pwd)"
|
||||
echo "Checking if gradlew exists:"
|
||||
ls -la android/gradlew || echo "gradlew not found in android/"
|
||||
|
||||
cd android
|
||||
if ! ./gradlew assembleRelease --quiet; then
|
||||
log_error "Android build failed"
|
||||
exit 1
|
||||
if [ "$CI_MATCH" = "true" ]; then
|
||||
log_info "Using Debug build with bundled JS (matches CI)"
|
||||
# Force JS bundling in debug build to match CI behavior
|
||||
if ! ./gradlew assembleDebug -PbundleInDebug=true --quiet; then
|
||||
log_error "Android build failed"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$WORKFLOW_MATCH" = "true" ]; then
|
||||
log_info "Using Release build for workflow match"
|
||||
if ! ./gradlew assembleRelease --quiet; then
|
||||
log_error "Android build failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Default local dev uses Debug (requires Metro, like iOS default)
|
||||
log_info "Using Debug build for local development"
|
||||
if ! ./gradlew assembleDebug --quiet; then
|
||||
log_error "Android build failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
log_success "Android build succeeded"
|
||||
cd ..
|
||||
@@ -454,8 +492,14 @@ build_android_app() {
|
||||
|
||||
install_android_app() {
|
||||
log_info "📦 Installing app on emulator..."
|
||||
# Check if APK was built successfully (matching workflow)
|
||||
APK_PATH="android/app/build/outputs/apk/release/app-release.apk"
|
||||
# Check if APK was built successfully
|
||||
if [ "$WORKFLOW_MATCH" = "true" ]; then
|
||||
# WORKFLOW_MATCH uses Release builds
|
||||
APK_PATH="android/app/build/outputs/apk/release/app-release.apk"
|
||||
else
|
||||
# CI_MATCH and default mode both use Debug builds
|
||||
APK_PATH="android/app/build/outputs/apk/debug/app-debug.apk"
|
||||
fi
|
||||
log_info "Looking for APK at: $APK_PATH"
|
||||
if [ ! -f "$APK_PATH" ]; then
|
||||
log_error "APK not found at expected location"
|
||||
@@ -575,7 +619,10 @@ run_ios_tests() {
|
||||
echo "🍎 Starting local iOS e2e testing..."
|
||||
|
||||
shutdown_all_simulators
|
||||
check_metro_running
|
||||
# Skip Metro check for CI_MATCH (bundled JS) and WORKFLOW_MATCH (Release)
|
||||
if [ "$WORKFLOW_MATCH" != "true" ] && [ "$CI_MATCH" != "true" ]; then
|
||||
check_metro_running
|
||||
fi
|
||||
setup_ios_environment
|
||||
setup_ios_simulator
|
||||
build_ios_app
|
||||
@@ -600,8 +647,8 @@ run_android_tests() {
|
||||
# Set up trap to cleanup emulator on script exit
|
||||
trap cleanup_android_emulator EXIT
|
||||
|
||||
# Only check Metro if not in workflow match mode
|
||||
if [ "$WORKFLOW_MATCH" != "true" ]; then
|
||||
# Skip Metro check for CI_MATCH (bundled JS) and WORKFLOW_MATCH (Release)
|
||||
if [ "$WORKFLOW_MATCH" != "true" ] && [ "$CI_MATCH" != "true" ]; then
|
||||
check_metro_running
|
||||
fi
|
||||
|
||||
@@ -628,13 +675,16 @@ main() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for workflow match mode
|
||||
# Check for workflow match mode and CI match mode
|
||||
WORKFLOW_MATCH="false"
|
||||
CI_MATCH="false"
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--workflow-match" ]; then
|
||||
WORKFLOW_MATCH="true"
|
||||
log_info "🔧 Running in workflow match mode (Release builds, no Metro)"
|
||||
break
|
||||
elif [ "$arg" = "--ci-match" ]; then
|
||||
CI_MATCH="true"
|
||||
log_info "🔧 Running in CI match mode (Debug builds with bundled JS, matches GitHub CI)"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
14
app/src/integrations/sumsub/index.ts
Normal file
14
app/src/integrations/sumsub/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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.
|
||||
|
||||
export type {
|
||||
AccessTokenResponse,
|
||||
SumsubApplicantInfo,
|
||||
SumsubResult,
|
||||
} from '@/integrations/sumsub/types';
|
||||
export {
|
||||
type SumsubConfig,
|
||||
fetchAccessToken,
|
||||
launchSumsub,
|
||||
} from '@/integrations/sumsub/sumsubService';
|
||||
108
app/src/integrations/sumsub/sumsubService.ts
Normal file
108
app/src/integrations/sumsub/sumsubService.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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 { SUMSUB_TEE_URL } from '@env';
|
||||
import SNSMobileSDK from '@sumsub/react-native-mobilesdk-module';
|
||||
|
||||
import type {
|
||||
AccessTokenResponse,
|
||||
SumsubResult,
|
||||
} from '@/integrations/sumsub/types';
|
||||
|
||||
export interface SumsubConfig {
|
||||
accessToken: string;
|
||||
locale?: string;
|
||||
debug?: boolean;
|
||||
onStatusChanged?: (prevStatus: string, newStatus: string) => void;
|
||||
onEvent?: (eventType: string, payload: unknown) => void;
|
||||
}
|
||||
|
||||
const FETCH_TIMEOUT_MS = 30000; // 30 seconds
|
||||
|
||||
export const fetchAccessToken = async (
|
||||
phoneNumber?: string,
|
||||
): Promise<AccessTokenResponse> => {
|
||||
const apiUrl = SUMSUB_TEE_URL;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const requestBody: Record<string, string> = {};
|
||||
|
||||
if (phoneNumber) {
|
||||
requestBody.phone = phoneNumber;
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}/access-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to get Sumsub access token (HTTP ${response.status})`,
|
||||
);
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
// Handle both string and object responses
|
||||
if (typeof body === 'string') {
|
||||
return JSON.parse(body) as AccessTokenResponse;
|
||||
}
|
||||
|
||||
return body as AccessTokenResponse;
|
||||
} catch (err) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (err instanceof Error) {
|
||||
if (err.name === 'AbortError') {
|
||||
throw new Error(
|
||||
`Request to Sumsub TEE timed out after ${FETCH_TIMEOUT_MS / 1000}s`,
|
||||
);
|
||||
}
|
||||
throw new Error(`Failed to get Sumsub access token: ${err.message}`);
|
||||
}
|
||||
|
||||
throw new Error('Failed to get Sumsub access token: Unknown error');
|
||||
}
|
||||
};
|
||||
|
||||
export const launchSumsub = async (
|
||||
config: SumsubConfig,
|
||||
): Promise<SumsubResult> => {
|
||||
const sdk = SNSMobileSDK.init(config.accessToken, async () => {
|
||||
// Token refresh not implemented for test flow
|
||||
throw new Error(
|
||||
'Sumsub token expired - refresh not implemented for test flow',
|
||||
);
|
||||
})
|
||||
.withHandlers({
|
||||
onStatusChanged: event => {
|
||||
console.log(`Sumsub status: ${event.prevStatus} => ${event.newStatus}`);
|
||||
config.onStatusChanged?.(event.prevStatus, event.newStatus);
|
||||
},
|
||||
onLog: _event => {
|
||||
// Log event received but don't log message (may contain PII)
|
||||
console.log('[Sumsub] Log event received');
|
||||
},
|
||||
onEvent: event => {
|
||||
// Only log event type, not full payload (may contain PII)
|
||||
console.log(`Sumsub event: ${event.eventType}`);
|
||||
config.onEvent?.(event.eventType, event.payload);
|
||||
},
|
||||
})
|
||||
.withDebug(config.debug ?? __DEV__)
|
||||
.withLocale(config.locale ?? 'en')
|
||||
.withAnalyticsEnabled(true) // Device Intelligence requires this
|
||||
.build();
|
||||
|
||||
return sdk.launch();
|
||||
};
|
||||
40
app/src/integrations/sumsub/types.ts
Normal file
40
app/src/integrations/sumsub/types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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.
|
||||
|
||||
export interface AccessTokenResponse {
|
||||
token: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface SumsubApplicantInfo {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
key: string;
|
||||
clientId: string;
|
||||
inspectionId: string;
|
||||
externalUserId: string;
|
||||
info?: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
dob?: string;
|
||||
country?: string;
|
||||
phone?: string;
|
||||
};
|
||||
email?: string;
|
||||
phone?: string;
|
||||
review: {
|
||||
reviewAnswer: string;
|
||||
reviewResult: {
|
||||
reviewAnswer: string;
|
||||
};
|
||||
};
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SumsubResult {
|
||||
success: boolean;
|
||||
status: string;
|
||||
errorType?: string;
|
||||
errorMsg?: string;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import DevHapticFeedbackScreen from '@/screens/dev/DevHapticFeedbackScreen';
|
||||
import DevLoadingScreen from '@/screens/dev/DevLoadingScreen';
|
||||
import DevPrivateKeyScreen from '@/screens/dev/DevPrivateKeyScreen';
|
||||
import DevSettingsScreen from '@/screens/dev/DevSettingsScreen';
|
||||
import SumsubTestScreen from '@/screens/dev/SumsubTestScreen';
|
||||
|
||||
const devHeaderOptions: NativeStackNavigationOptions = {
|
||||
headerStyle: {
|
||||
@@ -21,6 +22,7 @@ const devHeaderOptions: NativeStackNavigationOptions = {
|
||||
headerTitleStyle: {
|
||||
color: white,
|
||||
},
|
||||
headerTintColor: white,
|
||||
headerBackTitle: 'close',
|
||||
};
|
||||
|
||||
@@ -80,6 +82,13 @@ const devScreens = {
|
||||
title: 'Dev Loading Screen',
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
SumsubTest: {
|
||||
screen: SumsubTestScreen,
|
||||
options: {
|
||||
...devHeaderOptions,
|
||||
title: 'Sumsub Test',
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
};
|
||||
|
||||
export default devScreens;
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import { logNFCEvent, logProofEvent } from '@/config/sentry';
|
||||
import { fetchAccessToken, launchSumsub } from '@/integrations/sumsub';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { navigationRef } from '@/navigation';
|
||||
import {
|
||||
@@ -298,6 +299,16 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
|
||||
navigationRef.navigate('AadhaarUpload', { countryCode });
|
||||
}
|
||||
break;
|
||||
case 'kyc':
|
||||
fetchAccessToken()
|
||||
.then(accessToken => {
|
||||
launchSumsub({ accessToken: accessToken.token });
|
||||
})
|
||||
// TODO: show sumsub error screen
|
||||
.catch(error => {
|
||||
console.error('Error launching Sumsub:', error);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
if (countryCode) {
|
||||
navigationRef.navigate('ComingSoon', { countryCode });
|
||||
|
||||
@@ -681,6 +681,29 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('SumsubTest');
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
Sumsub Test Flow
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
{IS_DEV_MODE && (
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
|
||||
686
app/src/screens/dev/SumsubTestScreen.tsx
Normal file
686
app/src/screens/dev/SumsubTestScreen.tsx
Normal file
@@ -0,0 +1,686 @@
|
||||
// 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, useRef, useState } from 'react';
|
||||
import { Alert, ScrollView, TextInput } from 'react-native';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { Button, Text, XStack, YStack } from 'tamagui';
|
||||
import { SUMSUB_TEE_URL } from '@env';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { ChevronLeft } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
green500,
|
||||
red500,
|
||||
slate200,
|
||||
slate400,
|
||||
slate500,
|
||||
slate600,
|
||||
slate800,
|
||||
white,
|
||||
yellow500,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks';
|
||||
|
||||
import {
|
||||
fetchAccessToken,
|
||||
launchSumsub,
|
||||
type SumsubApplicantInfo,
|
||||
type SumsubResult,
|
||||
} from '@/integrations/sumsub';
|
||||
|
||||
const SumsubTestScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const [phoneNumber, setPhoneNumber] = useState('+11234567890');
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sdkLaunching, setSdkLaunching] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [result, setResult] = useState<SumsubResult | null>(null);
|
||||
const [applicantInfo, setApplicantInfo] =
|
||||
useState<SumsubApplicantInfo | null>(null);
|
||||
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
const hasSubscribedRef = useRef<boolean>(false);
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
const paddingBottom = useSafeBottomPadding(20);
|
||||
|
||||
const handleFetchToken = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setAccessToken(null);
|
||||
setUserId(null);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const response = await fetchAccessToken(phoneNumber);
|
||||
if (!isMountedRef.current) return;
|
||||
setAccessToken(response.token);
|
||||
setUserId(response.userId);
|
||||
Alert.alert('Success', 'Access token generated successfully', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} catch (err) {
|
||||
if (!isMountedRef.current) return;
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(message);
|
||||
Alert.alert('Error', `Failed to fetch access token: ${message}`, [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [phoneNumber]);
|
||||
|
||||
const subscribeToWebSocket = useCallback(() => {
|
||||
if (!userId || hasSubscribedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Connecting to WebSocket:', SUMSUB_TEE_URL);
|
||||
const socket = io(SUMSUB_TEE_URL, {
|
||||
transports: ['websocket', 'polling'],
|
||||
});
|
||||
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Socket connected, subscribing to user');
|
||||
hasSubscribedRef.current = true;
|
||||
socket.emit('subscribe', userId);
|
||||
});
|
||||
|
||||
socket.on('success', (data: SumsubApplicantInfo) => {
|
||||
console.log('Received applicant info');
|
||||
if (!isMountedRef.current) return;
|
||||
setApplicantInfo(data);
|
||||
Alert.alert(
|
||||
'Verification Complete',
|
||||
'Your verification was successful!',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('verification_failed', (reason: string) => {
|
||||
console.log('Verification failed:', reason);
|
||||
if (!isMountedRef.current) return;
|
||||
setError(`Verification failed: ${reason}`);
|
||||
Alert.alert('Verification Failed', reason, [{ text: 'OK' }]);
|
||||
});
|
||||
|
||||
socket.on('error', (errorMessage: string) => {
|
||||
console.error('Socket error:', errorMessage);
|
||||
if (!isMountedRef.current) return;
|
||||
setError(errorMessage);
|
||||
hasSubscribedRef.current = false;
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Socket disconnected');
|
||||
hasSubscribedRef.current = false;
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
const handleLaunchSumsub = useCallback(async () => {
|
||||
if (!accessToken) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'No access token available. Please generate one first.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setSdkLaunching(true);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const sdkResult = await launchSumsub({
|
||||
accessToken,
|
||||
debug: true,
|
||||
locale: 'en',
|
||||
onEvent: (eventType, _payload) => {
|
||||
console.log('SDK Event:', eventType);
|
||||
// Subscribe to WebSocket when verification is completed
|
||||
if (eventType === 'idCheck.onApplicantVerificationCompleted') {
|
||||
subscribeToWebSocket();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
setResult(sdkResult);
|
||||
|
||||
if (sdkResult.success) {
|
||||
Alert.alert(
|
||||
'SDK Closed',
|
||||
`Sumsub SDK closed with status: ${sdkResult.status}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
`Sumsub failed: ${sdkResult.errorMsg || sdkResult.errorType || 'Unknown error'}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Sumsub launch error:', err);
|
||||
if (!isMountedRef.current) return;
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(message);
|
||||
Alert.alert('Error', `Failed to launch Sumsub SDK: ${message}`, [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setSdkLaunching(false);
|
||||
}
|
||||
}
|
||||
}, [accessToken, subscribeToWebSocket]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setApplicantInfo(null);
|
||||
setAccessToken(null);
|
||||
setUserId(null);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
hasSubscribedRef.current = false;
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
hasSubscribedRef.current = false;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// If we have applicant info, show that
|
||||
if (applicantInfo) {
|
||||
return (
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<YStack
|
||||
gap="$4"
|
||||
alignItems="center"
|
||||
backgroundColor="white"
|
||||
flex={1}
|
||||
paddingHorizontal="$4"
|
||||
paddingTop="$4"
|
||||
paddingBottom={paddingBottom}
|
||||
>
|
||||
{/* Back Button */}
|
||||
<XStack width="100%" justifyContent="flex-start">
|
||||
<Button
|
||||
backgroundColor="transparent"
|
||||
borderRadius="$2"
|
||||
paddingHorizontal="$0"
|
||||
onPress={() => navigation.goBack()}
|
||||
icon={<ChevronLeft size={24} color={slate600} />}
|
||||
>
|
||||
<Text
|
||||
color={slate600}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Back
|
||||
</Text>
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
{/* Success Header */}
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={green500}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text
|
||||
fontSize="$7"
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
✓ Verification Complete
|
||||
</Text>
|
||||
<Text fontSize="$4" color={white} fontFamily={dinot} marginTop="$2">
|
||||
Your verification was successful
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
{/* Applicant Info */}
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={slate200}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
gap="$3"
|
||||
>
|
||||
<Text
|
||||
fontSize="$6"
|
||||
color={slate600}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Applicant Information
|
||||
</Text>
|
||||
|
||||
<YStack gap="$2">
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Name:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.info?.firstName || 'N/A'}{' '}
|
||||
{applicantInfo.info?.lastName || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Date of Birth:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.info?.dob || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Country:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.info?.country || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Phone:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.info?.phone || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Email:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.email || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Review Result:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={green500}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.review.reviewAnswer}
|
||||
</Text>
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
{/* Raw JSON */}
|
||||
<YStack
|
||||
marginTop="$2"
|
||||
backgroundColor={white}
|
||||
borderRadius="$2"
|
||||
padding="$3"
|
||||
>
|
||||
<Text
|
||||
fontSize="$3"
|
||||
color={slate400}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
marginBottom="$2"
|
||||
>
|
||||
Raw Data:
|
||||
</Text>
|
||||
<Text fontSize="$2" color={slate500} fontFamily={dinot}>
|
||||
{JSON.stringify(applicantInfo, null, 2)}
|
||||
</Text>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
<Button
|
||||
backgroundColor={slate600}
|
||||
borderRadius="$2"
|
||||
height="$6"
|
||||
width="100%"
|
||||
onPress={handleReset}
|
||||
>
|
||||
<Text
|
||||
color={white}
|
||||
fontSize="$6"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Start New Verification
|
||||
</Text>
|
||||
</Button>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<YStack
|
||||
gap="$4"
|
||||
alignItems="center"
|
||||
backgroundColor="white"
|
||||
flex={1}
|
||||
paddingHorizontal="$4"
|
||||
paddingTop="$4"
|
||||
paddingBottom={paddingBottom}
|
||||
>
|
||||
{/* Back Button */}
|
||||
<XStack width="100%" justifyContent="flex-start">
|
||||
<Button
|
||||
backgroundColor="transparent"
|
||||
borderRadius="$2"
|
||||
paddingHorizontal="$0"
|
||||
onPress={() => navigation.goBack()}
|
||||
icon={<ChevronLeft size={24} color={slate600} />}
|
||||
>
|
||||
<Text
|
||||
color={slate600}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Back
|
||||
</Text>
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
{/* TEE Service Status */}
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={slate200}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
>
|
||||
<Text
|
||||
fontSize="$5"
|
||||
color={slate600}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
TEE Service
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$3"
|
||||
color={slate500}
|
||||
fontFamily={dinot}
|
||||
marginTop="$2"
|
||||
>
|
||||
{SUMSUB_TEE_URL}
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
{/* Phone Number Input */}
|
||||
<YStack width="100%" gap="$2">
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate600}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Phone Number
|
||||
</Text>
|
||||
<TextInput
|
||||
value={phoneNumber}
|
||||
onChangeText={setPhoneNumber}
|
||||
placeholder="+11234567890"
|
||||
keyboardType="phone-pad"
|
||||
style={{
|
||||
backgroundColor: white,
|
||||
borderWidth: 1,
|
||||
borderColor: slate200,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
fontFamily: dinot,
|
||||
color: slate800,
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
{/* Generate Token Button */}
|
||||
<Button
|
||||
backgroundColor={slate600}
|
||||
borderRadius="$2"
|
||||
height="$6"
|
||||
width="100%"
|
||||
onPress={handleFetchToken}
|
||||
disabled={loading || !phoneNumber}
|
||||
opacity={loading || !phoneNumber ? 0.5 : 1}
|
||||
>
|
||||
<Text color={white} fontSize="$6" fontFamily={dinot} fontWeight="600">
|
||||
{loading ? 'Requesting token…' : 'Generate Access Token'}
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
{/* Token Status */}
|
||||
{accessToken && (
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={green500}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
>
|
||||
<Text
|
||||
fontSize="$5"
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
✓ Access Token Generated
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot} marginTop="$2">
|
||||
User ID: {userId}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$2"
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
marginTop="$2"
|
||||
opacity={0.8}
|
||||
>
|
||||
Token: {accessToken.substring(0, 30)}...
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
{/* Launch SDK Button */}
|
||||
{accessToken && (
|
||||
<Button
|
||||
backgroundColor={green500}
|
||||
borderRadius="$2"
|
||||
height="$6"
|
||||
width="100%"
|
||||
onPress={handleLaunchSumsub}
|
||||
disabled={sdkLaunching}
|
||||
opacity={sdkLaunching ? 0.5 : 1}
|
||||
>
|
||||
<Text
|
||||
color={white}
|
||||
fontSize="$6"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{sdkLaunching ? 'Launching…' : 'Launch Sumsub SDK'}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={red500}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
>
|
||||
<Text
|
||||
fontSize="$5"
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Error
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot} marginTop="$2">
|
||||
{error}
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
{/* SDK Result Display */}
|
||||
{result && (
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={slate200}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
gap="$2"
|
||||
>
|
||||
<Text
|
||||
fontSize="$6"
|
||||
color={slate600}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
SDK Result
|
||||
</Text>
|
||||
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Success:{' '}
|
||||
<Text
|
||||
fontWeight="600"
|
||||
color={result.success ? green500 : red500}
|
||||
>
|
||||
{result.success ? 'Yes' : 'No'}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Status:{' '}
|
||||
<Text fontWeight="600" color={slate600}>
|
||||
{result.status}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
{result.errorType && (
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Error Type:{' '}
|
||||
<Text fontWeight="600" color={red500}>
|
||||
{result.errorType}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{result.errorMsg && (
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Error Message:{' '}
|
||||
<Text fontWeight="600" color={red500}>
|
||||
{result.errorMsg}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
<Text
|
||||
fontSize="$3"
|
||||
color={slate500}
|
||||
fontFamily={dinot}
|
||||
marginTop="$2"
|
||||
>
|
||||
Waiting for verification results from WebSocket...
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
{/* Instructions */}
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={yellow500}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
gap="$2"
|
||||
>
|
||||
<Text fontSize="$5" color={white} fontFamily={dinot} fontWeight="600">
|
||||
Instructions
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
1. Make sure the TEE service is running at {SUMSUB_TEE_URL}
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
2. Enter a phone number and tap "Generate Access Token"
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
3. Tap "Launch Sumsub SDK" to start verification
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
4. Complete the verification flow
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
5. Results will appear automatically via WebSocket
|
||||
</Text>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SumsubTestScreen;
|
||||
56
app/src/types/sumsub.d.ts
vendored
Normal file
56
app/src/types/sumsub.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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.
|
||||
|
||||
declare module '@sumsub/react-native-mobilesdk-module' {
|
||||
export interface SumsubEvent {
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SumsubHandlers {
|
||||
onStatusChanged?: (event: SumsubStatusChangedEvent) => void;
|
||||
onLog?: (event: SumsubLogEvent) => void;
|
||||
onEvent?: (event: SumsubEvent) => void;
|
||||
}
|
||||
|
||||
export interface SumsubLogEvent {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface SumsubResult {
|
||||
success: boolean;
|
||||
status: string;
|
||||
errorType?: string;
|
||||
errorMsg?: string;
|
||||
}
|
||||
|
||||
export interface SumsubSDK {
|
||||
withHandlers(handlers: SumsubHandlers): SumsubSDK;
|
||||
withDebug(debug: boolean): SumsubSDK;
|
||||
withLocale(locale: string): SumsubSDK;
|
||||
withAnalyticsEnabled(enabled: boolean): SumsubSDK;
|
||||
withAutoCloseOnApprove(seconds: number): SumsubSDK;
|
||||
withApplicantConf(config: { email?: string; phone?: string }): SumsubSDK;
|
||||
withPreferredDocumentDefinitions(
|
||||
definitions: Record<string, { idDocType: string; country: string }>,
|
||||
): SumsubSDK;
|
||||
withDisableMLKit(disable: boolean): SumsubSDK;
|
||||
withStrings(strings: Record<string, string>): SumsubSDK;
|
||||
build(): SumsubSDK;
|
||||
launch(): Promise<SumsubResult>;
|
||||
dismiss(): void;
|
||||
}
|
||||
|
||||
export interface SumsubStatusChangedEvent {
|
||||
prevStatus: string;
|
||||
newStatus: string;
|
||||
}
|
||||
|
||||
export default class SNSMobileSDK {
|
||||
static init(
|
||||
accessToken: string,
|
||||
tokenExpirationHandler: () => Promise<string>,
|
||||
): SumsubSDK;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
// This pattern avoids hoisting issues with jest.mock
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { parseScanResponse, scan } from '@/integrations/nfc/nfcScanner';
|
||||
import { scan } from '@/integrations/nfc/nfcScanner';
|
||||
import { PassportReader } from '@/integrations/nfc/passportReader';
|
||||
|
||||
// Declare global variable for platform OS that can be modified per-test
|
||||
@@ -37,6 +37,23 @@ jest.mock('react-native', () => ({
|
||||
// Ensure the Node Buffer implementation is available to the module under test
|
||||
global.Buffer = Buffer;
|
||||
|
||||
// The static import above captures Platform.OS at load time. To test different platforms,
|
||||
// we need to clear the module cache and re-import with the current global.mockPlatformOS.
|
||||
const getFreshParseScanResponse = () => {
|
||||
jest.resetModules();
|
||||
jest.doMock('react-native', () => ({
|
||||
Platform: {
|
||||
get OS() {
|
||||
return global.mockPlatformOS;
|
||||
},
|
||||
Version: 14,
|
||||
select: (obj: Record<string, unknown>) =>
|
||||
obj[global.mockPlatformOS] || obj.default,
|
||||
},
|
||||
}));
|
||||
return require('@/integrations/nfc/nfcScanner').parseScanResponse;
|
||||
};
|
||||
|
||||
describe('parseScanResponse', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -44,8 +61,9 @@ describe('parseScanResponse', () => {
|
||||
global.mockPlatformOS = 'ios';
|
||||
});
|
||||
|
||||
it.skip('parses iOS response', () => {
|
||||
it('parses iOS response', () => {
|
||||
// Platform.OS is already mocked as 'ios' by default
|
||||
const parseScanResponse = getFreshParseScanResponse();
|
||||
const mrz =
|
||||
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14';
|
||||
const response = JSON.stringify({
|
||||
@@ -65,7 +83,6 @@ describe('parseScanResponse', () => {
|
||||
`"{"dataGroupHashes":"{\\"DG1\\":{\\"sodHash\\":\\"abcd\\"},\\"DG2\\":{\\"sodHash\\":\\"1234\\"}}","eContentBase64":"ZWM=","signedAttributes":"c2E=","passportMRZ":"P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14","signatureBase64":"AQI=","dataGroupsPresent":[1,2],"passportPhoto":"photo","documentSigningCertificate":"{\\"PEM\\":\\"CERT\\"}"}"`,
|
||||
);
|
||||
const result = parseScanResponse(response);
|
||||
console.log('Parsed Result:', result);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"dg1Hash": [
|
||||
@@ -111,6 +128,7 @@ describe('parseScanResponse', () => {
|
||||
it('parses Android response', () => {
|
||||
// Set Platform.OS to android for this test
|
||||
global.mockPlatformOS = 'android';
|
||||
const parseScanResponse = getFreshParseScanResponse();
|
||||
|
||||
const mrz =
|
||||
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14';
|
||||
@@ -178,6 +196,7 @@ describe('parseScanResponse', () => {
|
||||
|
||||
it('handles malformed iOS response', () => {
|
||||
// Platform.OS is already mocked as 'ios' by default
|
||||
const parseScanResponse = getFreshParseScanResponse();
|
||||
const response = '{"invalid": "json"';
|
||||
|
||||
expect(() => parseScanResponse(response)).toThrow();
|
||||
@@ -186,6 +205,7 @@ describe('parseScanResponse', () => {
|
||||
it('handles malformed Android response', () => {
|
||||
// Set Platform.OS to android for this test
|
||||
global.mockPlatformOS = 'android';
|
||||
const parseScanResponse = getFreshParseScanResponse();
|
||||
|
||||
const response = {
|
||||
mrz: 'valid_mrz',
|
||||
@@ -198,6 +218,7 @@ describe('parseScanResponse', () => {
|
||||
|
||||
it('handles missing required fields', () => {
|
||||
// Platform.OS is already mocked as 'ios' by default
|
||||
const parseScanResponse = getFreshParseScanResponse();
|
||||
const response = JSON.stringify({
|
||||
// Providing minimal data but missing critical passportMRZ field
|
||||
dataGroupHashes: JSON.stringify({
|
||||
@@ -217,6 +238,7 @@ describe('parseScanResponse', () => {
|
||||
|
||||
it('handles invalid hex data in dataGroupHashes', () => {
|
||||
// Platform.OS is already mocked as 'ios' by default
|
||||
const parseScanResponse = getFreshParseScanResponse();
|
||||
const response = JSON.stringify({
|
||||
dataGroupHashes: JSON.stringify({
|
||||
DG1: { sodHash: 'invalid_hex' },
|
||||
|
||||
@@ -24,6 +24,24 @@ jest.mock('@/services/analytics', () => ({
|
||||
flush: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock Sumsub SDK to prevent ES module parsing errors in isolateModules
|
||||
jest.mock('@sumsub/react-native-mobilesdk-module', () => {
|
||||
const createBuilder = () => ({
|
||||
withHandlers: jest.fn().mockReturnThis(),
|
||||
withDebug: jest.fn().mockReturnThis(),
|
||||
withLocale: jest.fn().mockReturnThis(),
|
||||
withAnalyticsEnabled: jest.fn().mockReturnThis(),
|
||||
build: jest.fn().mockReturnValue({
|
||||
launch: jest.fn().mockResolvedValue({ success: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
default: { init: jest.fn(() => createBuilder()) },
|
||||
};
|
||||
});
|
||||
|
||||
describe('navigation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -88,6 +106,7 @@ describe('navigation', () => {
|
||||
'ShowRecoveryPhrase',
|
||||
'Splash',
|
||||
'StarfallPushCode',
|
||||
'SumsubTest',
|
||||
'WebView',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"ios": {
|
||||
"build": 207,
|
||||
"lastDeployed": "2026-01-22T05:26:53.814Z"
|
||||
"build": 208,
|
||||
"lastDeployed": "2026-01-22T23:03:43.685Z"
|
||||
},
|
||||
"android": {
|
||||
"build": 137,
|
||||
"lastDeployed": "2026-01-22T00:34:57.056Z"
|
||||
"build": 138,
|
||||
"lastDeployed": "2026-01-22T23:03:43.685Z"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ template VC_AND_DISCLOSE_KYC(
|
||||
var country_length = COUNTRY_LENGTH();
|
||||
var id_number_length = ID_NUMBER_LENGTH();
|
||||
var idNumberIdx = ID_NUMBER_INDEX();
|
||||
var compressed_bit_len = max_length/2;
|
||||
var compressed_bit_len = max_length % 2 == 1 ? (max_length - 1)/2 : max_length / 2;
|
||||
|
||||
signal input data_padded[max_length];
|
||||
signal input compressed_disclose_sel[2];
|
||||
@@ -86,14 +86,14 @@ template VC_AND_DISCLOSE_KYC(
|
||||
low_bits.in <== compressed_disclose_sel[0];
|
||||
|
||||
// Convert disclose_sel_high (next 133 bits) to bit array
|
||||
component high_bits = Num2Bits(compressed_bit_len);
|
||||
component high_bits = Num2Bits(max_length - compressed_bit_len);
|
||||
high_bits.in <== compressed_disclose_sel[1];
|
||||
|
||||
// Combine the bit arrays (little-endian format)
|
||||
for(var i = 0; i < compressed_bit_len; i++){
|
||||
disclose_sel[i] <== low_bits.out[i];
|
||||
}
|
||||
for(var i = 0; i < compressed_bit_len; i++){
|
||||
for(var i = 0; i < max_length - compressed_bit_len; i++){
|
||||
disclose_sel[compressed_bit_len + i] <== high_bits.out[i];
|
||||
}
|
||||
|
||||
@@ -135,3 +135,14 @@ template VC_AND_DISCLOSE_KYC(
|
||||
signal output nullifier <== Poseidon(2)([secret, scope]);
|
||||
signal output attestation_id <== 4;
|
||||
}
|
||||
|
||||
component main {
|
||||
public [
|
||||
scope,
|
||||
merkle_root,
|
||||
ofac_name_dob_smt_root,
|
||||
ofac_name_yob_smt_root,
|
||||
user_identifier,
|
||||
current_date
|
||||
]
|
||||
} = VC_AND_DISCLOSE_KYC(40, 64, 64, 33);
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "./vc_and_disclose_kyc.circom";
|
||||
|
||||
component main {
|
||||
public [
|
||||
scope,
|
||||
merkle_root,
|
||||
ofac_name_dob_smt_root,
|
||||
ofac_name_yob_smt_root,
|
||||
user_identifier,
|
||||
current_date,
|
||||
attestation_id
|
||||
]
|
||||
} = VC_AND_DISCLOSE_KYC(40, 64, 64, 33);
|
||||
@@ -114,15 +114,16 @@ template REGISTER_AADHAAR(n, k, maxDataLength){
|
||||
|
||||
|
||||
component qrDataHasher = PackBytesAndPoseidon(maxDataLength);
|
||||
for (var i = 0; i < 9; i++){
|
||||
qrDataHasher.in[i] <== qrDataPadded[i];
|
||||
}
|
||||
for (var i = 9; i < 26; i++) {
|
||||
qrDataHasher.in[i] <== 0;
|
||||
}
|
||||
for (var i = 26; i < maxDataLength; i++){
|
||||
qrDataHasher.in[i] <== qrDataPadded[i];
|
||||
}
|
||||
qrDataHasher.in <== qrDataPadded;
|
||||
// for (var i = 0; i < 9; i++){
|
||||
// qrDataHasher.in[i] <== qrDataPadded[i];
|
||||
// }
|
||||
// for (var i = 9; i < 26; i++) {
|
||||
// qrDataHasher.in[i] <== 0;
|
||||
// }
|
||||
// for (var i = 26; i < maxDataLength; i++){
|
||||
// qrDataHasher.in[i] <== qrDataPadded[i];
|
||||
// }
|
||||
|
||||
// Generate commitment
|
||||
component packedCommitment = PackBytesAndPoseidon(42 + 62);
|
||||
|
||||
@@ -5,12 +5,14 @@ include "circomlib/circuits/babyjub.circom";
|
||||
include "../utils/kyc/constants.circom";
|
||||
include "../utils/passport/customHashers.circom";
|
||||
include "../utils/kyc/verifySignature.circom";
|
||||
include "circomlib/circuits/eddsaPoseidon.circom";
|
||||
include "circomlib/circuits/eddsaposeidon.circom";
|
||||
|
||||
template REGISTER_KYC() {
|
||||
var max_length = KYC_MAX_LENGTH();
|
||||
var country_length = COUNTRY_LENGTH();
|
||||
var id_number_length = ID_NUMBER_LENGTH();
|
||||
var id_type_length = ID_TYPE_LENGTH();
|
||||
var id_type_index = ID_TYPE_INDEX();
|
||||
var idNumberIdx = ID_NUMBER_INDEX();
|
||||
|
||||
signal input data_padded[max_length];
|
||||
@@ -49,7 +51,22 @@ template REGISTER_KYC() {
|
||||
for (var i = 0; i < id_number_length; i++) {
|
||||
id_num[i] <== data_padded[idNumberIdx + i];
|
||||
}
|
||||
signal output nullifier <== PackBytesAndPoseidon(id_number_length)(id_num);
|
||||
|
||||
signal nullifier_inputs[6 + id_number_length + id_type_length];
|
||||
|
||||
nullifier_inputs[0] <== 115; //s
|
||||
nullifier_inputs[1] <== 117; //u
|
||||
nullifier_inputs[2] <== 109; //m
|
||||
nullifier_inputs[3] <== 115; //s
|
||||
nullifier_inputs[4] <== 117; //u
|
||||
nullifier_inputs[5] <== 98; //b
|
||||
for (var i = 0; i < id_number_length; i++) {
|
||||
nullifier_inputs[i + 6] <== id_num[i];
|
||||
}
|
||||
for (var i = 0; i < id_type_length; i++) {
|
||||
nullifier_inputs[i + 6 + id_number_length] <== data_padded[id_type_index + i];
|
||||
}
|
||||
signal output nullifier <== PackBytesAndPoseidon(6 + id_number_length + id_type_length)(nullifier_inputs);
|
||||
signal output commitment <== Poseidon(2)([secret, msg_hasher.out]);
|
||||
|
||||
signal output pubkey_hash <== Poseidon(2)([verifyIdCommSig.Ax, verifyIdCommSig.Ay]);
|
||||
|
||||
@@ -72,20 +72,12 @@ function PHONE_NUMBER_LENGTH() {
|
||||
return 12;
|
||||
}
|
||||
|
||||
function DOCUMENT_INDEX() {
|
||||
function GENDER_INDEX() {
|
||||
return PHONE_NUMBER_INDEX() + PHONE_NUMBER_LENGTH();
|
||||
}
|
||||
|
||||
function DOCUMENT_LENGTH() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
function GENDER_INDEX() {
|
||||
return DOCUMENT_INDEX() + DOCUMENT_LENGTH();
|
||||
}
|
||||
|
||||
function GENDER_LENGTH() {
|
||||
return 6;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function ADDRESS_INDEX() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# run from root
|
||||
# first argument should register | dsc | disclose
|
||||
if [[ $1 != "register" && $1 != "dsc" && $1 != "disclose" && $1 != "register_id" && $1 != "register_aadhaar" ]]; then
|
||||
if [[ $1 != "register" && $1 != "dsc" && $1 != "disclose" && $1 != "register_id" && $1 != "register_aadhaar" && $1 != "register_kyc" ]]; then
|
||||
echo "first argument should be register | dsc | disclose | register_id | register_aadhaar"
|
||||
exit 1
|
||||
fi
|
||||
@@ -76,10 +76,15 @@ REGISTER_AADHAAR_CIRCUITS=(
|
||||
"register_aadhaar:true"
|
||||
)
|
||||
|
||||
REGISTER_KYC_CIRCUITS=(
|
||||
"register_kyc:true"
|
||||
)
|
||||
|
||||
DISCLOSE_CIRCUITS=(
|
||||
"vc_and_disclose:true"
|
||||
"vc_and_disclose_id:true"
|
||||
"vc_and_disclose_aadhaar:true"
|
||||
"vc_and_disclose_kyc:true"
|
||||
)
|
||||
|
||||
DSC_CIRCUITS=(
|
||||
@@ -124,6 +129,11 @@ elif [[ $1 == "register_aadhaar" ]]; then
|
||||
output="output/register"
|
||||
mkdir -p $output
|
||||
basepath="./circuits/circuits/register/instances"
|
||||
elif [[ $1 == "register_kyc" ]]; then
|
||||
allowed_circuits=("${REGISTER_KYC_CIRCUITS[@]}")
|
||||
output="output/register"
|
||||
mkdir -p $output
|
||||
basepath="./circuits/circuits/register/instances"
|
||||
elif [[ $1 == "dsc" ]]; then
|
||||
allowed_circuits=("${DSC_CIRCUITS[@]}")
|
||||
output="output/dsc"
|
||||
|
||||
@@ -18,7 +18,7 @@ CIRCUITS=(
|
||||
# "vc_and_disclose:20:true"
|
||||
# "vc_and_disclose_id:20:true"
|
||||
# "vc_and_disclose_aadhaar:20:true"
|
||||
"vc_and_disclose_selfrica:17:true"
|
||||
"vc_and_disclose_kyc:17:true"
|
||||
)
|
||||
|
||||
build_circuits "$CIRCUIT_TYPE" "$OUTPUT_DIR" "${CIRCUITS[@]}"
|
||||
|
||||
@@ -15,7 +15,7 @@ OUTPUT_DIR="build/${CIRCUIT_TYPE}"
|
||||
# Define circuits and their configurations
|
||||
# format: name:poweroftau:build_flag
|
||||
CIRCUITS=(
|
||||
"register_selfrica:15:true"
|
||||
"register_kyc:15:true"
|
||||
)
|
||||
|
||||
build_circuits "$CIRCUIT_TYPE" "$OUTPUT_DIR" "${CIRCUITS[@]}"
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -154,6 +154,33 @@ describe('VC_AND_DISCLOSE KYC Circuit Tests', () => {
|
||||
deepEqual(ofac_results, ['\x00', '\x00']);
|
||||
});
|
||||
|
||||
it('should return 0 for an OFAC person with reverse', async function () {
|
||||
this.timeout(0);
|
||||
const input = generateKycDiscloseInput(
|
||||
true,
|
||||
namedob_smt,
|
||||
nameyob_smt,
|
||||
tree as any,
|
||||
true,
|
||||
'0',
|
||||
'1234567890',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
'1234',
|
||||
true
|
||||
);
|
||||
const witness = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const revealedData_packed = await getRevealedDataPacked(witness);
|
||||
const revealedDataUnpacked = unpackReveal(revealedData_packed, 'id');
|
||||
const ofac_results = revealedDataUnpacked.slice(maxLength, maxLength + 2);
|
||||
|
||||
deepEqual(ofac_results, ['\x00', '\x00']);
|
||||
});
|
||||
|
||||
it('should return 1 for a non OFAC person', async function () {
|
||||
this.timeout(0);
|
||||
const input = generateKycDiscloseInput(
|
||||
@@ -193,7 +220,6 @@ describe('VC_AND_DISCLOSE KYC Circuit Tests', () => {
|
||||
'DOB',
|
||||
'PHOTO_HASH',
|
||||
'PHONE_NUMBER',
|
||||
'DOCUMENT',
|
||||
'GENDER',
|
||||
'ADDRESS',
|
||||
];
|
||||
@@ -251,7 +277,6 @@ describe('VC_AND_DISCLOSE KYC Circuit Tests', () => {
|
||||
'DOB',
|
||||
'PHOTO_HASH',
|
||||
'PHONE_NUMBER',
|
||||
'DOCUMENT',
|
||||
'GENDER',
|
||||
'ADDRESS',
|
||||
];
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('REGISTER AADHAAR Circuit Tests', function () {
|
||||
const w = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(w);
|
||||
});
|
||||
it.skip('should pass constrain and output correct nullifier and commitment', async function () {
|
||||
it('should pass constrain and output correct nullifier and commitment', async function () {
|
||||
this.timeout(0);
|
||||
const { inputs, nullifier, commitment } = prepareAadhaarRegisterTestData(
|
||||
privateKeyPem,
|
||||
|
||||
@@ -3,9 +3,17 @@ import { wasm as wasmTester } from 'circom_tester';
|
||||
import path from 'path';
|
||||
import { packBytesAndPoseidon } from '@selfxyz/common/utils/hash';
|
||||
import { poseidon2 } from 'poseidon-lite';
|
||||
import { generateMockKycRegisterInput } from '@selfxyz/common/utils/kyc/generateInputs.js';
|
||||
import {
|
||||
generateKycRegisterInput,
|
||||
generateMockKycRegisterInput,
|
||||
} from '@selfxyz/common/utils/kyc/generateInputs.js';
|
||||
import { KycRegisterInput } from '@selfxyz/common/utils/kyc/types';
|
||||
import { KYC_ID_NUMBER_INDEX, KYC_ID_NUMBER_LENGTH } from '@selfxyz/common/utils/kyc/constants';
|
||||
import {
|
||||
KYC_ID_NUMBER_INDEX,
|
||||
KYC_ID_NUMBER_LENGTH,
|
||||
KYC_ID_TYPE_INDEX,
|
||||
KYC_ID_TYPE_LENGTH,
|
||||
} from '@selfxyz/common/utils/kyc/constants';
|
||||
|
||||
describe('REGISTER KYC Circuit Tests', () => {
|
||||
let circuit: any;
|
||||
@@ -15,7 +23,7 @@ describe('REGISTER KYC Circuit Tests', () => {
|
||||
this.timeout(0);
|
||||
input = await generateMockKycRegisterInput(null, true, undefined);
|
||||
circuit = await wasmTester(
|
||||
path.join(__dirname, '../../circuits/register/instances/register_selfrica.circom'),
|
||||
path.join(__dirname, '../../circuits/register/instances/register_kyc.circom'),
|
||||
{
|
||||
verbose: true,
|
||||
logOutput: true,
|
||||
@@ -42,7 +50,12 @@ describe('REGISTER KYC Circuit Tests', () => {
|
||||
KYC_ID_NUMBER_INDEX,
|
||||
KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH
|
||||
);
|
||||
const nullifier = packBytesAndPoseidon(idnumber.map((x) => Number(x)));
|
||||
const nullifierInputs = [
|
||||
...'sumsub'.split('').map((x) => x.charCodeAt(0)),
|
||||
...idnumber,
|
||||
...input.data_padded.slice(KYC_ID_TYPE_INDEX, KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH),
|
||||
];
|
||||
const nullifier = packBytesAndPoseidon(nullifierInputs);
|
||||
const commitment = poseidon2([
|
||||
input.secret,
|
||||
packBytesAndPoseidon(input.data_padded.map((x) => Number(x))),
|
||||
|
||||
@@ -138,6 +138,7 @@ export {
|
||||
generateMockKycRegisterInput,
|
||||
NON_OFAC_DUMMY_INPUT,
|
||||
OFAC_DUMMY_INPUT,
|
||||
generateKycRegisterInput,
|
||||
} from './src/utils/kyc/generateInputs.js';
|
||||
|
||||
export {
|
||||
|
||||
90
common/src/utils/kyc/api.ts
Normal file
90
common/src/utils/kyc/api.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
//Helper function to destructure the kyc data from the api response
|
||||
import { Point } from '@zk-kit/baby-jubjub';
|
||||
import {
|
||||
KYC_ADDRESS_INDEX,
|
||||
KYC_ADDRESS_LENGTH,
|
||||
KYC_COUNTRY_INDEX,
|
||||
KYC_COUNTRY_LENGTH,
|
||||
KYC_DOB_INDEX,
|
||||
KYC_DOB_LENGTH,
|
||||
KYC_EXPIRY_DATE_INDEX,
|
||||
KYC_EXPIRY_DATE_LENGTH,
|
||||
KYC_FULL_NAME_INDEX,
|
||||
KYC_FULL_NAME_LENGTH,
|
||||
KYC_GENDER_INDEX,
|
||||
KYC_GENDER_LENGTH,
|
||||
KYC_ID_NUMBER_INDEX,
|
||||
KYC_ID_NUMBER_LENGTH,
|
||||
KYC_ID_TYPE_INDEX,
|
||||
KYC_ID_TYPE_LENGTH,
|
||||
KYC_ISSUANCE_DATE_INDEX,
|
||||
KYC_ISSUANCE_DATE_LENGTH,
|
||||
KYC_PHONE_NUMBER_INDEX,
|
||||
KYC_PHONE_NUMBER_LENGTH,
|
||||
KYC_PHOTO_HASH_INDEX,
|
||||
KYC_PHOTO_HASH_LENGTH,
|
||||
} from './constants.js';
|
||||
import { KycData } from './types.js';
|
||||
|
||||
//accepts a base64 signature and returns a signature object
|
||||
export function deserializeSignature(signature: string): { R: Point<bigint>; s: bigint } {
|
||||
const [Rx, Ry, s] = Buffer.from(signature, 'base64').toString('utf-8').split(',').map(BigInt);
|
||||
return { R: [Rx, Ry] as Point<bigint>, s };
|
||||
}
|
||||
|
||||
//accepts a base64 applicant info and returns a kyc data object
|
||||
export function deserializeApplicantInfo(
|
||||
applicantInfoBase64: string
|
||||
): Omit<
|
||||
KycData,
|
||||
'user_identifier' | 'current_date' | 'majority_age_ASCII' | 'selector_older_than'
|
||||
> {
|
||||
const applicantInfo = Buffer.from(applicantInfoBase64, 'base64').toString('utf-8');
|
||||
const country = applicantInfo
|
||||
.slice(KYC_COUNTRY_INDEX, KYC_COUNTRY_INDEX + KYC_COUNTRY_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const idType = applicantInfo
|
||||
.slice(KYC_ID_TYPE_INDEX, KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const idNumber = applicantInfo
|
||||
.slice(KYC_ID_NUMBER_INDEX, KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const issuanceDate = applicantInfo
|
||||
.slice(KYC_ISSUANCE_DATE_INDEX, KYC_ISSUANCE_DATE_INDEX + KYC_ISSUANCE_DATE_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const expiryDate = applicantInfo
|
||||
.slice(KYC_EXPIRY_DATE_INDEX, KYC_EXPIRY_DATE_INDEX + KYC_EXPIRY_DATE_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const fullName = applicantInfo
|
||||
.slice(KYC_FULL_NAME_INDEX, KYC_FULL_NAME_INDEX + KYC_FULL_NAME_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const dob = applicantInfo
|
||||
.slice(KYC_DOB_INDEX, KYC_DOB_INDEX + KYC_DOB_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const photoHash = applicantInfo
|
||||
.slice(KYC_PHOTO_HASH_INDEX, KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const phoneNumber = applicantInfo
|
||||
.slice(KYC_PHONE_NUMBER_INDEX, KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const gender = applicantInfo
|
||||
.slice(KYC_GENDER_INDEX, KYC_GENDER_INDEX + KYC_GENDER_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
const address = applicantInfo
|
||||
.slice(KYC_ADDRESS_INDEX, KYC_ADDRESS_INDEX + KYC_ADDRESS_LENGTH)
|
||||
.replace(/\x00/g, '');
|
||||
|
||||
return {
|
||||
country,
|
||||
idType,
|
||||
idNumber,
|
||||
issuanceDate,
|
||||
expiryDate,
|
||||
fullName,
|
||||
dob,
|
||||
photoHash,
|
||||
phoneNumber,
|
||||
gender,
|
||||
address,
|
||||
};
|
||||
}
|
||||
52
common/src/utils/kyc/build_kyc_ofac_smt.ts
Normal file
52
common/src/utils/kyc/build_kyc_ofac_smt.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// import * as fs from 'fs';
|
||||
// import { fileURLToPath } from 'url';
|
||||
// import { dirname, join } from 'path';
|
||||
|
||||
// import { buildKycSMT } from '../trees.js';
|
||||
|
||||
// async function build_kyc_ofac_smt() {
|
||||
// let startTime = performance.now();
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = dirname(__filename);
|
||||
// const baseInputPath = __dirname;
|
||||
// const baseOutputPath = join(__dirname, '../../../../circuits/tests/consts/ofac/');
|
||||
|
||||
// const names = JSON.parse(fs.readFileSync(join(baseInputPath, 'names.json'), 'utf-8'));
|
||||
|
||||
// // -----KYC DATA-----
|
||||
// console.log(`Reading data from ${baseInputPath}`);
|
||||
|
||||
// // -----KYC DATA-----
|
||||
// console.log('\nBuilding KYC SMTs...');
|
||||
// const nameAndDobKycTree = buildKycSMT(names, 'name_and_dob');
|
||||
// const nameAndYobKycTree = buildKycSMT(names, 'name_and_yob');
|
||||
|
||||
// console.log('\n--- Results ---');
|
||||
// console.log(
|
||||
// `KYC - Name & DOB Tree: Processed ${nameAndDobKycTree[0]}/${names.length} entries in ${nameAndDobKycTree[1].toFixed(2)} ms`
|
||||
// );
|
||||
// console.log(
|
||||
// `KYC - Name & YOB Tree: Processed ${nameAndYobKycTree[0]}/${names.length} entries in ${nameAndYobKycTree[1].toFixed(2)} ms`
|
||||
// );
|
||||
// console.log('Total Time:', (performance.now() - startTime).toFixed(2), 'ms');
|
||||
|
||||
// console.log(`\nExporting SMTs to ${baseOutputPath}...`);
|
||||
// const nameAndDobKycOfacJSON = nameAndDobKycTree[2].export();
|
||||
// const nameAndYobKycOfacJSON = nameAndYobKycTree[2].export();
|
||||
|
||||
// fs.writeFileSync(
|
||||
// `${baseOutputPath}nameAndDobKycSMT.json`,
|
||||
// JSON.stringify(nameAndDobKycOfacJSON)
|
||||
// );
|
||||
// fs.writeFileSync(
|
||||
// `${baseOutputPath}nameAndYobKycSMT.json`,
|
||||
// JSON.stringify(nameAndYobKycOfacJSON)
|
||||
// );
|
||||
|
||||
// console.log('✅ SMT export complete.');
|
||||
// }
|
||||
|
||||
// build_kyc_ofac_smt().catch((error) => {
|
||||
// console.error('Error building KYC OFAC SMTs:', error);
|
||||
// process.exit(1);
|
||||
// });
|
||||
@@ -5,7 +5,7 @@ export const KYC_ID_TYPE_INDEX = KYC_COUNTRY_INDEX + KYC_COUNTRY_LENGTH;
|
||||
export const KYC_ID_TYPE_LENGTH = 27;
|
||||
|
||||
export const KYC_ID_NUMBER_INDEX = KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH;
|
||||
export const KYC_ID_NUMBER_LENGTH = 32; // Updated: max(20, 32) = 32
|
||||
export const KYC_ID_NUMBER_LENGTH = 32;
|
||||
|
||||
export const KYC_ISSUANCE_DATE_INDEX = KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH;
|
||||
export const KYC_ISSUANCE_DATE_LENGTH = 8;
|
||||
@@ -25,11 +25,8 @@ export const KYC_PHOTO_HASH_LENGTH = 32;
|
||||
export const KYC_PHONE_NUMBER_INDEX = KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH;
|
||||
export const KYC_PHONE_NUMBER_LENGTH = 12;
|
||||
|
||||
export const KYC_DOCUMENT_INDEX = KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH;
|
||||
export const KYC_DOCUMENT_LENGTH = 32; // Updated: max(2, 32) = 32
|
||||
|
||||
export const KYC_GENDER_INDEX = KYC_DOCUMENT_INDEX + KYC_DOCUMENT_LENGTH;
|
||||
export const KYC_GENDER_LENGTH = 6;
|
||||
export const KYC_GENDER_INDEX = KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH;
|
||||
export const KYC_GENDER_LENGTH = 1;
|
||||
|
||||
export const KYC_ADDRESS_INDEX = KYC_GENDER_INDEX + KYC_GENDER_LENGTH;
|
||||
export const KYC_ADDRESS_LENGTH = 100;
|
||||
@@ -49,8 +46,7 @@ export const KYC_FIELD_LENGTHS = {
|
||||
DOB: KYC_DOB_LENGTH, // 8
|
||||
PHOTO_HASH: KYC_PHOTO_HASH_LENGTH, // 32
|
||||
PHONE_NUMBER: KYC_PHONE_NUMBER_LENGTH, // 12
|
||||
DOCUMENT: KYC_DOCUMENT_LENGTH, // 32 (updated)
|
||||
GENDER: KYC_GENDER_LENGTH, // 6
|
||||
GENDER: KYC_GENDER_LENGTH, // 1
|
||||
ADDRESS: KYC_ADDRESS_LENGTH, // 100
|
||||
} as const;
|
||||
|
||||
@@ -67,8 +63,7 @@ export const KYC_REVEAL_DATA_INDICES = {
|
||||
DOB: KYC_FULL_NAME_INDEX + KYC_FULL_NAME_LENGTH, // 142 (updated)
|
||||
PHOTO_HASH: KYC_DOB_INDEX + KYC_DOB_LENGTH, // 150 (updated)
|
||||
PHONE_NUMBER: KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH, // 182 (updated)
|
||||
DOCUMENT: KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH, // 194 (updated)
|
||||
GENDER: KYC_DOCUMENT_INDEX + KYC_DOCUMENT_LENGTH, // 226 (updated)
|
||||
GENDER: KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH, // 194 (updated)
|
||||
ADDRESS: KYC_GENDER_INDEX + KYC_GENDER_LENGTH, // 232 (updated)
|
||||
} as const;
|
||||
|
||||
@@ -106,14 +101,10 @@ export const KYC_SELECTOR_BITS = {
|
||||
{ length: KYC_PHONE_NUMBER_LENGTH },
|
||||
(_, i) => i + KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH
|
||||
) as number[], // 182-193 (updated)
|
||||
DOCUMENT: Array.from(
|
||||
{ length: KYC_DOCUMENT_LENGTH },
|
||||
(_, i) => i + KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH
|
||||
) as number[], // 194-225 (updated)
|
||||
GENDER: Array.from(
|
||||
{ length: KYC_GENDER_LENGTH },
|
||||
(_, i) => i + KYC_DOCUMENT_INDEX + KYC_DOCUMENT_LENGTH
|
||||
) as number[], // 226-231 (updated)
|
||||
(_, i) => i + KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH
|
||||
) as number[], // 194-194 (updated)
|
||||
ADDRESS: Array.from(
|
||||
{ length: KYC_ADDRESS_LENGTH },
|
||||
(_, i) => i + KYC_GENDER_INDEX + KYC_GENDER_LENGTH
|
||||
|
||||
@@ -14,6 +14,7 @@ import { signEdDSA } from './ecdsa/ecdsa.js';
|
||||
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
|
||||
import { packBytesAndPoseidon } from '../hash.js';
|
||||
import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js';
|
||||
import { deserializeApplicantInfo, deserializeSignature } from './api.js';
|
||||
|
||||
export const OFAC_DUMMY_INPUT: KycData = {
|
||||
country: 'KEN',
|
||||
@@ -25,8 +26,7 @@ export const OFAC_DUMMY_INPUT: KycData = {
|
||||
dob: '19481210',
|
||||
photoHash: '1234567890',
|
||||
phoneNumber: '1234567890',
|
||||
document: 'ID',
|
||||
gender: 'Male',
|
||||
gender: 'M',
|
||||
address: '1234567890',
|
||||
user_identifier: '1234567890',
|
||||
current_date: '20250101',
|
||||
@@ -44,8 +44,7 @@ export const NON_OFAC_DUMMY_INPUT: KycData = {
|
||||
dob: '19900101',
|
||||
photoHash: '1234567890',
|
||||
phoneNumber: '1234567890',
|
||||
document: 'ID',
|
||||
gender: 'Male',
|
||||
gender: 'M',
|
||||
address: '1234567890',
|
||||
user_identifier: '1234567890',
|
||||
current_date: '20250101',
|
||||
@@ -88,6 +87,31 @@ export const generateMockKycRegisterInput = async (
|
||||
return kycRegisterInput;
|
||||
};
|
||||
|
||||
export const generateKycRegisterInput = async (
|
||||
applicantInfoBase64: string,
|
||||
signatureBase64: string,
|
||||
pubkeyStr: [string, string],
|
||||
secret: string
|
||||
) => {
|
||||
const applicantInfo = deserializeApplicantInfo(applicantInfoBase64);
|
||||
const signature = deserializeSignature(signatureBase64);
|
||||
const pubkey = [BigInt(pubkeyStr[0]), BigInt(pubkeyStr[1])] as [bigint, bigint];
|
||||
|
||||
const serializedData = serializeKycData(applicantInfo);
|
||||
|
||||
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
|
||||
|
||||
const kycRegisterInput: KycRegisterInput = {
|
||||
data_padded: msgPadded.map((x) => Number(x)),
|
||||
s: signature.s,
|
||||
R: signature.R,
|
||||
pubKey: pubkey,
|
||||
secret,
|
||||
};
|
||||
|
||||
return kycRegisterInput;
|
||||
};
|
||||
|
||||
export const generateCircuitInputsOfac = (data: KycData, smt: SMT, proofLevel: number) => {
|
||||
const name = data.fullName;
|
||||
const dob = data.dob;
|
||||
@@ -124,9 +148,16 @@ export const generateKycDiscloseInput = (
|
||||
forbiddenCountriesList?: string[],
|
||||
minimumAge?: number,
|
||||
updateTree?: boolean,
|
||||
secret: string = '1234'
|
||||
secret: string = '1234',
|
||||
reverse?: boolean
|
||||
) => {
|
||||
const data = ofac_input ? OFAC_DUMMY_INPUT : NON_OFAC_DUMMY_INPUT;
|
||||
let data = ofac_input ? OFAC_DUMMY_INPUT : NON_OFAC_DUMMY_INPUT;
|
||||
if (reverse) {
|
||||
data = {
|
||||
...data,
|
||||
fullName: data.fullName.split(' ').reverse().join(' '),
|
||||
};
|
||||
}
|
||||
const serializedData = serializeKycData(data).padEnd(KYC_MAX_LENGTH, '\0');
|
||||
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
|
||||
const commitment = poseidon2([secret, packBytesAndPoseidon(msgPadded)]);
|
||||
|
||||
@@ -11,7 +11,6 @@ export type KycData = {
|
||||
dob: string;
|
||||
photoHash: string;
|
||||
phoneNumber: string;
|
||||
document: string;
|
||||
gender: string;
|
||||
address: string;
|
||||
user_identifier: string;
|
||||
@@ -20,11 +19,16 @@ export type KycData = {
|
||||
selector_older_than: string;
|
||||
};
|
||||
|
||||
export const serializeKycData = (kycData: KycData) => {
|
||||
export const serializeKycData = (
|
||||
kycData: Omit<
|
||||
KycData,
|
||||
'user_identifier' | 'current_date' | 'majority_age_ASCII' | 'selector_older_than'
|
||||
>
|
||||
) => {
|
||||
//ensure max length of each field
|
||||
let serializedData = '';
|
||||
serializedData += kycData.country.toUpperCase().padEnd(constants.KYC_COUNTRY_LENGTH, '\0');
|
||||
serializedData += kycData.idType.toUpperCase().padEnd(constants.KYC_ID_TYPE_LENGTH, '\0');
|
||||
serializedData += kycData.country.padEnd(constants.KYC_COUNTRY_LENGTH, '\0');
|
||||
serializedData += kycData.idType.padEnd(constants.KYC_ID_TYPE_LENGTH, '\0');
|
||||
serializedData += kycData.idNumber.padEnd(constants.KYC_ID_NUMBER_LENGTH, '\0');
|
||||
serializedData += kycData.issuanceDate.padEnd(constants.KYC_ISSUANCE_DATE_LENGTH, '\0');
|
||||
serializedData += kycData.expiryDate.padEnd(constants.KYC_EXPIRY_DATE_LENGTH, '\0');
|
||||
@@ -32,7 +36,6 @@ export const serializeKycData = (kycData: KycData) => {
|
||||
serializedData += kycData.dob.padEnd(constants.KYC_DOB_LENGTH, '\0');
|
||||
serializedData += kycData.photoHash.padEnd(constants.KYC_PHOTO_HASH_LENGTH, '\0');
|
||||
serializedData += kycData.phoneNumber.padEnd(constants.KYC_PHONE_NUMBER_LENGTH, '\0');
|
||||
serializedData += kycData.document.padEnd(constants.KYC_DOCUMENT_LENGTH, '\0');
|
||||
serializedData += kycData.gender.padEnd(constants.KYC_GENDER_LENGTH, '\0');
|
||||
serializedData += kycData.address.padEnd(constants.KYC_ADDRESS_LENGTH, '\0');
|
||||
|
||||
|
||||
@@ -723,20 +723,29 @@ export function buildKycSMT(field: any[], treetype: string): [number, number, SM
|
||||
console.log(`Processing ${providerName}`, treetype, 'number', i, 'out of', field.length);
|
||||
}
|
||||
|
||||
let leaf = BigInt(0);
|
||||
let leafs = [BigInt(0), BigInt(0)];
|
||||
if (treetype == 'name_and_dob') {
|
||||
leaf = processNameAndDobKyc(entry, i);
|
||||
leafs[0] = processNameAndDobKyc(entry, i, false);
|
||||
leafs[1] = processNameAndDobKyc(entry, i, true);
|
||||
} else if (treetype == 'name_and_yob') {
|
||||
leaf = processNameAndYobKyc(entry, i);
|
||||
leafs[0] = processNameAndYobKyc(entry, i, false);
|
||||
leafs[1] = processNameAndYobKyc(entry, i, true);
|
||||
}
|
||||
|
||||
if (leaf == BigInt(0) || tree.createProof(leaf).membership) {
|
||||
if (leafs[0] == BigInt(0) || tree.createProof(leafs[0]).membership) {
|
||||
console.log('This entry already exists in the tree, skipping...');
|
||||
continue;
|
||||
} else if (leafs[1] == BigInt(0) || tree.createProof(leafs[1]).membership) {
|
||||
console.log('This entry already exists in the tree, skipping...');
|
||||
continue;
|
||||
}
|
||||
|
||||
tree.add(leafs[0], BigInt(1));
|
||||
count += 1;
|
||||
tree.add(leaf, BigInt(1));
|
||||
if (leafs[0] != leafs[1]) {
|
||||
tree.add(leafs[1], BigInt(1));
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Total ${providerName}`, treetype, 'parsed are : ', count, ' over ', field.length);
|
||||
@@ -744,7 +753,7 @@ export function buildKycSMT(field: any[], treetype: string): [number, number, SM
|
||||
return [count, performance.now() - startTime, tree];
|
||||
}
|
||||
|
||||
const processNameAndDobKyc = (entry: any, i: number): bigint => {
|
||||
const processNameAndDobKyc = (entry: any, i: number, reverse: boolean): bigint => {
|
||||
const firstName = entry.First_Name;
|
||||
const lastName = entry.Last_Name;
|
||||
const day = entry.day;
|
||||
@@ -756,7 +765,7 @@ const processNameAndDobKyc = (entry: any, i: number): bigint => {
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
const nameHash = processNameKyc(firstName, lastName, i);
|
||||
const nameHash = processNameKyc(firstName, lastName, i, reverse);
|
||||
const dobHash = processDobKyc(day, month, year, i);
|
||||
|
||||
return generateSmallKey(poseidon2([dobHash, nameHash]));
|
||||
@@ -773,7 +782,12 @@ export const getNameDobLeafKyc = (name: string, dob: string) => {
|
||||
return generateSmallKey(poseidon2([dobHash, nameHash]));
|
||||
};
|
||||
|
||||
const processNameKyc = (firstName: string, lastName: string, i: number): bigint => {
|
||||
const processNameKyc = (
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
i: number,
|
||||
reverse: boolean
|
||||
): bigint => {
|
||||
const namePaddingLength = 64;
|
||||
|
||||
firstName = firstName.replace(/'/g, '');
|
||||
@@ -783,8 +797,8 @@ const processNameKyc = (firstName: string, lastName: string, i: number): bigint
|
||||
lastName = lastName.replace(/[- ]/g, '<');
|
||||
lastName = lastName.replace(/\./g, '');
|
||||
|
||||
//TODO: check if smile id does first name and last name || last name and first name
|
||||
const nameArr = (lastName + ' ' + firstName)
|
||||
let nameStr = reverse ? lastName + ' ' + firstName : firstName + ' ' + lastName;
|
||||
const nameArr = nameStr
|
||||
.padEnd(namePaddingLength, '\0')
|
||||
.split('')
|
||||
.map((char) => char.charCodeAt(0));
|
||||
@@ -825,7 +839,7 @@ export const getNameYobLeafKyc = (name: string, yob: string) => {
|
||||
return generateSmallKey(poseidon2([yearHash, nameHash]));
|
||||
};
|
||||
|
||||
const processNameAndYobKyc = (entry: any, i: number): bigint => {
|
||||
const processNameAndYobKyc = (entry: any, i: number, reverse: boolean): bigint => {
|
||||
const firstName = entry.First_Name;
|
||||
const lastName = entry.Last_Name;
|
||||
const year = entry.year;
|
||||
@@ -834,7 +848,7 @@ const processNameAndYobKyc = (entry: any, i: number): bigint => {
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
const nameHash = processNameKyc(firstName, lastName, i);
|
||||
const nameHash = processNameKyc(firstName, lastName, i, reverse);
|
||||
const yearHash = processYearKyc(year, i);
|
||||
return generateSmallKey(poseidon2([yearHash, nameHash]));
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {RegisterProofVerifierLib} from "./libraries/RegisterProofVerifierLib.sol
|
||||
import {DscProofVerifierLib} from "./libraries/DscProofVerifierLib.sol";
|
||||
import {RootCheckLib} from "./libraries/RootCheckLib.sol";
|
||||
import {OfacCheckLib} from "./libraries/OfacCheckLib.sol";
|
||||
import {console} from "hardhat/console.sol";
|
||||
|
||||
/**
|
||||
* @title IdentityVerificationHubImplV2
|
||||
|
||||
@@ -138,23 +138,12 @@ library CircuitConstantsV2 {
|
||||
passportNoSmtRootIndex: 99
|
||||
});
|
||||
} else if (attestationId == AttestationId.KYC) {
|
||||
// Selfrica circuit pubSignals layout (30 elements total):
|
||||
// [0-8] revealedData_packed (9 elements)
|
||||
// [9-12] forbidden_countries_list_packed (4 elements)
|
||||
// [13-15] nullifier + padding (3 elements)
|
||||
// [16] scope (public input)
|
||||
// [17] merkle_root (public input)
|
||||
// [18] ofac_name_dob_smt_root (public input)
|
||||
// [19] ofac_name_yob_smt_root (public input)
|
||||
// [20] user_identifier (public input)
|
||||
// [21-28] current_date (8 elements, public input)
|
||||
// [29] attestation_id (public input)
|
||||
return
|
||||
DiscloseIndices({
|
||||
revealedDataPackedIndex: 0,
|
||||
forbiddenCountriesListPackedIndex: 11,
|
||||
nullifierIndex: 15,
|
||||
attestationIdIndex: 29,
|
||||
forbiddenCountriesListPackedIndex: 10,
|
||||
nullifierIndex: 14,
|
||||
attestationIdIndex: 15,
|
||||
merkleRootIndex: 17,
|
||||
currentDateIndex: 21,
|
||||
namedobSmtRootIndex: 18,
|
||||
|
||||
@@ -64,6 +64,6 @@ interface IVcAndDiscloseSelfricaCircuitVerifier {
|
||||
uint256[2] calldata a,
|
||||
uint256[2][2] calldata b,
|
||||
uint256[2] calldata c,
|
||||
uint256[30] calldata pubSignals
|
||||
uint256[29] calldata pubSignals
|
||||
) external view returns (bool);
|
||||
}
|
||||
|
||||
@@ -125,14 +125,14 @@ library CircuitAttributeHandlerV2 {
|
||||
nationalityEnd: 2,
|
||||
dateOfBirthStart: 142,
|
||||
dateOfBirthEnd: 149,
|
||||
genderStart: 226,
|
||||
genderEnd: 232,
|
||||
genderStart: 194,
|
||||
genderEnd: 194,
|
||||
expiryDateStart: 70,
|
||||
expiryDateEnd: 77,
|
||||
olderThanStart: 334,
|
||||
olderThanEnd: 334,
|
||||
ofacStart: 332,
|
||||
ofacEnd: 333
|
||||
olderThanStart: 297,
|
||||
olderThanEnd: 297,
|
||||
ofacStart: 295,
|
||||
ofacEnd: 296
|
||||
});
|
||||
} else {
|
||||
revert("Invalid attestation ID");
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {console} from "hardhat/console.sol";
|
||||
|
||||
/**
|
||||
* @title Formatter Library
|
||||
* @notice A library providing utility functions to format names, dates, and encode data.
|
||||
|
||||
@@ -67,8 +67,8 @@ library ProofVerifierLib {
|
||||
revert InvalidVcAndDiscloseProof();
|
||||
}
|
||||
} else if (attestationId == AttestationId.KYC) {
|
||||
uint256[30] memory pubSignals;
|
||||
for (uint256 i = 0; i < 30; i++) {
|
||||
uint256[29] memory pubSignals;
|
||||
for (uint256 i = 0; i < 29; i++) {
|
||||
pubSignals[i] = vcAndDiscloseProof.pubSignals[i];
|
||||
}
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - Only Owner
|
||||
// External Functions - Role-Based Access Control
|
||||
// ====================================================
|
||||
|
||||
/// @notice Updates the hub address.
|
||||
|
||||
@@ -388,7 +388,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - Only Owner
|
||||
// External Functions - Role-Based Access Control
|
||||
// ====================================================
|
||||
|
||||
/**
|
||||
|
||||
@@ -411,7 +411,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - Only Owner
|
||||
// External Functions - Role-Based Access Control
|
||||
// ====================================================
|
||||
|
||||
/**
|
||||
|
||||
@@ -370,7 +370,7 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - Only Owner TODO: add only role(SECURITY_ROLE) or something
|
||||
// External Functions - Role-Based Access Control
|
||||
// ====================================================
|
||||
|
||||
/**
|
||||
@@ -378,7 +378,7 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newHubAddress The new address of the hub.
|
||||
*/
|
||||
function updateHub(address newHubAddress) external onlyProxy {
|
||||
function updateHub(address newHubAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
if (newHubAddress == address(0)) revert HUB_ADDRESS_ZERO();
|
||||
_hub = newHubAddress;
|
||||
emit HubUpdated(newHubAddress);
|
||||
@@ -389,7 +389,7 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newPCR0ManagerAddress The new address of the PCR0Manager.
|
||||
*/
|
||||
function updatePCR0Manager(address newPCR0ManagerAddress) external virtual onlyProxy {
|
||||
function updatePCR0Manager(address newPCR0ManagerAddress) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_PCR0Manager = newPCR0ManagerAddress;
|
||||
emit PCR0ManagerUpdated(newPCR0ManagerAddress);
|
||||
}
|
||||
@@ -399,7 +399,7 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param nameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndDobOfacRoot(uint256 nameAndDobOfacRoot) external virtual onlyProxy {
|
||||
function updateNameAndDobOfacRoot(uint256 nameAndDobOfacRoot) external virtual onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_nameAndDobOfacRoot = nameAndDobOfacRoot;
|
||||
emit NameAndDobOfacRootUpdated(nameAndDobOfacRoot);
|
||||
}
|
||||
@@ -409,7 +409,7 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param nameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndYobOfacRoot(uint256 nameAndYobOfacRoot) external virtual onlyProxy {
|
||||
function updateNameAndYobOfacRoot(uint256 nameAndYobOfacRoot) external virtual onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_nameAndYobOfacRoot = nameAndYobOfacRoot;
|
||||
emit NameAndYobOfacRootUpdated(nameAndYobOfacRoot);
|
||||
}
|
||||
@@ -419,7 +419,7 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param gcpRootCAPubkeyHash The new GCP root CA pubkey hash value.
|
||||
*/
|
||||
function updateGCPRootCAPubkeyHash(uint256 gcpRootCAPubkeyHash) external virtual onlyProxy {
|
||||
function updateGCPRootCAPubkeyHash(uint256 gcpRootCAPubkeyHash) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_gcpRootCAPubkeyHash = gcpRootCAPubkeyHash;
|
||||
emit GCPRootCAPubkeyHashUpdated(gcpRootCAPubkeyHash);
|
||||
}
|
||||
@@ -427,7 +427,7 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
/// @notice Updates the GCP JWT verifier contract address.
|
||||
/// @dev Callable only by the contract owner.
|
||||
/// @param verifier The new GCP JWT verifier address.
|
||||
function updateGCPJWTVerifier(address verifier) external onlyProxy {
|
||||
function updateGCPJWTVerifier(address verifier) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_gcpJwtVerifier = verifier;
|
||||
emit GCPJWTVerifierUpdated(verifier);
|
||||
}
|
||||
@@ -435,7 +435,7 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
/// @notice Updates the TEE address.
|
||||
/// @dev Callable only by the contract owner.
|
||||
/// @param teeAddress The new TEE address.
|
||||
function updateTEE(address teeAddress) external onlyProxy {
|
||||
function updateTEE(address teeAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_tee = teeAddress;
|
||||
emit TEEUpdated(teeAddress);
|
||||
}
|
||||
@@ -491,7 +491,10 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
/// @dev Callable only by the owner for testing or administration.
|
||||
/// @param nullifier The nullifier associated with the identity commitment.
|
||||
/// @param commitment The identity commitment to add.
|
||||
function devAddIdentityCommitment(uint256 nullifier, uint256 commitment) external onlyProxy {
|
||||
function devAddIdentityCommitment(
|
||||
uint256 nullifier,
|
||||
uint256 commitment
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_nullifiers[nullifier] = true;
|
||||
uint256 imt_root = _identityCommitmentIMT._insert(commitment);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
@@ -504,7 +507,11 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
/// @param oldLeaf The current identity commitment to update.
|
||||
/// @param newLeaf The new identity commitment.
|
||||
/// @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||
function devUpdateCommitment(uint256 oldLeaf, uint256 newLeaf, uint256[] calldata siblingNodes) external onlyProxy {
|
||||
function devUpdateCommitment(
|
||||
uint256 oldLeaf,
|
||||
uint256 newLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _identityCommitmentIMT._update(oldLeaf, newLeaf, siblingNodes);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
||||
@@ -514,7 +521,10 @@ contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, II
|
||||
/// @dev Caller must be the owner. Provides sibling nodes for proof of position.
|
||||
/// @param oldLeaf The identity commitment to remove.
|
||||
/// @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||
function devRemoveCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy {
|
||||
function devRemoveCommitment(
|
||||
uint256 oldLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _identityCommitmentIMT._remove(oldLeaf, siblingNodes);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
||||
|
||||
@@ -16,9 +16,9 @@ import RegisterVerifierArtifactLocal from "../../artifacts/contracts/verifiers/l
|
||||
import RegisterIdVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/register_id/Verifier_register_id_sha256_sha256_sha256_rsa_65537_4096_staging.sol/Verifier_register_id_sha256_sha256_sha256_rsa_65537_4096_staging.json";
|
||||
import RegisterAadhaarVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/register/Verifier_register_aadhaar_staging.sol/Verifier_register_aadhaar_staging.json";
|
||||
import DscVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/dsc/Verifier_dsc_sha256_rsa_65537_4096_staging.sol/Verifier_dsc_sha256_rsa_65537_4096_staging.json";
|
||||
import RegisterSelfricaVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/register/Verifier_register_selfrica_staging.sol/Verifier_register_selfrica_staging.json";
|
||||
import RegisterSelfricaVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/register/Verifier_register_kyc_staging.sol/Verifier_register_kyc_staging.json";
|
||||
// import GCPJWTVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/gcp_jwt_verifier/Verifier_gcp_jwt_verifier_staging.sol/Verifier_gcp_jwt_verifier_staging.json";
|
||||
import VcAndDiscloseSelfricaVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/disclose/Verifier_vc_and_disclose_selfrica_staging.sol/Verifier_vc_and_disclose_selfrica_staging.json";
|
||||
import VcAndDiscloseSelfricaVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/disclose/Verifier_vc_and_disclose_kyc_staging.sol/Verifier_vc_and_disclose_kyc_staging.json";
|
||||
|
||||
export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
|
||||
let identityVerificationHubV2: any;
|
||||
|
||||
@@ -48,11 +48,11 @@ const registerCircuitsAadhaar: CircuitArtifacts = {
|
||||
},
|
||||
};
|
||||
|
||||
const registerCircuitsSelfrica: CircuitArtifacts = {
|
||||
register_selfrica: {
|
||||
wasm: "../circuits/build/register/register_selfrica/register_selfrica_js/register_selfrica.wasm",
|
||||
zkey: "../circuits/build/register/register_selfrica/register_selfrica_final.zkey",
|
||||
vkey: "../circuits/build/register/register_selfrica/register_selfrica_vkey.json",
|
||||
const registerCircuitsKyc: CircuitArtifacts = {
|
||||
register_kyc: {
|
||||
wasm: "../circuits/build/register/register_kyc/register_kyc_js/register_kyc.wasm",
|
||||
zkey: "../circuits/build/register/register_kyc/register_kyc_final.zkey",
|
||||
vkey: "../circuits/build/register/register_kyc/register_kyc_vkey.json",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -88,9 +88,9 @@ const vcAndDiscloseCircuitsAadhaar: CircuitArtifacts = {
|
||||
|
||||
const vcAndDiscloseCircuitsSelfrica: CircuitArtifacts = {
|
||||
vc_and_disclose_selfrica: {
|
||||
wasm: "../circuits/build/disclose/vc_and_disclose_selfrica/vc_and_disclose_selfrica_js/vc_and_disclose_selfrica.wasm",
|
||||
zkey: "../circuits/build/disclose/vc_and_disclose_selfrica/vc_and_disclose_selfrica_final.zkey",
|
||||
vkey: "../circuits/build/disclose/vc_and_disclose_selfrica/vc_and_disclose_selfrica_vkey.json",
|
||||
wasm: "../circuits/build/disclose/vc_and_disclose_kyc/vc_and_disclose_kyc_js/vc_and_disclose_kyc.wasm",
|
||||
zkey: "../circuits/build/disclose/vc_and_disclose_kyc/vc_and_disclose_kyc_final.zkey",
|
||||
vkey: "../circuits/build/disclose/vc_and_disclose_kyc/vc_and_disclose_kyc_vkey.json",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -211,9 +211,9 @@ export async function generateRegisterSelfricaProof(
|
||||
//return type of prepareAadhaarTestData
|
||||
inputs: Awaited<ReturnType<typeof generateMockKycRegisterInput>>,
|
||||
): Promise<GenericProofStructStruct> {
|
||||
const circuitName = "register_selfrica";
|
||||
const circuitName = "register_kyc";
|
||||
|
||||
const circuitArtifacts = registerCircuitsSelfrica;
|
||||
const circuitArtifacts = registerCircuitsKyc;
|
||||
const artifactKey = circuitName;
|
||||
|
||||
const registerProof = await groth16.fullProve(
|
||||
@@ -225,7 +225,7 @@ export async function generateRegisterSelfricaProof(
|
||||
const vKey = JSON.parse(fs.readFileSync(circuitArtifacts[artifactKey].vkey, "utf8"));
|
||||
const isValid = await groth16.verify(vKey, registerProof.publicSignals, registerProof.proof);
|
||||
if (!isValid) {
|
||||
throw new Error("Generated register-selfrica proof verification failed");
|
||||
throw new Error("Generated register-kyc proof verification failed");
|
||||
}
|
||||
|
||||
const rawCallData = await groth16.exportSolidityCallData(registerProof.proof, registerProof.publicSignals);
|
||||
|
||||
@@ -106,12 +106,12 @@ describe("Self Verification Flow V2 - Selfrica", () => {
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
|
||||
describe("Complete V2 Verification Flow - Selfrica", () => {
|
||||
// TODO: Fix test setup - the proof's merkle root needs to be registered in the registry
|
||||
describe("Complete V2 Verification Flow - KYC", () => {
|
||||
// The issue is that generateKycDiscloseInput creates a commitment in the local tree,
|
||||
// TODO: Fix test setup - the proof's merkle root needs to be registered in the registry
|
||||
// but the registry has its own separate tree. The proof uses the local tree's root,
|
||||
// which is not registered in the registry.
|
||||
it("should complete full Selfrica verification flow with proper proof encoding", async () => {
|
||||
it("should complete full KYC verification flow with proper proof encoding", async () => {
|
||||
const destChainId = ethers.zeroPadValue(ethers.toBeHex(31337), 32);
|
||||
const user1Address = await deployedActors.user1.getAddress();
|
||||
const userData = ethers.toUtf8Bytes("test-user-data-for-verification");
|
||||
@@ -6,8 +6,11 @@ import { generateMockKycRegisterInput } from "@selfxyz/common/utils/kyc/generate
|
||||
import { generateRegisterSelfricaProof } from "../utils/generateProof";
|
||||
import { expect } from "chai";
|
||||
|
||||
function getCurrentDateDigitsYYMMDDHHMMSS(): bigint[] {
|
||||
function getCurrentDateDigitsYYMMDDHHMMSS(hoursOffset: number = 0): bigint[] {
|
||||
const now = new Date();
|
||||
if (hoursOffset !== 0) {
|
||||
now.setUTCHours(now.getUTCHours() + hoursOffset);
|
||||
}
|
||||
const pad2 = (n: number) => n.toString().padStart(2, "0");
|
||||
const yy = pad2(now.getUTCFullYear() % 100);
|
||||
const mm = pad2(now.getUTCMonth() + 1);
|
||||
@@ -106,7 +109,7 @@ describe("Selfrica Registration test", function () {
|
||||
|
||||
// Add the corresponding PCR0 (16 zero bytes + 32 hash bytes)
|
||||
const pcr0Bytes = ethers.getBytes(
|
||||
"0x" + "00".repeat(16) + "d2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04",
|
||||
"0x" + "d2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04".padStart(32, "0"),
|
||||
);
|
||||
await deployedActors.pcr0Manager.addPCR0(pcr0Bytes);
|
||||
|
||||
@@ -240,7 +243,7 @@ describe("Selfrica Registration test", function () {
|
||||
it("should not allow non-owner to update GCP root CA pubkey hash", async () => {
|
||||
await expect(
|
||||
deployedActors.registrySelfrica.connect(deployedActors.user1).updateGCPRootCAPubkeyHash(12345n),
|
||||
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should fail with INVALID_IMAGE when image hash not in PCR0Manager", async () => {
|
||||
@@ -265,7 +268,7 @@ describe("Selfrica Registration test", function () {
|
||||
deployedActors.registrySelfrica
|
||||
.connect(deployedActors.user1)
|
||||
.updateGCPJWTVerifier(ethers.Wallet.createRandom().address),
|
||||
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should allow owner to update GCP JWT verifier", async () => {
|
||||
@@ -296,7 +299,7 @@ describe("Selfrica Registration test", function () {
|
||||
it("should not allow non-owner to update TEE", async () => {
|
||||
await expect(
|
||||
deployedActors.registrySelfrica.connect(deployedActors.user1).updateTEE(ethers.Wallet.createRandom().address),
|
||||
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should allow owner to update TEE", async () => {
|
||||
@@ -341,13 +344,24 @@ describe("Selfrica Registration test", function () {
|
||||
});
|
||||
|
||||
it("should fail with INVALID_TIMESTAMP when timestamp is in the past or future", async () => {
|
||||
// Add the PCR0 image hash so the image validation passes and we can test timestamp validation
|
||||
// addPCR0 takes 32 bytes and pads to 48 bytes internally, isPCR0Set requires 48 bytes
|
||||
const pcr0Hash = "d2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04";
|
||||
const pcr0Bytes32 = ethers.getBytes("0x" + pcr0Hash);
|
||||
const pcr0Bytes48 = ethers.getBytes("0x" + "00".repeat(16) + pcr0Hash);
|
||||
// Only add PCR0 if not already set (may have been added by earlier test)
|
||||
const isAlreadySet = await deployedActors.pcr0Manager.isPCR0Set(pcr0Bytes48);
|
||||
if (!isAlreadySet) {
|
||||
await deployedActors.pcr0Manager.addPCR0(pcr0Bytes32);
|
||||
}
|
||||
|
||||
let mockPubkeyCommitment = 12345678901234567890123456789012n;
|
||||
const [p0, p1, p2] = packUint256ToHexFields(BigInt(mockPubkeyCommitment));
|
||||
|
||||
let previousHourDate = getCurrentDateDigitsYYMMDDHHMMSS();
|
||||
previousHourDate[3 * 2] = previousHourDate[3 * 2] - 1n;
|
||||
// Create a timestamp 2 hours in the past (more than 1 hour threshold)
|
||||
const previousHourDate = getCurrentDateDigitsYYMMDDHHMMSS(-2);
|
||||
|
||||
const mockPubSignals = [
|
||||
const mockPubSignalsPast = [
|
||||
GCP_ROOT_CA_PUBKEY_HASH,
|
||||
p0,
|
||||
p1,
|
||||
@@ -363,19 +377,30 @@ describe("Selfrica Registration test", function () {
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
mockPubSignals,
|
||||
mockPubSignalsPast,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "INVALID_TIMESTAMP");
|
||||
|
||||
let nextHourDate = getCurrentDateDigitsYYMMDDHHMMSS();
|
||||
nextHourDate[3 * 2] = nextHourDate[3 * 2] + 1n;
|
||||
// Create a timestamp 2 hours in the future (more than 1 hour threshold)
|
||||
const nextHourDate = getCurrentDateDigitsYYMMDDHHMMSS(2);
|
||||
|
||||
const mockPubSignalsFuture = [
|
||||
GCP_ROOT_CA_PUBKEY_HASH,
|
||||
p0,
|
||||
p1,
|
||||
p2,
|
||||
177384435506496807268973340845468654286294928521500580044819492874465981028n,
|
||||
175298970718174405520284770870231222447414486446296682893283627688949855078n,
|
||||
13360n,
|
||||
...nextHourDate,
|
||||
];
|
||||
|
||||
await expect(
|
||||
deployedActors.registrySelfrica.registerPubkeyCommitment(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
mockPubSignals,
|
||||
mockPubSignalsFuture,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "INVALID_TIMESTAMP");
|
||||
});
|
||||
16
packages/mobile-sdk-alpha/src/config/features.ts
Normal file
16
packages/mobile-sdk-alpha/src/config/features.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Static feature flags for the SDK.
|
||||
* These are compile-time constants that control feature availability.
|
||||
* Set to true when ready to launch the feature.
|
||||
*/
|
||||
export const FeatureFlags = {
|
||||
/**
|
||||
* Enable Sumsub/KYC "Other IDs" option in the ID selection screen.
|
||||
* When false, the KYC button will be hidden from users.
|
||||
*/
|
||||
KYC_ENABLED: false,
|
||||
} as const;
|
||||
@@ -7,10 +7,12 @@ import { StyleSheet } from 'react-native';
|
||||
|
||||
import AadhaarLogo from '../../../svgs/icons/aadhaar.svg';
|
||||
import EPassportLogoRounded from '../../../svgs/icons/epassport_rounded.svg';
|
||||
import PassportCameraScanIcon from '../../../svgs/icons/passport_camera_scan.svg';
|
||||
import PlusIcon from '../../../svgs/icons/plus.svg';
|
||||
import SelfLogo from '../../../svgs/logo.svg';
|
||||
import { BodyText, RoundFlag, View, XStack, YStack } from '../../components';
|
||||
import { black, slate100, slate300, slate400, white } from '../../constants/colors';
|
||||
import { FeatureFlags } from '../../config/features';
|
||||
import { black, blue100, blue600, slate100, slate300, slate400, white } from '../../constants/colors';
|
||||
import { advercase, dinot } from '../../constants/fonts';
|
||||
import { useSelfClient } from '../../context';
|
||||
import { buttonTap } from '../../haptic';
|
||||
@@ -24,6 +26,8 @@ const getDocumentName = (docType: string): string => {
|
||||
return 'ID card';
|
||||
case 'a':
|
||||
return 'Aadhaar';
|
||||
case 'kyc':
|
||||
return 'Other IDs';
|
||||
default:
|
||||
return 'Unknown Document';
|
||||
}
|
||||
@@ -37,12 +41,14 @@ const getDocumentNameForEvent = (docType: string): string => {
|
||||
return 'id_card';
|
||||
case 'a':
|
||||
return 'aadhaar';
|
||||
case 'kyc':
|
||||
return 'kyc';
|
||||
default:
|
||||
return 'unknown_document';
|
||||
}
|
||||
};
|
||||
|
||||
const getDocumentDescription = (docType: string): string => {
|
||||
const getDocumentDescription = (docType: string): string | null => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
return 'Verified Biometric Passport';
|
||||
@@ -50,6 +56,8 @@ const getDocumentDescription = (docType: string): string => {
|
||||
return 'Verified Biometric ID card';
|
||||
case 'a':
|
||||
return 'Verified mAadhaar QR code';
|
||||
case 'kyc':
|
||||
return "National ID, Driver's License etc.";
|
||||
default:
|
||||
return 'Unknown Document';
|
||||
}
|
||||
@@ -63,11 +71,61 @@ const getDocumentLogo = (docType: string): React.ReactNode => {
|
||||
return <EPassportLogoRounded />;
|
||||
case 'a':
|
||||
return <AadhaarLogo />;
|
||||
case 'kyc':
|
||||
// same color as epassport_rounded.svg
|
||||
return <PassportCameraScanIcon color={'#075985'} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getDocumentSecurityBadge = (docType: string): string | null => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
case 'i':
|
||||
case 'a':
|
||||
return 'Best security';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
type DocumentItemProps = {
|
||||
docType: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const DocumentItem: React.FC<DocumentItemProps> = ({ docType, onPress }) => {
|
||||
const securityBadge = getDocumentSecurityBadge(docType);
|
||||
const description = getDocumentDescription(docType);
|
||||
|
||||
return (
|
||||
<XStack
|
||||
style={styles.documentItem}
|
||||
backgroundColor={white}
|
||||
borderWidth={1}
|
||||
borderColor={slate300}
|
||||
elevation={4}
|
||||
borderRadius={'$5'}
|
||||
padding={'$3'}
|
||||
pressStyle={{
|
||||
transform: [{ scale: 0.97 }],
|
||||
backgroundColor: slate100,
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<XStack alignItems="center" gap={'$3'} flex={1}>
|
||||
{securityBadge && <BodyText style={styles.securityBadgeText}>{securityBadge}</BodyText>}
|
||||
<View style={styles.documentLogoContainer}>{getDocumentLogo(docType)}</View>
|
||||
<YStack gap={'$1'}>
|
||||
<BodyText style={styles.documentNameText}>{getDocumentName(docType)}</BodyText>
|
||||
{description && <BodyText style={styles.documentDescriptionText}>{description}</BodyText>}
|
||||
</YStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
);
|
||||
};
|
||||
|
||||
type IDSelectionScreenProps = {
|
||||
countryCode: string;
|
||||
documentTypes: string[];
|
||||
@@ -112,30 +170,14 @@ const IDSelectionScreen: React.FC<IDSelectionScreenProps> = props => {
|
||||
</YStack>
|
||||
<YStack gap="$3">
|
||||
{documentTypes.map((docType: string) => (
|
||||
<XStack
|
||||
key={docType}
|
||||
backgroundColor={white}
|
||||
borderWidth={1}
|
||||
borderColor={slate300}
|
||||
elevation={4}
|
||||
borderRadius={'$5'}
|
||||
padding={'$3'}
|
||||
pressStyle={{
|
||||
transform: [{ scale: 0.97 }],
|
||||
backgroundColor: slate100,
|
||||
}}
|
||||
onPress={() => onSelectDocumentType(docType)}
|
||||
>
|
||||
<XStack alignItems="center" gap={'$3'} flex={1}>
|
||||
{getDocumentLogo(docType)}
|
||||
<YStack gap={'$1'}>
|
||||
<BodyText style={styles.documentNameText}>{getDocumentName(docType)}</BodyText>
|
||||
<BodyText style={styles.documentDescriptionText}>{getDocumentDescription(docType)}</BodyText>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
<DocumentItem key={docType} docType={docType} onPress={() => onSelectDocumentType(docType)} />
|
||||
))}
|
||||
<BodyText style={styles.footerText}>Be sure your document is ready to scan</BodyText>
|
||||
{FeatureFlags.KYC_ENABLED && (
|
||||
<View style={styles.kycContainer}>
|
||||
<DocumentItem docType="kyc" onPress={() => onSelectDocumentType('kyc')} />
|
||||
</View>
|
||||
)}
|
||||
</YStack>
|
||||
</YStack>
|
||||
);
|
||||
@@ -149,6 +191,33 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
color: black,
|
||||
},
|
||||
documentLogoContainer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
documentItem: {
|
||||
position: 'relative',
|
||||
borderWidth: 1,
|
||||
},
|
||||
securityBadgeText: {
|
||||
fontSize: 12,
|
||||
fontFamily: dinot,
|
||||
color: blue600,
|
||||
backgroundColor: blue100,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: blue600,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
position: 'absolute',
|
||||
top: -20,
|
||||
right: -20,
|
||||
},
|
||||
kycContainer: {
|
||||
marginTop: 36,
|
||||
},
|
||||
documentNameText: {
|
||||
fontSize: 24,
|
||||
fontFamily: dinot,
|
||||
|
||||
14
patches/@sumsub+react-native-mobilesdk-module+1.40.2.patch
Normal file
14
patches/@sumsub+react-native-mobilesdk-module+1.40.2.patch
Normal file
@@ -0,0 +1,14 @@
|
||||
diff --git a/node_modules/@sumsub/react-native-mobilesdk-module/android/build.gradle b/node_modules/@sumsub/react-native-mobilesdk-module/android/build.gradle
|
||||
index 1234567..abcdefg 100644
|
||||
--- a/node_modules/@sumsub/react-native-mobilesdk-module/android/build.gradle
|
||||
+++ b/node_modules/@sumsub/react-native-mobilesdk-module/android/build.gradle
|
||||
@@ -69,9 +69,9 @@ dependencies {
|
||||
implementation "com.sumsub.sns:idensic-mobile-sdk:1.40.2"
|
||||
|
||||
// remove comment to enable Device Intelligence
|
||||
- // implementation "com.sumsub.sns:idensic-mobile-sdk-fisherman:1.40.2"
|
||||
+ implementation "com.sumsub.sns:idensic-mobile-sdk-fisherman:1.40.2"
|
||||
// remove comment if you need VideoIdent support
|
||||
- // implementation "com.sumsub.sns:idensic-mobile-sdk-videoident:1.40.2"
|
||||
+ implementation "com.sumsub.sns:idensic-mobile-sdk-videoident:1.40.2"
|
||||
// remove comment if you need EID support
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@selfxyz/qrcode",
|
||||
"version": "1.0.17-beta.2",
|
||||
"version": "1.0.18",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/selfxyz/self"
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@@ -8486,6 +8486,7 @@ __metadata:
|
||||
"@sentry/react": "npm:^9.32.0"
|
||||
"@sentry/react-native": "npm:7.0.1"
|
||||
"@stablelib/cbor": "npm:^2.0.1"
|
||||
"@sumsub/react-native-mobilesdk-module": "npm:1.40.2"
|
||||
"@tamagui/animations-css": "npm:1.126.14"
|
||||
"@tamagui/animations-react-native": "npm:1.126.14"
|
||||
"@tamagui/config": "npm:1.126.14"
|
||||
@@ -8577,7 +8578,7 @@ __metadata:
|
||||
react-native-safe-area-context: "npm:^5.6.1"
|
||||
react-native-screens: "npm:4.15.3"
|
||||
react-native-sqlite-storage: "npm:^6.0.1"
|
||||
react-native-svg: "npm:15.14.0"
|
||||
react-native-svg: "npm:15.15.0"
|
||||
react-native-svg-transformer: "npm:^1.5.1"
|
||||
react-native-svg-web: "npm:1.0.9"
|
||||
react-native-url-polyfill: "npm:^3.0.0"
|
||||
@@ -8586,7 +8587,7 @@ __metadata:
|
||||
react-qr-barcode-scanner: "npm:^2.1.8"
|
||||
react-test-renderer: "npm:^18.3.1"
|
||||
rollup-plugin-visualizer: "npm:^6.0.3"
|
||||
socket.io-client: "npm:^4.8.1"
|
||||
socket.io-client: "npm:^4.8.3"
|
||||
stream-browserify: "npm:^3.0.0"
|
||||
tamagui: "npm:1.126.14"
|
||||
ts-morph: "npm:^22.0.0"
|
||||
@@ -11147,6 +11148,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sumsub/react-native-mobilesdk-module@npm:1.40.2":
|
||||
version: 1.40.2
|
||||
resolution: "@sumsub/react-native-mobilesdk-module@npm:1.40.2"
|
||||
peerDependencies:
|
||||
react-native: ">=0.60.0-rc.0 <1.0.x"
|
||||
checksum: 10c0/fc5f368d3afdffdb27496ab3b152b38bb0ee8b52f8592b2b48e0a3941197de33fd445502123eeca676d0ee1ecaeaa2e7c6573601e06c4eea79dfa2aec64ed75b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@svgr/babel-plugin-add-jsx-attribute@npm:8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:8.0.0"
|
||||
@@ -29840,9 +29850,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-svg@npm:15.14.0":
|
||||
version: 15.14.0
|
||||
resolution: "react-native-svg@npm:15.14.0"
|
||||
"react-native-svg@npm:15.15.0":
|
||||
version: 15.15.0
|
||||
resolution: "react-native-svg@npm:15.15.0"
|
||||
dependencies:
|
||||
css-select: "npm:^5.1.0"
|
||||
css-tree: "npm:^1.1.3"
|
||||
@@ -29850,7 +29860,7 @@ __metadata:
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 10c0/5855bee2a76313f580ac3f8c476d07bb63d1a8e5e0883154275be5a1f4224e35f2416b0ba3b03f5d4c637c3b2f9320df34ede918da27188adb6881b3b8ac96c8
|
||||
checksum: 10c0/0da39529fedcc84d2deaad19e261453069675520cb446a08d97eebc10f1205ce0393a9760ad58bc7b80feaa60e94c30d2d5b90d3111451f260b3888a4fbc2e06
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -31782,7 +31792,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socket.io-client@npm:^4.8.1":
|
||||
"socket.io-client@npm:^4.8.1, socket.io-client@npm:^4.8.3":
|
||||
version: 4.8.3
|
||||
resolution: "socket.io-client@npm:4.8.3"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user