diff --git a/.giga/specifications.json b/.giga/specifications.json deleted file mode 100644 index e91e29dbb..000000000 --- a/.giga/specifications.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "fileName": "main-overview.mdc", - "path": ".cursor/rules", - "description": "Complete system overview of the identity verification platform, covering the passport/ID verification workflow, zero-knowledge proof system, and compliance mechanisms" - }, - { - "fileName": "mobile-sdk-migration.mdc", - "path": ".cursor/rules", - "description": "Comprehensive migration strategy and testing-first approach for porting identity verification logic from app to mobile-sdk-alpha package with detailed checklist and validation workflow" - }, - { - "fileName": "technical-specification.mdc", - "path": ".cursor/rules", - "description": "Consolidated technical implementation specification for zero-knowledge proof circuits, data models, verification workflows, and implementation requirements with performance constraints" - }, - { - "fileName": "compliance-verification.mdc", - "path": ".cursor/rules", - "description": "Critical compliance verification requirements for OFAC checks, age verification, and forbidden country validation with specific implementation details and constraints" - } -] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..1acc07d98 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## Summary + + + +## Test plan + + + +--- + +### Native Consolidation Checklist + + + +- [ ] CONTRACTS.md reviewed - no unintended contract changes +- [ ] Layer 1 bridge contract tests pass (`cd app && yarn jest:run` / `yarn workspace @selfxyz/rn-sdk-test-app test`) +- [ ] Layer 3 builds pass (app iOS, RN test app iOS, RN test app Android) +- [ ] Layer 4 manual smoke test signed off (if consolidation PR) +- [ ] No new native business logic added (logic belongs in TypeScript) diff --git a/.github/actions/find-ios-simulator/action.yml b/.github/actions/find-ios-simulator/action.yml new file mode 100644 index 000000000..8c75f7543 --- /dev/null +++ b/.github/actions/find-ios-simulator/action.yml @@ -0,0 +1,25 @@ +name: Find iOS Simulator +description: Finds an available iPhone simulator on the runner and outputs its UUID. + +outputs: + id: + description: UUID of the first available iPhone simulator + value: ${{ steps.find.outputs.id }} + +runs: + using: composite + steps: + - id: find + shell: bash + run: | + SIM_ID=$(xcrun simctl list devices available -j | python3 -c " + import json, sys + data = json.load(sys.stdin) + for runtime, devices in data['devices'].items(): + if 'iOS' in runtime: + for d in devices: + if 'iPhone' in d['name'] and d['isAvailable']: + print(d['udid']); sys.exit(0) + sys.exit(1)") + echo "Found simulator: $SIM_ID" + echo "id=$SIM_ID" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/kmp-ci.yml b/.github/workflows/kmp-ci.yml index b2cd1c9d6..774965e6b 100644 --- a/.github/workflows/kmp-ci.yml +++ b/.github/workflows/kmp-ci.yml @@ -5,12 +5,24 @@ permissions: on: pull_request: - paths: ["packages/kmp-sdk/**", "packages/kmp-test-app/**"] + paths: + - "packages/kmp-sdk/**" + - "packages/kmp-sdk-test-app/**" + - "packages/kmp-minipay-sample/**" + - ".github/workflows/kmp-ci.yml" + - ".github/actions/**" push: branches: [dev, staging, main] - paths: ["packages/kmp-sdk/**", "packages/kmp-test-app/**"] + paths: + - "packages/kmp-sdk/**" + - "packages/kmp-sdk-test-app/**" + - "packages/kmp-minipay-sample/**" + - ".github/workflows/kmp-ci.yml" + - ".github/actions/**" jobs: + # ── KMP SDK ────────────────────────────────────────────── + kmp-sdk-tests: runs-on: ubuntu-latest timeout-minutes: 60 @@ -34,12 +46,68 @@ jobs: name: kmp-sdk-test-results path: packages/kmp-sdk/shared/build/reports/tests/ + kmp-sdk-android-build: + runs-on: ubuntu-latest + timeout-minutes: 60 + defaults: + run: + working-directory: packages/kmp-sdk + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: ./.github/actions/cache-gradle + - uses: gradle/actions/setup-gradle@v4 + with: + cache-disabled: true + - run: ./gradlew :shared:assembleDebug + + kmp-sdk-ios-framework: + runs-on: namespace-profile-apple-silicon-6cpu + timeout-minutes: 60 + defaults: + run: + working-directory: packages/kmp-sdk + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: ./.github/actions/cache-gradle + - uses: gradle/actions/setup-gradle@v4 + with: + cache-disabled: true + - run: ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 + + kmp-sdk-ios-test: + runs-on: namespace-profile-apple-silicon-6cpu + timeout-minutes: 60 + defaults: + run: + working-directory: packages/kmp-sdk + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: ./.github/actions/cache-gradle + - uses: gradle/actions/setup-gradle@v4 + with: + cache-disabled: true + - run: ./gradlew :shared:iosSimulatorArm64Test + + # ── KMP Test App ───────────────────────────────────────── + kmp-test-app-tests: runs-on: ubuntu-latest timeout-minutes: 60 defaults: run: - working-directory: packages/kmp-test-app + working-directory: packages/kmp-sdk-test-app steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 @@ -55,4 +123,134 @@ jobs: if: always() with: name: kmp-test-app-test-results - path: packages/kmp-test-app/composeApp/build/reports/tests/ + path: packages/kmp-sdk-test-app/composeApp/build/reports/tests/ + + kmp-test-app-android-build: + runs-on: ubuntu-latest + timeout-minutes: 60 + defaults: + run: + working-directory: packages/kmp-sdk-test-app + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: ./.github/actions/cache-gradle + - uses: gradle/actions/setup-gradle@v4 + with: + cache-disabled: true + - run: ./gradlew :composeApp:assembleDebug + + kmp-test-app-ios-build: + runs-on: namespace-profile-apple-silicon-6cpu + timeout-minutes: 60 + defaults: + run: + working-directory: packages/kmp-sdk-test-app + steps: + - uses: actions/checkout@v4 + - name: Generate token for private dependencies + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" + - name: Configure git for HTTPS dependency fetch + run: git config --global url."https://github.com/".insteadOf "git@github.com:" + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: ./.github/actions/cache-gradle + - uses: gradle/actions/setup-gradle@v4 + with: + cache-disabled: true + - name: Build KMP framework for iOS + working-directory: packages/kmp-sdk + run: ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 + - name: Install CocoaPods dependencies + working-directory: packages/kmp-sdk-test-app/iosApp + run: pod install + - name: Find iOS Simulator + id: sim + uses: ./.github/actions/find-ios-simulator + - name: Build iOS app + working-directory: packages/kmp-sdk-test-app/iosApp + run: | + xcodebuild -workspace iosApp.xcworkspace \ + -scheme iosApp \ + -sdk iphonesimulator \ + -destination "id=${{ steps.sim.outputs.id }}" \ + ONLY_ACTIVE_ARCH=YES \ + build + + # ── KMP Minipay Sample ────────────────────────────────── + + kmp-minipay-android-build: + runs-on: ubuntu-latest + timeout-minutes: 60 + defaults: + run: + working-directory: packages/kmp-minipay-sample + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: ./.github/actions/cache-gradle + - uses: gradle/actions/setup-gradle@v4 + with: + cache-disabled: true + - run: ./gradlew :composeApp:assembleDebug + + kmp-minipay-ios-build: + runs-on: namespace-profile-apple-silicon-6cpu + timeout-minutes: 60 + defaults: + run: + working-directory: packages/kmp-minipay-sample + steps: + - uses: actions/checkout@v4 + - name: Generate token for private dependencies + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" + - name: Configure git for HTTPS dependency fetch + run: git config --global url."https://github.com/".insteadOf "git@github.com:" + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: ./.github/actions/cache-gradle + - uses: gradle/actions/setup-gradle@v4 + with: + cache-disabled: true + - name: Build KMP framework for iOS + working-directory: packages/kmp-sdk + run: ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 + - name: Find iOS Simulator + id: sim + uses: ./.github/actions/find-ios-simulator + - name: Resolve SPM dependencies + working-directory: packages/kmp-minipay-sample/iosApp + run: | + xcodebuild -project iosApp.xcodeproj \ + -resolvePackageDependencies + - name: Build iOS app + working-directory: packages/kmp-minipay-sample/iosApp + run: | + xcodebuild -project iosApp.xcodeproj \ + -scheme iosApp \ + -sdk iphonesimulator \ + -destination "id=${{ steps.sim.outputs.id }}" \ + ONLY_ACTIVE_ARCH=YES \ + ARCHS=arm64 \ + SWIFT_ENABLE_EXPLICIT_MODULES=NO \ + build diff --git a/.github/workflows/mobile-ci.yml b/.github/workflows/mobile-ci.yml index a419eda00..473daf031 100644 --- a/.github/workflows/mobile-ci.yml +++ b/.github/workflows/mobile-ci.yml @@ -202,319 +202,3 @@ jobs: # Run jest through yarn to avoid the build:deps step since CI already built dependencies yarn test:ci working-directory: ./app - build-ios: - # This is mostly covered in mobile-e2e.yml so we don't need to run it here frequently - if: github.event_name == 'workflow_dispatch' - # runs-on: macos-latest-large - runs-on: namespace-profile-apple-silicon-6cpu - needs: build-deps - timeout-minutes: 60 - env: - # iOS project configuration - hardcoded for CI stability - IOS_PROJECT_NAME: "Self" - IOS_PROJECT_SCHEME: "OpenPassport" - steps: - - uses: actions/checkout@v6 - - name: Read and sanitize Node.js version - shell: bash - run: | - if [ ! -f .nvmrc ] || [ -z "$(cat .nvmrc)" ]; then - echo "❌ .nvmrc is missing or empty"; exit 1; - fi - VERSION="$(tr -d '\r\n' < .nvmrc)" - VERSION="${VERSION#v}" - if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then - echo "Invalid .nvmrc content: '$VERSION'"; exit 1; - fi - echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV" - echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV" - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Enable Corepack - run: corepack enable - - name: Activate Yarn 4.12.0 - run: corepack prepare yarn@4.12.0 --activate - - name: Set up Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ env.XCODE_VERSION }} - - name: Configure Xcode path - run: | - echo "🔧 Configuring Xcode path to fix iOS SDK issues..." - # Fix for macOS 15 runner iOS SDK issues - # See: https://github.com/actions/runner-images/issues/12758 - sudo xcode-select --switch /Applications/Xcode_${{ env.XCODE_VERSION }}.app - echo "✅ Xcode path configured" - - # Verify Xcode setup - echo "Xcode version:" - xcodebuild -version - echo "Xcode path:" - xcode-select -p - - - name: Check Java installation - run: | - echo "INSTALL_JAVA=false" >> "$GITHUB_ENV" - if command -v java &> /dev/null && java -version &> /dev/null; then - echo "Java already installed: $(java -version 2>&1 | head -n 1)" - else - echo "Java not found or not working, will install..." - echo "INSTALL_JAVA=true" >> "$GITHUB_ENV" - fi - - name: Setup Java environment - if: env.INSTALL_JAVA == 'true' - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: ${{ env.JAVA_VERSION }} - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ env.RUBY_VERSION }} - bundler-cache: false - - name: Cache Yarn - uses: ./.github/actions/cache-yarn - with: - path: | - .yarn/cache - node_modules - app/node_modules - cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }} - - name: Cache Ruby gems - uses: ./.github/actions/cache-bundler - with: - path: app/vendor/bundle - lock-file: app/Gemfile.lock - cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }} - - name: Cache Pods - uses: ./.github/actions/cache-pods - with: - path: | - app/ios/Pods - ~/Library/Caches/CocoaPods - lockfile: app/ios/Podfile.lock - cache-version: ${{ env.GH_CACHE_VERSION }} - - name: Cache Xcode build - uses: actions/cache@v4 - with: - path: | - app/ios/build - ~/Library/Developer/Xcode/DerivedData - ~/Library/Caches/com.apple.dt.Xcode - key: ${{ runner.os }}-xcode-${{ env.XCODE_VERSION }}-${{ hashFiles('app/ios/Podfile.lock', 'app/ios/OpenPassport.xcworkspace/contents.xcworkspacedata', 'app/ios/Self.xcworkspace/contents.xcworkspacedata') }} - restore-keys: | - ${{ runner.os }}-xcode-${{ env.XCODE_VERSION }}-${{ hashFiles('app/ios/Podfile.lock') }}- - ${{ runner.os }}-xcode-${{ env.XCODE_VERSION }}- - - name: Cache Xcode Index - uses: actions/cache@v4 - with: - path: app/ios/build/Index.noindex - key: ${{ runner.os }}-xcode-index-${{ env.XCODE_VERSION }}-${{ hashFiles('app/ios/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-xcode-index-${{ env.XCODE_VERSION }}- - - name: Generate token for self repositories - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} - uses: ./.github/actions/generate-github-token - id: github-token - with: - app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} - private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} - configure-netrc: "true" - - name: Install Mobile Dependencies - uses: ./.github/actions/yarn-install - - name: Cache Built Dependencies - id: built-deps - uses: ./.github/actions/cache-built-deps - with: - cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }} - - name: Build dependencies (cache miss) - # if: steps.built-deps.outputs.cache-hit != 'true' - env: - SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token || '' }} - run: | - echo "Cache miss for built dependencies. Building now..." - yarn workspace @selfxyz/mobile-app run build:deps - - name: Install Ruby Dependencies - run: | - echo "Installing Ruby dependencies..." - bundle config set --local path 'vendor/bundle' - bundle install --jobs 4 --retry 3 - working-directory: ./app - - name: Install iOS Dependencies - uses: nick-fields/retry@v3 - with: - timeout_minutes: 20 - max_attempts: 3 - retry_wait_seconds: 10 - command: | - cd app/ios - bundle exec bash scripts/pod-install-with-cache-fix.sh - env: - SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - - name: Resolve iOS workspace - run: | - WORKSPACE_OPEN="ios/OpenPassport.xcworkspace" - WORKSPACE_SELF="ios/Self.xcworkspace" - - if xcodebuild -list -workspace "$WORKSPACE_OPEN" 2>/dev/null | grep -q "OpenPassport"; then - WORKSPACE_PATH="$WORKSPACE_OPEN" - else - WORKSPACE_PATH="$WORKSPACE_SELF" - fi - - echo "WORKSPACE_PATH=$WORKSPACE_PATH" >> "$GITHUB_ENV" - echo "Resolved workspace: $WORKSPACE_PATH" - working-directory: ./app - - name: Verify iOS Workspace - run: | - echo "Verifying iOS workspace setup..." - - if [ -z "$WORKSPACE_PATH" ]; then - echo "❌ WORKSPACE_PATH is not set" - exit 1 - fi - - if [ ! -d "$WORKSPACE_PATH" ]; then - echo "❌ Workspace not found at: $WORKSPACE_PATH" - echo "Available workspaces:" - find ios -name "*.xcworkspace" -type d - exit 1 - fi - - if [ ! -d "ios/Pods" ]; then - echo "❌ Pods directory is missing" - exit 1 - fi - - # Verify scheme exists by listing available schemes - echo "Verifying scheme availability..." - AVAILABLE_SCHEMES=$(xcodebuild -list -workspace "$WORKSPACE_PATH" 2>/dev/null | grep -A 200 "Schemes:" | grep -v "Schemes:" | xargs) - echo "Available schemes (first 20): $(echo $AVAILABLE_SCHEMES | cut -d' ' -f1-20)..." - - if [[ ! "$AVAILABLE_SCHEMES" =~ ${{ env.IOS_PROJECT_SCHEME }} ]]; then - echo "❌ Scheme '${{ env.IOS_PROJECT_SCHEME }}' not found" - echo "Full scheme list:" - xcodebuild -list -workspace "$WORKSPACE_PATH" 2>/dev/null | grep -A 200 "Schemes:" | grep -v "Schemes:" | head -50 - exit 1 - fi - - echo "✅ iOS workspace is properly configured" - echo "✅ Using workspace: $WORKSPACE_PATH" - echo "✅ Using scheme: ${{ env.IOS_PROJECT_SCHEME }}" - working-directory: ./app - - name: Build iOS - run: | - echo "Building iOS app for simulator (no signing required)..." - echo "Project: ${{ env.IOS_PROJECT_NAME }}, Scheme: ${{ env.IOS_PROJECT_SCHEME }}" - # Build for iOS Simulator to avoid code signing issues in CI - xcodebuild -workspace "$WORKSPACE_PATH" \ - -scheme ${{ env.IOS_PROJECT_SCHEME }} \ - -configuration Release \ - -sdk iphonesimulator \ - -destination "generic/platform=iOS Simulator" \ - -derivedDataPath ios/build \ - -jobs "$(sysctl -n hw.ncpu)" \ - -parallelizeTargets \ - -quiet || { echo "❌ iOS build failed"; exit 1; } - echo "✅ iOS build succeeded" - working-directory: ./app - - build-android: - runs-on: ubuntu-latest - needs: build-deps - # This is mostly covered in mobile-e2e.yml so we don't need to run it here frequently - if: github.event_name == 'workflow_dispatch' - timeout-minutes: 60 - steps: - - uses: actions/checkout@v6 - - name: Read and sanitize Node.js version - shell: bash - run: | - if [ ! -f .nvmrc ] || [ -z "$(cat .nvmrc)" ]; then - echo "❌ .nvmrc is missing or empty"; exit 1; - fi - VERSION="$(tr -d '\r\n' < .nvmrc)" - VERSION="${VERSION#v}" - if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then - echo "Invalid .nvmrc content: '$VERSION'"; exit 1; - fi - echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV" - echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV" - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Enable Corepack - run: corepack enable - - name: Activate Yarn 4.12.0 - run: corepack prepare yarn@4.12.0 --activate - - name: Cache Yarn - uses: ./.github/actions/cache-yarn - with: - path: | - .yarn/cache - node_modules - app/node_modules - cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }} - - name: Free up disk space - uses: ./.github/actions/free-disk-space - - name: Cache Gradle - uses: ./.github/actions/cache-gradle - with: - cache-version: ${{ env.GH_CACHE_VERSION }} - - name: Setup Java environment - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: ${{ env.JAVA_VERSION }} - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - with: - accept-android-sdk-licenses: true - - name: Cache NDK - id: ndk-cache - uses: actions/cache@v4 - with: - path: ${{ env.ANDROID_HOME }}/ndk/${{ env.ANDROID_NDK_VERSION }} - key: ${{ runner.os }}-ndk-${{ env.ANDROID_NDK_VERSION }} - - name: Install NDK - if: steps.ndk-cache.outputs.cache-hit != 'true' - uses: nick-fields/retry@v3 - with: - timeout_minutes: 15 - max_attempts: 3 - retry_wait_seconds: 10 - command: sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}" - - name: Install Mobile Dependencies - uses: ./.github/actions/yarn-install - - name: Cache Built Dependencies - id: built-deps - uses: ./.github/actions/cache-built-deps - with: - cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }} - - name: Build dependencies (cache miss) - if: steps.built-deps.outputs.cache-hit != 'true' - run: | - echo "Cache miss for built dependencies. Building now..." - yarn workspace @selfxyz/mobile-app run build:deps - - name: Generate token for self repositories - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} - uses: ./.github/actions/generate-github-token - id: github-token - with: - app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} - private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} - - name: Setup Android private modules - run: | - cd ${{ env.APP_PATH }} - PLATFORM=android node scripts/setup-private-modules.cjs - env: - SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} - CI: true - - name: Build Android (with AAPT2 symlink fix) - run: yarn android:ci - working-directory: ./app - - name: Clean up Gradle build artifacts - uses: ./.github/actions/cleanup-gradle-artifacts diff --git a/.github/workflows/mobile-deploy.yml b/.github/workflows/mobile-deploy.yml index 8e08271a9..5877edec3 100644 --- a/.github/workflows/mobile-deploy.yml +++ b/.github/workflows/mobile-deploy.yml @@ -690,6 +690,9 @@ jobs: CI_IOS_BUILD: ${{ needs.bump-version.outputs.ios_build }} CI_ANDROID_BUILD: ${{ needs.bump-version.outputs.android_build }} ENABLE_DEBUG_LOGS: ${{ secrets.ENABLE_DEBUG_LOGS }} + GOOGLE_SIGNIN_ANDROID_CLIENT_ID: ${{ secrets.GOOGLE_SIGNIN_ANDROID_CLIENT_ID }} + GOOGLE_SIGNIN_IOS_CLIENT_ID: ${{ secrets.GOOGLE_SIGNIN_IOS_CLIENT_ID }} + GOOGLE_SIGNIN_WEB_CLIENT_ID: ${{ secrets.GOOGLE_SIGNIN_WEB_CLIENT_ID }} GRAFANA_LOKI_PASSWORD: ${{ secrets.GRAFANA_LOKI_PASSWORD }} GRAFANA_LOKI_URL: ${{ secrets.GRAFANA_LOKI_URL }} GRAFANA_LOKI_USERNAME: ${{ secrets.GRAFANA_LOKI_USERNAME }} @@ -1165,6 +1168,8 @@ jobs: ANDROID_PACKAGE_NAME: ${{ secrets.ANDROID_PACKAGE_NAME }} ENABLE_DEBUG_LOGS: ${{ secrets.ENABLE_DEBUG_LOGS }} GOOGLE_SIGNIN_ANDROID_CLIENT_ID: ${{ secrets.GOOGLE_SIGNIN_ANDROID_CLIENT_ID }} + GOOGLE_SIGNIN_IOS_CLIENT_ID: ${{ secrets.GOOGLE_SIGNIN_IOS_CLIENT_ID }} + GOOGLE_SIGNIN_WEB_CLIENT_ID: ${{ secrets.GOOGLE_SIGNIN_WEB_CLIENT_ID }} GRAFANA_LOKI_PASSWORD: ${{ secrets.GRAFANA_LOKI_PASSWORD }} GRAFANA_LOKI_URL: ${{ secrets.GRAFANA_LOKI_URL }} GRAFANA_LOKI_USERNAME: ${{ secrets.GRAFANA_LOKI_USERNAME }} diff --git a/.github/workflows/mobile-sdk-demo-e2e.yml b/.github/workflows/mobile-sdk-demo-e2e.yml index 0b31ac989..c5dfca17f 100644 --- a/.github/workflows/mobile-sdk-demo-e2e.yml +++ b/.github/workflows/mobile-sdk-demo-e2e.yml @@ -40,10 +40,8 @@ on: workflow_dispatch: jobs: - android-e2e: - name: Android E2E Tests Demo App - # Currently build-only for Android. E2E steps are preserved but skipped (if: false). - # To re-enable full E2E: change `if: false` to `if: true` on emulator steps. + android-build: + name: Android Build Validation Demo App concurrency: group: ${{ github.workflow }}-android-${{ github.ref }} cancel-in-progress: true @@ -108,23 +106,6 @@ jobs: max_attempts: 3 retry_wait_seconds: 5 command: yarn install --immutable --silent - - name: Validate Maestro test file - if: false # Skip for build-only test - keep logic for future E2E - run: | - [ -f packages/mobile-sdk-demo/tests/e2e/launch.android.flow.yaml ] || { echo "❌ Android E2E test file missing"; exit 1; } - - name: Cache Maestro - if: false # Skip for build-only test - keep logic for future E2E - id: cache-maestro - uses: actions/cache@v4 - with: - path: ~/.maestro - key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }} - - name: Install Maestro - if: false # Skip for build-only test - keep logic for future E2E - run: curl -Ls "https://get.maestro.mobile.dev" | bash - - name: Add Maestro to path - if: false # Skip for build-only test - keep logic for future E2E - run: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" - name: Free up disk space uses: ./.github/actions/free-disk-space - name: Setup Java environment @@ -145,11 +126,6 @@ jobs: max_attempts: 3 retry_wait_seconds: 10 command: sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}" - - name: Enable KVM group perms - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - name: Build dependencies run: | echo "Building dependencies..." @@ -178,37 +154,6 @@ jobs: echo "📱 APK size: $APK_SIZE bytes" echo "🎉 Build verification completed successfully!" - echo "ℹ️ Emulator testing is temporarily disabled - build testing only" - - name: Run Maestro tests on Android - if: false # Skip emulator/E2E for build-only test - keep logic for future E2E - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ env.ANDROID_API_LEVEL }} - arch: x86_64 - target: google_apis - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none -memory 8192 -cores 4 -accel on - disable-animations: true - script: | - echo "Installing app on emulator..." - APK_PATH="packages/mobile-sdk-demo/android/app/build/outputs/apk/debug/app-debug.apk" - [ -f "$APK_PATH" ] || { echo "❌ APK not found at $APK_PATH"; exit 1; } - adb install -r "$APK_PATH" || { echo "❌ App installation failed"; exit 1; } - echo "✅ App installed successfully" - - echo "⏰ Giving the emulator a moment to settle..." - sleep 5 - - echo "🎭 Running Maestro tests..." - export MAESTRO_DRIVER_STARTUP_TIMEOUT=180000 - maestro test packages/mobile-sdk-demo/tests/e2e/launch.android.flow.yaml --format junit --output packages/mobile-sdk-demo/maestro-results-android.xml - - name: Upload Android test results - if: false # Skip for build-only test - keep logic for future E2E - uses: actions/upload-artifact@v4 - with: - name: maestro-results-android - path: packages/mobile-sdk-demo/maestro-results-android.xml - if-no-files-found: warn ios-e2e: timeout-minutes: 60 diff --git a/.github/workflows/rn-sdk-test-app-ci.yml b/.github/workflows/rn-sdk-test-app-ci.yml new file mode 100644 index 000000000..c9b6c7d3e --- /dev/null +++ b/.github/workflows/rn-sdk-test-app-ci.yml @@ -0,0 +1,113 @@ +name: RN SDK Test App CI + +permissions: + contents: read + +on: + pull_request: + paths: + - "packages/rn-sdk/**" + - "packages/rn-sdk-test-app/**" + - "packages/kmp-sdk/**" + - "packages/self-sdk-swift/**" + - ".github/workflows/rn-sdk-test-app-ci.yml" + - ".github/actions/**" + push: + branches: [dev, staging, main] + paths: + - "packages/rn-sdk/**" + - "packages/rn-sdk-test-app/**" + - "packages/kmp-sdk/**" + - "packages/self-sdk-swift/**" + - ".github/workflows/rn-sdk-test-app-ci.yml" + - ".github/actions/**" + +jobs: + types: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Check for nested require() in tests + run: | + if grep -rE "require\(['\"]react(-native)?['\"])" packages/rn-sdk/src/__tests__/ 2>/dev/null; then + echo "❌ Found nested require() patterns that cause OOM in CI" + exit 1 + fi + echo "✅ No nested require() patterns found" + - name: Build webview-bridge + run: yarn workspace @selfxyz/webview-bridge build + - name: Typecheck rn-sdk + run: yarn workspace @selfxyz/rn-sdk typecheck + - name: Test rn-sdk + run: yarn workspace @selfxyz/rn-sdk test + - name: Build rn-sdk types + run: yarn workspace @selfxyz/rn-sdk tsup + - name: Typecheck rn-sdk-test-app + run: yarn workspace @selfxyz/rn-sdk-test-app types + + android-build: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - name: Generate debug keystore + run: | + keytool -genkeypair -v -keystore packages/rn-sdk-test-app/android/app/debug.keystore \ + -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 \ + -storepass android -keypass android \ + -dname "CN=Android Debug,O=Android,C=US" + - name: Publish KMP SDK to mavenLocal + run: | + cd packages/kmp-sdk && ./gradlew :shared:publishToMavenLocal --quiet + - name: Build Android debug APK + run: | + chmod +x packages/rn-sdk-test-app/android/gradlew + cd packages/rn-sdk-test-app/android && ./gradlew assembleDebug --quiet --parallel --build-cache --no-configuration-cache + + ios-build: + runs-on: namespace-profile-apple-silicon-6cpu + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + - name: Generate token for private dependencies + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" + - name: Configure git for HTTPS dependency fetch + run: git config --global url."https://github.com/".insteadOf "git@github.com:" + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Build webview-bridge + run: yarn workspace @selfxyz/webview-bridge build + - name: Install CocoaPods dependencies + working-directory: packages/rn-sdk-test-app/ios + run: pod install + - name: Find iOS Simulator + id: sim + uses: ./.github/actions/find-ios-simulator + - name: Build iOS app + working-directory: packages/rn-sdk-test-app/ios + run: | + xcodebuild -workspace SelfRNTestApp.xcworkspace \ + -scheme SelfRNTestApp \ + -sdk iphonesimulator \ + -destination "id=${{ steps.sim.outputs.id }}" \ + ONLY_ACTIVE_ARCH=YES \ + build diff --git a/.github/workflows/swift-sdk-ci.yml b/.github/workflows/swift-sdk-ci.yml new file mode 100644 index 000000000..238e53094 --- /dev/null +++ b/.github/workflows/swift-sdk-ci.yml @@ -0,0 +1,45 @@ +name: Swift SDK CI + +permissions: + contents: read + +on: + pull_request: + paths: + - "packages/self-sdk-swift/**" + - ".github/workflows/swift-sdk-ci.yml" + - ".github/actions/**" + push: + branches: [dev, staging, main] + paths: + - "packages/self-sdk-swift/**" + - ".github/workflows/swift-sdk-ci.yml" + - ".github/actions/**" + +jobs: + build: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + runs-on: namespace-profile-apple-silicon-6cpu + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Generate token for private dependencies + uses: ./.github/actions/generate-github-token + id: github-token + with: + app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }} + private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }} + configure-netrc: "true" + - name: Configure git for HTTPS dependency fetch + run: git config --global url."https://github.com/".insteadOf "git@github.com:" + - name: Resolve Swift package dependencies + working-directory: packages/self-sdk-swift + run: swift package resolve + - name: Build Swift package for iOS Simulator + working-directory: packages/self-sdk-swift + run: | + xcodebuild build \ + -scheme SelfSdkSwift \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + ONLY_ACTIVE_ARCH=NO diff --git a/.github/workflows/webview-app-ci.yml b/.github/workflows/webview-app-ci.yml new file mode 100644 index 000000000..0d935ab64 --- /dev/null +++ b/.github/workflows/webview-app-ci.yml @@ -0,0 +1,44 @@ +name: WebView App CI + +permissions: + contents: read + +on: + pull_request: + paths: + - "packages/webview-app/**" + - "packages/webview-bridge/**" + - ".github/workflows/webview-app-ci.yml" + - ".github/actions/**" + push: + branches: [dev, staging, main] + paths: + - "packages/webview-app/**" + - "packages/webview-bridge/**" + - ".github/workflows/webview-app-ci.yml" + - ".github/actions/**" + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Build webview-bridge + run: yarn workspace @selfxyz/webview-bridge build + - name: Build webview-app + run: yarn workspace @selfxyz/webview-app build + + types: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Build webview-bridge + run: yarn workspace @selfxyz/webview-bridge build + - name: Typecheck + run: yarn workspace @selfxyz/webview-app typecheck diff --git a/.github/workflows/webview-bridge-ci.yml b/.github/workflows/webview-bridge-ci.yml new file mode 100644 index 000000000..16146fb8a --- /dev/null +++ b/.github/workflows/webview-bridge-ci.yml @@ -0,0 +1,48 @@ +name: WebView Bridge CI + +permissions: + contents: read + +on: + pull_request: + paths: + - "packages/webview-bridge/**" + - ".github/workflows/webview-bridge-ci.yml" + - ".github/actions/**" + push: + branches: [dev, staging, main] + paths: + - "packages/webview-bridge/**" + - ".github/workflows/webview-bridge-ci.yml" + - ".github/actions/**" + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Build + run: yarn workspace @selfxyz/webview-bridge build + + types: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Typecheck + run: yarn workspace @selfxyz/webview-bridge typecheck + + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Test + run: yarn workspace @selfxyz/webview-bridge test diff --git a/.gitignore b/.gitignore index 1eecf1280..b02436964 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,11 @@ app/platforms/ packages/mobile-sdk-alpha/licenses/ packages/mobile-sdk-alpha/platforms/ +# Renamed KMP test app (old folder, build artifacts only) +packages/kmp-test-app/.gradle/ +packages/kmp-test-app/build/ +packages/kmp-test-app/**/build/ + # Private Android modules (cloned at build time) app/android/android-passport-nfc-reader/ @@ -43,3 +48,7 @@ app/android/android-passport-nfc-reader/ contracts/out/ contracts/cache_forge/ contracts/broadcast/ + +# Keep RN test app config files tracked (global gitignore may ignore *.config.*) +!packages/rn-sdk-test-app/metro.config.cjs +!packages/rn-sdk-test-app/react-native.config.cjs diff --git a/.gitleaks.toml b/.gitleaks.toml index eec728b0a..d281b1029 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -23,6 +23,8 @@ minVersion = "v8.25.0" description = "global allow lists" paths = [ '''gitleaks\.toml''', + '''new-common/src/data/mockCertificates\.ts$''', + '''new-common/src/testing/mockAadhaarCert\.ts$''', '''(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$''', '''(?i)\.(?:eot|[ot]tf|woff2?)$''', '''(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf)$''', diff --git a/AGENTS.md b/AGENTS.md index a19a303ee..a70d7a2d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,7 @@ Before creating a PR, ensure: - [ ] PR description includes context for AI reviewers - [ ] Complex changes have inline comments explaining intent - [ ] Security-sensitive changes flagged for special review +- [ ] Review/spec text is signal-only: remove non-actionable praise, back-patting, and generic commentary; keep concrete issues, risks, decisions, owners, next steps, and validation evidence #### Follow-up Planning @@ -206,18 +207,29 @@ These workspace-specific files override or extend the root instructions for thei The `specs/` folder contains architecture and implementation specs for the Self SDK project (WebView engine + native shells). These specs are designed to serve as both human documentation and AI agent prompts. +### Spec Structure & Naming Rules + +- Do not create one-file folders. If a folder would contain only one markdown file, keep that file at the parent project level. +- File names should describe doc type, not repeat project name when already inside the project folder. +- Preferred project-level names: `INDEX.md`, `OVERVIEW.md`, `PLAN.md`, `STATUS.md`, `HANDOFF.md`, `REVIEW.md`, `ARCHITECTURE.md`, `INITIATIVE.md`. +- `INDEX.md` is navigation only (entrypoint/table of contents for that folder). +- `OVERVIEW.md` is substantive context (architecture/scope/status summary), not just a link list. +- Do not use `INDEX.md` and `OVERVIEW.md` as synonyms for the same purpose. +- Workstream docs under `workstreams//` use `SPEC.md` (context + implementation in one file). +- PR execution docs belong under `workstreams//plans/-.md`; use one plan file per PR. +- Use suffixed variants (for example `SPEC-.md`) only when multiple specs of the same type are required in the same folder. +- When renaming/moving spec files, update all references in `specs/`, `AGENTS.md`, and `CLAUDE.md` in the same change. + **Start here:** [specs/README.md](./specs/README.md) — table of contents and reading order. Key files: -- `specs/SDK-OVERVIEW.md` — Architecture, bridge protocol, module table, decision matrix -- `specs/WAVE-PLAN.md` — Dependency-ordered execution plan for parallel agent work -- `specs/SPEC-GUIDE.md` — How to write specs -- `specs/PROJECT-RULES.md` — Project-specific rules and guardrails -- `specs/person*/OVERVIEW.md` — Workstream orientation (what you own, dependencies) -- `specs/person*/SPEC.md` — Implementation details (chunks, code changes, I/O examples) +- `specs/projects/sdk/INDEX.md` — SDK project entry point, workstream links +- `specs/projects/sdk/OVERVIEW.md` — Architecture, bridge protocol, module table, execution status +- `specs/projects/sdk/workstreams/*/SPEC.md` — Durable workstream context, invariants, backlog, active plan links +- `specs/projects/sdk/workstreams/*/plans/*.md` — PR-sized execution plans -**Before implementing SDK work:** Read `specs/PROJECT-RULES.md` and the relevant workstream's `SPEC.md`. These specs contain explicit constraints ("You will NOT..."), validation commands, and file ownership boundaries that prevent common mistakes. +**Before implementing SDK work:** Read `CLAUDE.md` Key Rules and the relevant workstream `SPEC.md` under `specs/projects/sdk/workstreams/`. These specs contain explicit constraints ("You will NOT..."), validation commands, and file ownership boundaries that prevent common mistakes. ## Scope diff --git a/CLAUDE.md b/CLAUDE.md index 606155f12..22dd6008f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,9 +26,23 @@ nvm use && corepack enable && yarn install - **Native handlers are thin wrappers.** No business logic in Kotlin or Swift. All logic lives in TypeScript. - **Keychain is always native-managed.** No web fallbacks for secure storage. This is a security boundary. - **No “slop comments.”** Only add comments when they convey non-obvious intent or constraints. Never add generic or chatty comments. +- **Signal over praise in docs/reviews.** Remove feel-good or back-patting text that does not change decisions or actions. Keep only actionable content: concrete issues, risks, decisions, owners, next steps, and validation evidence. +- **Spec naming and structure must be context-first.** Use doc-type file names (for example `OVERVIEW.md`, `SPEC.md`) and do not repeat project prefixes in file names. Use descriptive labels in markdown links — `[SDK Overview](./OVERVIEW.md)` not `[OVERVIEW.md](./OVERVIEW.md)` — so the link text is meaningful without folder context. +- **No singleton spec folders.** Do not create a folder that exists only to hold one markdown file; keep single docs at the nearest meaningful project/shared root. +- **Workstream spec names are fixed.** Under `workstreams//`, use `SPEC.md` (context + implementation in one file); use `SPEC-.md` only when multiple implementation specs are needed in that same folder. +- **Use the two-layer spec model.** `INDEX.md` and `OVERVIEW.md` are stable project context. Each workstream `SPEC.md` is durable context plus backlog. PR execution lives in `workstreams//plans/-.md`. - **Test value over mock wiring.** Prefer tests that validate behavior. Avoid tests that only assert mocks were called unless that is the behavior being validated. - **PR size target:** 1k–3k LOC changed. Smaller is fine for focused fixes. If >3k, add a brief justification for why it can’t be split. - **No generated artifacts in source PRs.** Do not commit build outputs or generated assets unless the build system requires them for runtime or distribution. +- **Each chunk = one PR.** Don't bundle chunks into mega PRs. Keeps reviews fast, reverts clean, and progress visible. +- **TypeScript is the primary surface area.** All core logic (proving machine, state machines, stores, UI) lives in TypeScript in the WebView. Kotlin and Swift exist only for hardware access (NFC, camera, biometrics), OS-level APIs (keychain, lifecycle), and crypto signing/key-gen. Before writing any native code, ask: "Can this run in the WebView?" If yes or maybe, it belongs in TypeScript. +- **Maximize code reuse through `mobile-sdk-alpha`.** Before adding code to `webview-app`, `kmp-sdk`, or `app/`, check if `mobile-sdk-alpha` already has it or should have it. Types, interfaces, constants, parsing, validation, formatting, state machines, and stores belong in the SDK. Migrate shared code to `mobile-sdk-alpha` before building WebView UI that needs it. +- **Bridge protocol is the only coupling.** Native shells and the WebView share a JSON contract, not code. New native handlers must follow the bridge protocol exactly — no custom messaging, no side channels, no platform-specific extensions. The WebView must not know which native shell it's running inside. +- **Adapter interfaces are the coupling layer.** WebView code imports adapter interfaces from SDK core. Native shells implement bridge handlers. Nobody imports code across the bridge boundary. +- **Fail closed on security-critical boundaries.** Default-deny for protocol compatibility, remote bundle loading, and verification session lifecycle. Reject unknown protocol versions, block remote `devServerUrl` in production. +- **No regressions in the RN app.** Every change to `mobile-sdk-alpha` must be backwards-compatible with the existing Self Wallet app. +- **Specs stay current.** When implementation deviates from the spec, update the spec. A stale spec is worse than no spec. +- **Constraint tie-breaker.** If rules conflict: correctness and security first, then scope/clarity (small PRs, small files), then reuse. Document the tradeoff in the spec. ## Specs & Planning @@ -36,29 +50,46 @@ nvm use && corepack enable && yarn install ### Spec System (`specs/`) -| File | Purpose | When to Read | -| -------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------ | -| [README.md](./specs/README.md) | Table of contents, reading order | First. Always. | -| [SPEC-GUIDE.md](./specs/SPEC-GUIDE.md) | How to write specs (three-tier system, review checklist, AI agent guidelines) | Before writing or reviewing any spec | -| [TEMPLATES.md](./specs/TEMPLATES.md) | Copy-paste templates for all three tiers | When creating a new spec | -| [PROJECT-RULES.md](./specs/PROJECT-RULES.md) | Project-specific rules and guardrails | Before starting any implementation work | -| [SDK-OVERVIEW.md](./specs/SDK-OVERVIEW.md) | Architecture, bridge protocol, module table, decision matrix | For system-level context | -| [WAVE-PLAN.md](./specs/WAVE-PLAN.md) | Dependency-ordered execution plan | When planning which chunks to execute next | +| File | Purpose | When to Read | +| ------------------------------------------------ | ------------------------------------------- | ------------------------ | +| [Specs README](./specs/README.md) | Table of contents, reading order | First. Always. | +| [Templates](./specs/framework/TEMPLATES.md) | Copy-paste templates for all three tiers | When creating a new spec | +| [SDK Overview](./specs/projects/sdk/OVERVIEW.md) | Architecture, bridge protocol, module table | For system-level context | -Workstream specs live in `specs/person*-*/` with `OVERVIEW.md` (stable orientation) and `SPEC.md` (living implementation details). +Workstream specs live in `specs/projects/sdk/workstreams/*/` with `SPEC.md` (living implementation details). + +### Spec-Reading Protocol (for chunk execution) + +To execute a chunk: + +1. Read `specs/projects/sdk/INDEX.md` — find your workstream +2. Read the workstream `SPEC.md` — find your chunk +3. If you need architecture context, read the project `OVERVIEW.md` + +That's it. Do not read framework docs unless you are writing a new spec. ### Planning Protocol -1. **Read** `specs/PROJECT-RULES.md` and the relevant workstream specs — understand the current state and constraints -2. **Write a plan to disk** — use the appropriate tier from `specs/TEMPLATES.md`: - - **Large features / new workstreams:** Create a full implementation spec (`specs/person-scope/SPEC.md`) - - **Medium features / multi-chunk work:** Create a session plan file in `specs/` or update the relevant SPEC.md - - **Small features / single-chunk fixes:** Add a chunk to an existing SPEC.md, or create a minimal plan in the spec folder +1. **Read** the relevant workstream specs and this file's Key Rules — understand the current state and constraints +2. **Write a plan to disk** — use the appropriate tier from `specs/framework/TEMPLATES.md`: + - **Large features / new workstreams:** Create a full implementation spec (`specs/projects/sdk/workstreams//SPEC.md`) + - **Medium features / multi-chunk work:** Create a plan file in `workstreams//plans/` named `-.md` and link it from the backlog in the relevant `SPEC.md` + - **Small features / single-chunk fixes:** Create a minimal plan file in `workstreams//plans/` named `-.md` or add the chunk to an existing active plan 3. **Include in every plan:** scope of work, files modified, I/O examples, validation command, definition of done 4. **Then implement** — update chunk status as you complete work -5. **After completion:** Mark chunks done in both SPEC.md and OVERVIEW.md status checklists +5. **After completion:** Mark chunks done in SPEC.md status tables. Review status checklists at session start — if something is marked "Done" that isn't, or "Pending" that's in progress, fix it first. -Quick-start prompts for creating new specs are in [SPEC-GUIDE.md](./specs/SPEC-GUIDE.md#quick-start). +### Spec-Writing Guidelines + +When writing specs, follow these principles so they work as AI agent prompts: + +- **Use second person.** "You are making X portable" not "X should be made portable." +- **Be explicit about constraints.** "You will NOT modify..." not just "Focus on..." +- **Provide exact file paths with line numbers.** `src/proving/provingMachine.ts:543` not "the proving machine file." +- **State the validation command.** Agents will run it. If it's not there, they'll skip validation. +- **One chunk = one self-contained prompt.** The chunk must include enough context to execute without reading the full spec. +- **One PR = one plan file.** A plan file is the execution handoff. It must be self-contained enough that a new agent can pick it up after session loss. +- **Use `--remote` for M and L chunks.** Medium and large chunks benefit from `claude --remote` so work continues in the background. ### Why Even Minor Features diff --git a/app/AGENTS.md b/app/AGENTS.md index cf0e37494..78c99161d 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -258,5 +258,5 @@ See `.cursor/rules/test-memory-optimization.mdc` for comprehensive guidelines, e The Self Wallet app serves as a **test environment** for the SDK refactor. For SDK architecture context: -- **[SDK Specs](../specs/README.md)** — Table of contents and reading order -- **[SDK Overview](../specs/SDK-OVERVIEW.md)** — System architecture, bridge protocol, decision matrix +- **[SDK Overview](../specs/projects/sdk/OVERVIEW.md)** — System architecture, bridge protocol, decision matrix +- **[SDK Project Index](../specs/projects/sdk/INDEX.md)** — Workstream links and entry point diff --git a/app/Gemfile.lock b/app/Gemfile.lock index 6e4265122..e6dd8c67b 100644 --- a/app/Gemfile.lock +++ b/app/Gemfile.lock @@ -23,8 +23,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1220.0) - aws-sdk-core (3.242.0) + aws-partitions (1.1223.0) + aws-sdk-core (3.243.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -35,8 +35,8 @@ GEM aws-sdk-kms (1.122.0) aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.213.0) - aws-sdk-core (~> 3, >= 3.241.4) + aws-sdk-s3 (1.215.0) + aws-sdk-core (~> 3, >= 3.243.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -237,7 +237,7 @@ GEM i18n (1.14.8) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.18.1) + json (2.19.0) jwt (2.10.2) base64 logger (1.7.0) @@ -271,7 +271,7 @@ GEM declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - retriable (3.2.1) + retriable (3.3.0) rexml (3.4.4) rouge (3.28.0) ruby-macho (2.5.1) diff --git a/app/babel.config.cjs b/app/babel.config.cjs index 37dcde959..8ad90d030 100644 --- a/app/babel.config.cjs +++ b/app/babel.config.cjs @@ -2,9 +2,31 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. +// tsup wraps require() as __require() in ESM builds for externalized modules. +// Metro's dependency collector only recognizes standard require() calls, so __require() +// calls are invisible to bundling. This plugin converts them back to require() so Metro +// can resolve and include the assets (e.g. .lottie files from the SDK dist). +function rewriteDunderRequire() { + return { + visitor: { + CallExpression(path) { + if ( + path.node.callee.type === 'Identifier' && + path.node.callee.name === '__require' && + path.node.arguments.length === 1 && + path.node.arguments[0].type === 'StringLiteral' + ) { + path.node.callee.name = 'require'; + } + }, + }, + }; +} + module.exports = { presets: ['module:@react-native/babel-preset'], plugins: [ + rewriteDunderRequire, [ 'module-resolver', { diff --git a/app/env.sample b/app/env.sample index de71afad0..de867c802 100644 --- a/app/env.sample +++ b/app/env.sample @@ -1,6 +1,8 @@ ENABLE_DEBUG_LOGS= GITLEAKS_LICENSE= GOOGLE_SIGNIN_ANDROID_CLIENT_ID= +GOOGLE_SIGNIN_IOS_CLIENT_ID= +GOOGLE_SIGNIN_WEB_CLIENT_ID= GRAFANA_LOKI_URL= GRAFANA_LOKI_USERNAME= GRAFANA_LOKI_PASSWORD= @@ -9,4 +11,3 @@ MIXPANEL_NFC_PROJECT_TOKEN= SEGMENT_KEY= SENTRY_DSN= SUMSUB_TEE_URL= -IS_TEST_BUILD= diff --git a/app/env.ts b/app/env.ts index 28b238f5a..e4be1c79a 100644 --- a/app/env.ts +++ b/app/env.ts @@ -13,6 +13,9 @@ export const ENABLE_DEBUG_LOGS = process.env.ENABLE_DEBUG_LOGS === 'true'; export const GOOGLE_SIGNIN_ANDROID_CLIENT_ID = process.env.GOOGLE_SIGNIN_ANDROID_CLIENT_ID; +export const GOOGLE_SIGNIN_IOS_CLIENT_ID = + process.env.GOOGLE_SIGNIN_IOS_CLIENT_ID; + export const GOOGLE_SIGNIN_WEB_CLIENT_ID = process.env.GOOGLE_SIGNIN_WEB_CLIENT_ID; diff --git a/app/fastlane/.env.secrets.example b/app/fastlane/.env.secrets.example index 107f170fb..686b5480b 100644 --- a/app/fastlane/.env.secrets.example +++ b/app/fastlane/.env.secrets.example @@ -1,4 +1,7 @@ ANDROID_KEYSTORE= +GOOGLE_SIGNIN_ANDROID_CLIENT_ID= +GOOGLE_SIGNIN_IOS_CLIENT_ID= +GOOGLE_SIGNIN_WEB_CLIENT_ID= ANDROID_KEYSTORE_PASSWORD= ANDROID_KEY_ALIAS= ANDROID_KEY_PASSWORD= diff --git a/app/ios/LiveMRZScannerView.swift b/app/ios/LiveMRZScannerView.swift index 7def9801d..9e253a428 100644 --- a/app/ios/LiveMRZScannerView.swift +++ b/app/ios/LiveMRZScannerView.swift @@ -1,7 +1,5 @@ // 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 -// LiveMRZScannerView.swift - import SwiftUI import QKMRZParser @@ -13,177 +11,11 @@ struct LiveMRZScannerView: View { var onScanComplete: ((QKMRZResult) -> Void)? = nil var onScanResultAsDict: (([String: Any]) -> Void)? = nil - func singleCorrectDocumentNumberInMRZ(result: String, docNumber: String, parser: QKMRZParser) -> QKMRZResult? { - let replacements: [Character: [Character]] = [ - // "0": ["O", "D"], - // "1": ["I"], - "O": ["0"], - "D": ["0"], - "I": ["1"], - "L": ["1"], - "S": ["5"], - "G": ["6"], - // "2": ["Z"], "Z": ["2"], - // "8": ["B"], "B": ["8"] - ] - let lines = result.components(separatedBy: "\n") - guard lines.count >= 2 else { return nil } - for (i, char) in docNumber.enumerated() { - if let subs = replacements[char] { - for sub in subs { - var chars = Array(docNumber) - chars[i] = sub - let candidate = String(chars) - if let range = lines[1].range(of: docNumber) { - var newLine = lines[1] - let start = newLine.distance(from: newLine.startIndex, to: range.lowerBound) - var lineChars = Array(newLine) - let docNumChars = Array(candidate) - for j in 0.. [String: Any] { - return [ - "documentType": result.documentType, - "countryCode": result.countryCode, - "surnames": result.surnames, - "givenNames": result.givenNames, - "documentNumber": result.documentNumber, - "nationalityCountryCode": result.nationalityCountryCode, - "dateOfBirth": result.birthdate?.description ?? "", - "sex": result.sex ?? "", - "expiryDate": result.expiryDate?.description ?? "", - "personalNumber": result.personalNumber, - "personalNumber2": result.personalNumber2 ?? "", - "isDocumentNumberValid": result.isDocumentNumberValid, - "isBirthdateValid": result.isBirthdateValid, - "isExpiryDateValid": result.isExpiryDateValid, - "isPersonalNumberValid": result.isPersonalNumberValid ?? false, - "allCheckDigitsValid": result.allCheckDigitsValid - ] - } - - private func correctBelgiumDocumentNumber(result: String) -> String? { - // Belgium TD1 format: IDBEL000001115<7027 - let line1RegexPattern = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9<]{3})(?\\d)" - guard let line1Regex = try? NSRegularExpression(pattern: line1RegexPattern) else { return nil } - let line1Matcher = line1Regex.firstMatch(in: result, options: [], range: NSRange(location: 0, length: result.count)) - - if let line1Matcher = line1Matcher { - let doc9Range = line1Matcher.range(withName: "doc9") - let doc3Range = line1Matcher.range(withName: "doc3") - let checkDigitRange = line1Matcher.range(withName: "checkDigit") - - let doc9 = (result as NSString).substring(with: doc9Range) - let doc3 = (result as NSString).substring(with: doc3Range) - let checkDigit = (result as NSString).substring(with: checkDigitRange) - - if let cleanedDoc = cleanBelgiumDocumentNumber(doc9: doc9, doc3: doc3, checkDigit: checkDigit) { - let correctedMRZLine = "IDBEL\(cleanedDoc)\(checkDigit)" - return correctedMRZLine - } - } - return nil - } - - private func cleanBelgiumDocumentNumber(doc9: String, doc3: String, checkDigit: String) -> String? { - // For Belgium TD1 format: IDBEL000001115<7027 - // doc9 = "000001115" (9 digits) - // doc3 = "702" (3 digits after <) - // checkDigit = "7" (single check digit) - - var cleanDoc9 = doc9 - // Strip first 3 characters - let startIndex = cleanDoc9.index(cleanDoc9.startIndex, offsetBy: 3) - cleanDoc9 = String(cleanDoc9[startIndex...]) - - let fullDocumentNumber = cleanDoc9 + doc3 - - - return fullDocumentNumber - } - - private func isValidMRZResult(_ result: QKMRZResult) -> Bool { - return result.isDocumentNumberValid && result.isExpiryDateValid && result.isBirthdateValid - } - private func handleValidMRZResult(_ result: QKMRZResult) { parsedMRZ = result scanComplete = true onScanComplete?(result) - onScanResultAsDict?(mapVisionResultToDictionary(result)) - } - - private func processBelgiumDocument(result: String, parser: QKMRZParser) -> QKMRZResult? { - print("[LiveMRZScannerView] Processing Belgium document") - - guard let correctedBelgiumLine = correctBelgiumDocumentNumber(result: result) else { - print("[LiveMRZScannerView] Failed to correct Belgium document number") - return nil - } - - // print("[LiveMRZScannerView] Belgium corrected line: \(correctedBelgiumLine)") - - // Split MRZ into lines and replace the first line - let lines = result.components(separatedBy: "\n") - guard lines.count >= 3 else { - print("[LiveMRZScannerView] Invalid MRZ format - not enough lines") - return nil - } - - let originalFirstLine = lines[0] - // print("[LiveMRZScannerView] Original first line: \(originalFirstLine)") - - // Pad the corrected line to 30 characters (TD1 format) - let paddedCorrectedLine = correctedBelgiumLine.padding(toLength: 30, withPad: "<", startingAt: 0) - // print("[LiveMRZScannerView] Padded corrected line: \(paddedCorrectedLine)") - - // Reconstruct the MRZ with the corrected first line - var correctedLines = lines - correctedLines[0] = paddedCorrectedLine - let correctedMRZString = correctedLines.joined(separator: "\n") - // print("[LiveMRZScannerView] Corrected MRZ string: \(correctedMRZString)") - - guard let belgiumMRZResult = parser.parse(mrzString: correctedMRZString) else { - print("[LiveMRZScannerView] Belgium MRZ result is not valid") - return nil - } - - // print("[LiveMRZScannerView] Belgium MRZ result: \(belgiumMRZResult)") - - // Try the corrected MRZ first - if isValidMRZResult(belgiumMRZResult) { - return belgiumMRZResult - } - - // If document number is still invalid, try single character correction - if !belgiumMRZResult.isDocumentNumberValid { - if let correctedResult = singleCorrectDocumentNumberInMRZ(result: correctedMRZString, docNumber: belgiumMRZResult.documentNumber, parser: parser) { - // print("[LiveMRZScannerView] Single correction successful: \(correctedResult)") - if isValidMRZResult(correctedResult) { - return correctedResult - } - } - } - - return nil + onScanResultAsDict?(MrzResultMapper.toDictionary(result)) } var body: some View { @@ -191,35 +23,27 @@ struct LiveMRZScannerView: View { CameraView( frameHandler: { image, roi in if scanComplete { return } - MRZScanner.scan(image: image, roi: roi) { result, boxes in + MrzScanEngine.scan(image: image, roi: roi) { result, boxes in recognizedText = result lastMRZDetection = Date() - // print("[LiveMRZScannerView] result: \(result)") let parser = QKMRZParser(ocrCorrection: false) if let mrzResult = parser.parse(mrzString: result) { - let doc = mrzResult - // print("[LiveMRZScannerView] doc: \(doc)") - guard !scanComplete else { return } - // Check if already valid - if doc.allCheckDigitsValid { + if mrzResult.allCheckDigitsValid { handleValidMRZResult(mrzResult) return } - // Handle Belgium documents (only if not already valid) - if doc.countryCode == "BEL" { - if let belgiumResult = processBelgiumDocument(result: result, parser: parser) { + if mrzResult.countryCode == "BEL" { + if let belgiumResult = MrzOcrCorrection.processBelgiumDocument(mrzString: result, parser: parser) { handleValidMRZResult(belgiumResult) } return } - // Handle other documents with invalid document numbers - if !doc.isDocumentNumberValid { - if let correctedResult = singleCorrectDocumentNumberInMRZ(result: result, docNumber: doc.documentNumber, parser: parser) { - // print("[LiveMRZScannerView] correctedDoc: \(correctedResult)") + if !mrzResult.isDocumentNumberValid { + if let correctedResult = MrzOcrCorrection.singleCorrectDocumentNumber(mrzString: result, docNumber: mrzResult.documentNumber, parser: parser) { if correctedResult.allCheckDigitsValid { handleValidMRZResult(correctedResult) } diff --git a/app/ios/MrzOcrCorrection.swift b/app/ios/MrzOcrCorrection.swift new file mode 100644 index 000000000..9b1794d33 --- /dev/null +++ b/app/ios/MrzOcrCorrection.swift @@ -0,0 +1,113 @@ +// 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 QKMRZParser + +struct MrzOcrCorrection { + + static func isValid(_ result: QKMRZResult) -> Bool { + return result.isDocumentNumberValid && result.isExpiryDateValid && result.isBirthdateValid + } + + static func singleCorrectDocumentNumber(mrzString: String, docNumber: String, parser: QKMRZParser) -> QKMRZResult? { + let replacements: [Character: [Character]] = [ + "O": ["0"], + "D": ["0"], + "I": ["1"], + "L": ["1"], + "S": ["5"], + "G": ["6"], + ] + let lines = mrzString.components(separatedBy: "\n") + guard lines.count >= 2 else { return nil } + // TD1 (3-line): doc number is on line 0; TD3 (2-line): doc number is on line 1 + let docNumberLineIndex = lines.count == 3 ? 0 : 1 + for (i, char) in docNumber.enumerated() { + if let subs = replacements[char] { + for sub in subs { + var chars = Array(docNumber) + chars[i] = sub + let candidate = String(chars) + if let range = lines[docNumberLineIndex].range(of: docNumber) { + var newLine = lines[docNumberLineIndex] + let start = newLine.distance(from: newLine.startIndex, to: range.lowerBound) + var lineChars = Array(newLine) + let docNumChars = Array(candidate) + for j in 0.. QKMRZResult? { + guard let correctedLine = correctBelgiumDocumentNumber(mrzString: mrzString) else { + return nil + } + + let lines = mrzString.components(separatedBy: "\n") + guard lines.count >= 3 else { return nil } + + let paddedLine = correctedLine.padding(toLength: 30, withPad: "<", startingAt: 0) + + var correctedLines = lines + correctedLines[0] = paddedLine + let correctedMRZString = correctedLines.joined(separator: "\n") + + guard let belgiumResult = parser.parse(mrzString: correctedMRZString) else { + return nil + } + + if isValid(belgiumResult) { + return belgiumResult + } + + if !belgiumResult.isDocumentNumberValid { + if let correctedResult = singleCorrectDocumentNumber(mrzString: correctedMRZString, docNumber: belgiumResult.documentNumber, parser: parser) { + if isValid(correctedResult) { + return correctedResult + } + } + } + + return nil + } + + // MARK: - Private + + private static func correctBelgiumDocumentNumber(mrzString: String) -> String? { + let line1RegexPattern = "IDBEL(?[A-Z0-9]{9})<(?[A-Z0-9<]{3})(?\\d)" + guard let line1Regex = try? NSRegularExpression(pattern: line1RegexPattern) else { return nil } + let line1Matcher = line1Regex.firstMatch(in: mrzString, options: [], range: NSRange(mrzString.startIndex..., in: mrzString)) + + if let line1Matcher = line1Matcher { + let doc9Range = line1Matcher.range(withName: "doc9") + let doc3Range = line1Matcher.range(withName: "doc3") + let checkDigitRange = line1Matcher.range(withName: "checkDigit") + + let doc9 = (mrzString as NSString).substring(with: doc9Range) + let doc3 = (mrzString as NSString).substring(with: doc3Range) + let checkDigit = (mrzString as NSString).substring(with: checkDigitRange) + + let startIndex = doc9.index(doc9.startIndex, offsetBy: 3) + let cleanDoc9 = String(doc9[startIndex...]) + let fullDocumentNumber = cleanDoc9 + doc3 + + return "IDBEL\(fullDocumentNumber)\(checkDigit)" + } + return nil + } +} diff --git a/app/ios/MrzResultMapper.swift b/app/ios/MrzResultMapper.swift new file mode 100644 index 000000000..a618bc689 --- /dev/null +++ b/app/ios/MrzResultMapper.swift @@ -0,0 +1,28 @@ +// 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 QKMRZParser + +struct MrzResultMapper { + static func toDictionary(_ result: QKMRZResult) -> [String: Any] { + return [ + "documentType": result.documentType, + "countryCode": result.countryCode, + "surnames": result.surnames, + "givenNames": result.givenNames, + "documentNumber": result.documentNumber, + "nationalityCountryCode": result.nationalityCountryCode, + "dateOfBirth": result.birthdate?.description ?? "", + "sex": result.sex ?? "", + "expiryDate": result.expiryDate?.description ?? "", + "personalNumber": result.personalNumber, + "personalNumber2": result.personalNumber2 ?? "", + "isDocumentNumberValid": result.isDocumentNumberValid, + "isBirthdateValid": result.isBirthdateValid, + "isExpiryDateValid": result.isExpiryDateValid, + "isPersonalNumberValid": result.isPersonalNumberValid ?? false, + "allCheckDigitsValid": result.allCheckDigitsValid + ] + } +} diff --git a/packages/mobile-sdk-alpha/ios/SelfSDK/SelfMRZScanner.swift b/app/ios/MrzScanEngine.swift similarity index 69% rename from packages/mobile-sdk-alpha/ios/SelfSDK/SelfMRZScanner.swift rename to app/ios/MrzScanEngine.swift index 683a44c32..3749f0151 100644 --- a/packages/mobile-sdk-alpha/ios/SelfSDK/SelfMRZScanner.swift +++ b/app/ios/MrzScanEngine.swift @@ -2,14 +2,10 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -// -// MRZScanner.swift - import Vision import UIKit -struct SelfMRZScanner { +struct MrzScanEngine { static func scan(image: UIImage, roi: CGRect? = nil, completion: @escaping (String, [CGRect]) -> Void) { guard let cgImage = image.cgImage else { DispatchQueue.main.async { @@ -31,37 +27,30 @@ struct SelfMRZScanner { return } - // print("Found \(observations.count) text observations") - var mrzLines: [String] = [] var boxes: [CGRect] = [] - // Sort lines from top to bottom let sortedObservations = observations.sorted { $0.boundingBox.minY > $1.boundingBox.minY } - for (index, obs) in sortedObservations.enumerated() { + for (_, obs) in sortedObservations.enumerated() { if let candidate = obs.topCandidates(1).first { let text = candidate.string - let confidence = candidate.confidence - // print("Line \(index): '\(text)' (confidence: \(confidence), position: \(obs.boundingBox))") - // Check if this looks like an MRZ line (either contains "<" or matches MRZ pattern) // TD1 format (ID cards): 30 chars, TD3 format (passports): 44 chars - if text.contains("<") || - text.matches(pattern: "^[A-Z0-9<]{30}$") || //TD1 //case where there's no '<' in MRZ - text.matches(pattern: "^[A-Z0-9<]{44}$") //TD3 - { - // print("Matched MRZ pattern: \(text)") + if text.matches(pattern: "^[A-Z0-9<]{30}$") || + text.matches(pattern: "^[A-Z0-9<]{44}$") { + if let currentWidth = mrzLines.first?.count, currentWidth != text.count { + mrzLines = [] + boxes = [] + } + mrzLines.append(text) boxes.append(obs.boundingBox) - // Check if we have a complete MRZ - if (mrzLines.count == 2 && mrzLines.allSatisfy { $0.count == 44 }) || // TD3 - passport - (mrzLines.count == 3 && mrzLines.allSatisfy { $0.count == 30 }) { // TD1 - ID card + if (mrzLines.count == 2 && mrzLines.allSatisfy { $0.count == 44 }) || + (mrzLines.count == 3 && mrzLines.allSatisfy { $0.count == 30 }) { break } - } else { - print("Did not match MRZ pattern: \(text)") } } } @@ -80,15 +69,12 @@ struct SelfMRZScanner { request.usesLanguageCorrection = false request.recognitionLanguages = ["en"] - // Use provided ROI. If not use as bottom 20% if let roi = roi { - // print("[MRZScanner] Using provided ROI: \(roi) (image size: \(cgImage.width)x\(cgImage.height))") request.regionOfInterest = roi } else { let imageHeight = CGFloat(cgImage.height) - let roiHeight = imageHeight * 0.2 // Bottom 20% + let roiHeight = imageHeight * 0.2 let defaultRoi = CGRect(x: 0, y: 0, width: 1.0, height: roiHeight / imageHeight) - // print("[MRZScanner] Using default ROI: \(defaultRoi) (image size: \(cgImage.width)x\(cgImage.height), roi height: \(roiHeight))") request.regionOfInterest = defaultRoi } diff --git a/app/ios/OpenPassport/Info.plist b/app/ios/OpenPassport/Info.plist index f5af29238..146fd66c5 100644 --- a/app/ios/OpenPassport/Info.plist +++ b/app/ios/OpenPassport/Info.plist @@ -31,6 +31,10 @@ proofofpassport com.warroom.proofofpassport + + + + com.googleusercontent.apps.YOUR_CLIENT_ID diff --git a/app/ios/OpenPassport/OpenPassport.entitlements b/app/ios/OpenPassport/OpenPassport.entitlements index 514a3d982..92fe066b2 100644 --- a/app/ios/OpenPassport/OpenPassport.entitlements +++ b/app/ios/OpenPassport/OpenPassport.entitlements @@ -37,5 +37,9 @@ com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) + com.apple.developer.applesignin + + Default + diff --git a/app/ios/OpenPassport/OpenPassportDebug.entitlements b/app/ios/OpenPassport/OpenPassportDebug.entitlements index 50490a0e0..7182fae53 100644 --- a/app/ios/OpenPassport/OpenPassportDebug.entitlements +++ b/app/ios/OpenPassport/OpenPassportDebug.entitlements @@ -39,5 +39,9 @@ com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) + com.apple.developer.applesignin + + Default + diff --git a/app/ios/Podfile b/app/ios/Podfile index b7b9c7738..119a8e2e2 100755 --- a/app/ios/Podfile +++ b/app/ios/Podfile @@ -86,7 +86,7 @@ target "Self" do # Explicitly declare Mixpanel to ensure it's available even in E2E builds # (NFCPassportReader also includes Mixpanel, but is skipped during E2E testing) - pod "Mixpanel-swift", :modular_headers => true + pod "Mixpanel-swift", "~> 5.0.0", :modular_headers => true pod "QKMRZScanner" pod "lottie-ios" @@ -101,11 +101,8 @@ target "Self" do # Flipper設定は削除 ) - pod "Firebase", :modular_headers => true - pod "FirebaseCore", :modular_headers => true - pod "FirebaseCoreInternal", :modular_headers => true - pod "GoogleUtilities", :modular_headers => true - pod "FirebaseMessaging" + # Firebase pods removed - handled automatically by RNFirebase autolinking + # This resolves GoogleUtilities 7.x vs 8.x version conflicts if flipper_enabled pod "RCT-Folly", :podspec => "#{config[:reactNativePath]}/third-party-podspecs/RCT-Folly.podspec" diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 17bf54f74..fe5076db8 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -5,6 +5,10 @@ PODS: - AppAuth/Core (2.0.0) - AppAuth/ExternalUserAgent (2.0.0): - AppAuth/Core + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) - boost (1.84.0) - BVLinearGradient (2.8.3): - React-Core @@ -37,6 +41,10 @@ PODS: - ExpoModulesCore - Expo (52.0.49): - ExpoModulesCore + - ExpoAdapterGoogleSignIn (16.1.2): + - ExpoModulesCore + - GoogleSignIn (~> 9.0) + - React-Core - ExpoAsset (11.0.5): - ExpoModulesCore - ExpoFileSystem (18.0.12): @@ -72,141 +80,86 @@ PODS: - fast_float (6.1.4) - FBLazyVector (0.77.0) - FingerprintPro (2.13.0) - - Firebase (10.24.0): - - Firebase/Core (= 10.24.0) - - Firebase/Core (10.24.0): + - Firebase/CoreOnly (11.11.0): + - FirebaseCore (~> 11.11.0) + - Firebase/Messaging (11.11.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 10.24.0) - - Firebase/CoreOnly (10.24.0): - - FirebaseCore (= 10.24.0) - - Firebase/Messaging (10.24.0): + - FirebaseMessaging (~> 11.11.0) + - Firebase/RemoteConfig (11.11.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.24.0) - - Firebase/RemoteConfig (10.24.0): - - Firebase/CoreOnly - - FirebaseRemoteConfig (~> 10.24.0) - - FirebaseABTesting (10.29.0): - - FirebaseCore (~> 10.0) - - FirebaseAnalytics (10.24.0): - - FirebaseAnalytics/AdIdSupport (= 10.24.0) - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - - GoogleUtilities/MethodSwizzler (~> 7.11) - - GoogleUtilities/Network (~> 7.11) - - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (10.24.0): - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.24.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - - GoogleUtilities/MethodSwizzler (~> 7.11) - - GoogleUtilities/Network (~> 7.11) - - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseCore (10.24.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.12) - - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreExtension (10.29.0): - - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.29.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.29.0): - - FirebaseCore (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.24.0): - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleDataTransport (~> 9.3) - - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/Reachability (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseRemoteConfig (10.24.0): - - FirebaseABTesting (~> 10.0) - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 10.0) - - FirebaseRemoteConfigInterop (~> 10.23) - - FirebaseSharedSwift (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseRemoteConfigInterop (10.29.0) - - FirebaseSharedSwift (10.29.0) + - FirebaseRemoteConfig (~> 11.11.0) + - FirebaseABTesting (11.11.0): + - FirebaseCore (~> 11.11.0) + - FirebaseCore (11.11.0): + - FirebaseCoreInternal (~> 11.11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.11.0): + - FirebaseCore (~> 11.11.0) + - FirebaseCoreInternal (11.11.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseInstallations (11.11.0): + - FirebaseCore (~> 11.11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (11.11.0): + - FirebaseCore (~> 11.11.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Reachability (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) + - FirebaseRemoteConfig (11.11.0): + - FirebaseABTesting (~> 11.0) + - FirebaseCore (~> 11.11.0) + - FirebaseInstallations (~> 11.0) + - FirebaseRemoteConfigInterop (~> 11.0) + - FirebaseSharedSwift (~> 11.0) + - GoogleUtilities/Environment (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseRemoteConfigInterop (11.15.0) + - FirebaseSharedSwift (11.15.0) - fmt (11.0.2) - glog (0.3.5) - - GoogleAppMeasurement (10.24.0): - - GoogleAppMeasurement/AdIdSupport (= 10.24.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - - GoogleUtilities/MethodSwizzler (~> 7.11) - - GoogleUtilities/Network (~> 7.11) - - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (10.24.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.24.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - - GoogleUtilities/MethodSwizzler (~> 7.11) - - GoogleUtilities/Network (~> 7.11) - - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (10.24.0): - - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - - GoogleUtilities/MethodSwizzler (~> 7.11) - - GoogleUtilities/Network (~> 7.11) - - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleDataTransport (9.4.1): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30911.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities (7.13.3): - - GoogleUtilities/AppDelegateSwizzler (= 7.13.3) - - GoogleUtilities/Environment (= 7.13.3) - - GoogleUtilities/ISASwizzler (= 7.13.3) - - GoogleUtilities/Logger (= 7.13.3) - - GoogleUtilities/MethodSwizzler (= 7.13.3) - - GoogleUtilities/Network (= 7.13.3) - - "GoogleUtilities/NSData+zlib (= 7.13.3)" - - GoogleUtilities/Privacy (= 7.13.3) - - GoogleUtilities/Reachability (= 7.13.3) - - GoogleUtilities/SwizzlerTestHelpers (= 7.13.3) - - GoogleUtilities/UserDefaults (= 7.13.3) - - GoogleUtilities/AppDelegateSwizzler (7.13.3): + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleSignIn (9.1.0): + - AppAuth (~> 2.0) + - AppCheckCore (~> 11.0) + - GTMAppAuth (~> 5.0) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Environment (8.1.0): - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/ISASwizzler (7.13.3): - - GoogleUtilities/Privacy - - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/MethodSwizzler (7.13.3): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - GoogleUtilities/Network (7.13.3): + - GoogleUtilities/Network (8.1.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.13.3)": + - "GoogleUtilities/NSData+zlib (8.1.0)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.3) - - GoogleUtilities/Reachability (7.13.3): + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/SwizzlerTestHelpers (7.13.3): - - GoogleUtilities/MethodSwizzler - - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - GTMAppAuth (5.0.0): + - AppAuth/Core (~> 2.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher/Core (3.5.0) - hermes-engine (0.77.0): - hermes-engine/Pre-built (= 0.77.0) - hermes-engine/Pre-built (0.77.0) @@ -223,11 +176,11 @@ PODS: - Mixpanel-swift (5.0.0): - Mixpanel-swift/Complete (= 5.0.0) - Mixpanel-swift/Complete (5.0.0) - - nanopb (2.30910.0): - - nanopb/decode (= 2.30910.0) - - nanopb/encode (= 2.30910.0) - - nanopb/decode (2.30910.0) - - nanopb/encode (2.30910.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - NFCPassportReader (2.1.1): - Mixpanel-swift (~> 5.0.0) - OpenSSL-Universal (= 1.1.1900) @@ -1517,7 +1470,7 @@ PODS: - React-Core - react-native-nfc-manager (3.17.2): - React-Core - - react-native-passkey (3.3.1): + - react-native-passkey (3.3.2): - DoubleConversion - glog - hermes-engine @@ -1916,6 +1869,8 @@ PODS: - React-logger (= 0.77.0) - React-perflogger (= 0.77.0) - React-utils (= 0.77.0) + - RNAppleAuthentication (2.5.1): + - React-Core - RNCAsyncStorage (2.2.0): - DoubleConversion - glog @@ -1960,16 +1915,16 @@ PODS: - Yoga - RNDeviceInfo (15.0.1): - React-Core - - RNFBApp (19.3.0): - - Firebase/CoreOnly (= 10.24.0) + - RNFBApp (21.14.0): + - Firebase/CoreOnly (= 11.11.0) - React-Core - - RNFBMessaging (19.3.0): - - Firebase/Messaging (= 10.24.0) + - RNFBMessaging (21.14.0): + - Firebase/Messaging (= 11.11.0) - FirebaseCoreExtension - React-Core - RNFBApp - - RNFBRemoteConfig (19.3.0): - - Firebase/RemoteConfig (= 10.24.0) + - RNFBRemoteConfig (21.14.0): + - Firebase/RemoteConfig (= 11.11.0) - React-Core - RNFBApp - RNGestureHandler (2.22.1): @@ -1993,6 +1948,28 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNGoogleSignin (16.1.2): + - DoubleConversion + - glog + - GoogleSignIn (~> 9.0) + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RNInAppBrowser (3.7.0): - React-Core - RNKeychain (10.0.0): @@ -2126,7 +2103,7 @@ PODS: - ReactCommon/turbomodule/core - Sentry/HybridSDK (= 8.53.2) - Yoga - - RNSVG (15.12.1): + - RNSVG (15.14.0): - DoubleConversion - glog - hermes-engine @@ -2146,9 +2123,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.12.1) + - RNSVG/common (= 15.14.0) - Yoga - - RNSVG/common (15.12.1): + - RNSVG/common (15.14.0): - DoubleConversion - glog - hermes-engine @@ -2188,6 +2165,7 @@ DEPENDENCIES: - EXApplication (from `../node_modules/expo-application/ios`) - EXConstants (from `../node_modules/expo-constants/ios`) - Expo (from `../node_modules/expo`) + - "ExpoAdapterGoogleSignIn (from `../node_modules/@react-native-google-signin/google-signin/expo/ios`)" - ExpoAsset (from `../node_modules/expo-asset/ios`) - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) - ExpoFont (from `../node_modules/expo-font/ios`) @@ -2195,16 +2173,11 @@ DEPENDENCIES: - ExpoModulesCore (from `../node_modules/expo-modules-core`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - Firebase - - FirebaseCore - - FirebaseCoreInternal - - FirebaseMessaging - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - - GoogleUtilities - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - lottie-ios - - Mixpanel-swift + - Mixpanel-swift (~> 5.0.0) - "NFCPassportReader (from `git@github.com:selfxyz/NFCPassportReader.git`, commit `9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b`)" - QKMRZScanner - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) @@ -2280,6 +2253,7 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNAppleAuthentication (from `../node_modules/@invertase/react-native-apple-authentication`)" - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) @@ -2287,6 +2261,7 @@ DEPENDENCIES: - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" - "RNFBRemoteConfig (from `../node_modules/@react-native-firebase/remote-config`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - "RNGoogleSignin (from `../node_modules/@react-native-google-signin/google-signin`)" - RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`) - RNKeychain (from `../node_modules/react-native-keychain`) - RNLocalize (from `../node_modules/react-native-localize`) @@ -2304,10 +2279,10 @@ SPEC REPOS: - IdensicMobileSDK trunk: - AppAuth + - AppCheckCore - FingerprintPro - Firebase - FirebaseABTesting - - FirebaseAnalytics - FirebaseCore - FirebaseCoreExtension - FirebaseCoreInternal @@ -2316,9 +2291,11 @@ SPEC REPOS: - FirebaseRemoteConfig - FirebaseRemoteConfigInterop - FirebaseSharedSwift - - GoogleAppMeasurement - GoogleDataTransport + - GoogleSignIn - GoogleUtilities + - GTMAppAuth + - GTMSessionFetcher - lottie-ios - LottieFiles-dotLottie-iOS - Mixpanel-swift @@ -2346,6 +2323,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-constants/ios" Expo: :path: "../node_modules/expo" + ExpoAdapterGoogleSignIn: + :path: "../node_modules/@react-native-google-signin/google-signin/expo/ios" ExpoAsset: :path: "../node_modules/expo-asset/ios" ExpoFileSystem: @@ -2512,6 +2491,8 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNAppleAuthentication: + :path: "../node_modules/@invertase/react-native-apple-authentication" RNCAsyncStorage: :path: "../node_modules/@react-native-async-storage/async-storage" RNCClipboard: @@ -2526,6 +2507,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-firebase/remote-config" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" + RNGoogleSignin: + :path: "../node_modules/@react-native-google-signin/google-signin" RNInAppBrowser: :path: "../node_modules/react-native-inappbrowser-reborn" RNKeychain: @@ -2559,6 +2542,7 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063 + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70 dotlottie-react-native: 056445614fe969f8d8d90a744944089261e6a620 @@ -2566,6 +2550,7 @@ SPEC CHECKSUMS: EXApplication: 4c72f6017a14a65e338c5e74fca418f35141e819 EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b Expo: 4bb70893882e6382b41d1e910d7226c6a1b85f0a + ExpoAdapterGoogleSignIn: ab4d9fc38cb91077a4138d178395525ec65d0c2e ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516 ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655 ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188 @@ -2574,28 +2559,29 @@ SPEC CHECKSUMS: fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 2bc03a5cf64e29c611bbc5d7eb9d9f7431f37ee6 FingerprintPro: 2f419138022451a72f783db9c94967f5a68e9977 - Firebase: 91fefd38712feb9186ea8996af6cbdef41473442 - FirebaseABTesting: d87f56707159bae64e269757a6e963d490f2eebe - FirebaseAnalytics: b5efc493eb0f40ec560b04a472e3e1a15d39ca13 - FirebaseCore: 11dc8a16dfb7c5e3c3f45ba0e191a33ac4f50894 - FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f - FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 - FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd - FirebaseMessaging: 4d52717dd820707cc4eadec5eb981b4832ec8d5d - FirebaseRemoteConfig: 95dddc50496b37eef199dadce850d5652b534b43 - FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d - FirebaseSharedSwift: 20530f495084b8d840f78a100d8c5ee613375f6e + Firebase: 6a8f201c61eda24e98f1ce2b44b1b9c2caf525cc + FirebaseABTesting: 8551c24eb28e300ce697f8eb72c1a519bb96eb40 + FirebaseCore: 2321536f9c423b1f857e047a82b8a42abc6d9e2c + FirebaseCoreExtension: 3a64994969dd05f4bcb7e6896c654eded238e75b + FirebaseCoreInternal: 31ee350d87b30a9349e907f84bf49ef8e6791e5a + FirebaseInstallations: 781e0e37aa0e1c92b44d00e739aba79ad31b2dba + FirebaseMessaging: c7be9357fd8ba33bc45b9a6c3cdff0b466e1e2a4 + FirebaseRemoteConfig: ca2e03fdd86e31d79ded53e24fa4ac719494dc35 + FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa + FirebaseSharedSwift: e17c654ef1f1a616b0b33054e663ad1035c8fd40 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 - GoogleAppMeasurement: f3abf08495ef2cba7829f15318c373b8d9226491 - GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 hermes-engine: 1f783c3d53940aed0d2c84586f0b7a85ab7827ef IdensicMobileSDK: 00b13320e1b1e0574e68475bd0fbc7cd30fce26e lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3 LottieFiles-dotLottie-iOS: e9b34e7cff6d04f5affd97336c2dab934b86e6fb Mixpanel-swift: e9bef28a9648faff384d5ba6f48ecc2787eb24c0 - nanopb: 438bc412db1928dac798aa6fd75726007be04262 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 NFCPassportReader: 48873f856f91215dbfa1eaaec20eae639672862e OpenSSL-Universal: 84efb8a29841f2764ac5403e0c4119a28b713346 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 @@ -2639,7 +2625,7 @@ SPEC CHECKSUMS: react-native-mobilesdk-module: 08c16fea2be97669f8e4c38153106e5fe698126a react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 react-native-nfc-manager: c8891e460b4943b695d63f7f4effc6345bbefc83 - react-native-passkey: 84eaf6d62d3f9cbb8bc3c837dcb9ca794eec5140 + react-native-passkey: 8818f842d1b80e45c06e906a5c85964719782bf5 react-native-safe-area-context: 5b5d3eb6ec9ef848f16c064a4eab4a92c7d7895e react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed react-native-webview: 05734d99f1e422c5ddfeefbd083d53abd78fccb1 @@ -2672,20 +2658,22 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 6e8d68583f39dc31ee65235110287277eb8556ef ReactCodegen: 58a974a1a86362975fd49596480c5f0f17ee06a2 ReactCommon: e686c5766f0ebe5293be5a3957b833645cdac8ad + RNAppleAuthentication: a89c9804592b38ed4ab11f0aee68d05ba12ad432 RNCAsyncStorage: 6a8127b6987dc9fbce778669b252b14c8355c7ce RNCClipboard: 9f7b908de4bf4353871fb454c15fc03db4917b88 RNDeviceInfo: 36d7f232bfe7c9b5c494cb7793230424ed32c388 - RNFBApp: 4097f75673f8b42a7cd1ba17e6ea85a94b45e4d1 - RNFBMessaging: 92325b0d5619ac90ef023a23cfd16fd3b91d0a88 - RNFBRemoteConfig: a569bacaa410acfcaba769370e53a787f80fd13b + RNFBApp: 4105e54d9ca4a1c10893a032268470f670181110 + RNFBMessaging: 6857871d9dff8f26b0c325fc7d97ba69cb77d213 + RNFBRemoteConfig: 8d3675f18c052483ce294bb97b857428467fb41e RNGestureHandler: 36aca36e4ef19f55dbf97239199d00fd58494e34 + RNGoogleSignin: 60c3f470558dbff0ae54f2f164ef82a89d3eb561 RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20 RNKeychain: 35beaa17938f7d8e4990d8a38fad5f8a748fc47c RNLocalize: 67cd0eece3ba20fb5dae7625d77f02e88d3d9573 RNReactNativeHapticFeedback: eb5395b503c7a8f10de5e6722ef8afd3c61bc4f5 RNScreens: b0811b109e1a0b8b579f3348018e177bee374840 RNSentry: 98ab9f6a16c9596e36565ccf1a5871323f334766 - RNSVG: f79679fe33eb77562fe7d6e3fbb0c8855829e549 + RNSVG: d926926b169d8b81eb06aeb69734076e1dd566a3 segment-analytics-react-native: 0eae155b0e9fa560fa6b17d78941df64537c35b7 Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 @@ -2694,6 +2682,6 @@ SPEC CHECKSUMS: SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb Yoga: c34725819ab0a5962e85455b9e56679b306910ee -PODFILE CHECKSUM: 63b07a2aa49988e11d648524199e37c6a96de7aa +PODFILE CHECKSUM: a95943ec849e3235c1bfecf266b2a6c6ffa3d0d6 COCOAPODS: 1.16.2 diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj index 10665e652..097e11c66 100644 --- a/app/ios/Self.xcodeproj/project.pbxproj +++ b/app/ios/Self.xcodeproj/project.pbxproj @@ -29,9 +29,11 @@ 97E31F23A5A11A2C115FE2BB /* Pods_Self.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0823092D57FC544FD63682A /* Pods_Self.framework */; }; AE6147EC2DC95A8D00445C0F /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AE6147EB2DC95A8D00445C0F /* GoogleService-Info.plist */; }; B49D2B112E28AA7900946F64 /* IBMPlexMono-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = B49D2B102E28AA7900946F64 /* IBMPlexMono-Regular.otf */; }; + BB000002000000000000001A /* MrzOcrCorrection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000002000000000000001B /* MrzOcrCorrection.swift */; }; + BB000003000000000000001A /* MrzResultMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000003000000000000001B /* MrzResultMapper.swift */; }; BF1044812DD53540009B3688 /* LiveMRZScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1044802DD53540009B3688 /* LiveMRZScannerView.swift */; }; BF1044832DD5354F009B3688 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1044822DD5354F009B3688 /* CameraView.swift */; }; - BF1044852DD53570009B3688 /* MRZScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1044842DD53570009B3688 /* MRZScanner.swift */; }; + BF1044852DD53570009B3688 /* MrzScanEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1044842DD53570009B3688 /* MrzScanEngine.swift */; }; BF5649262F43B1EB00DE07A1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5649252F43B1E700DE07A1 /* AppDelegate.swift */; }; BF6F0D552E38ED81008EA85C /* SelfAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6F0D542E38ED81008EA85C /* SelfAnalytics.swift */; }; BFBA0C772E339D2B00E82A52 /* NativeLoggerBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBA0C762E339D2B00E82A52 /* NativeLoggerBridge.swift */; }; @@ -74,9 +76,11 @@ A78F43717F170EC139960991 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Self/ExpoModulesProvider.swift"; sourceTree = ""; }; AE6147EB2DC95A8D00445C0F /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "GoogleService-Info.plist"; sourceTree = ""; }; B49D2B102E28AA7900946F64 /* IBMPlexMono-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "IBMPlexMono-Regular.otf"; path = "../src/assets/fonts/IBMPlexMono-Regular.otf"; sourceTree = SOURCE_ROOT; }; + BB000002000000000000001B /* MrzOcrCorrection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MrzOcrCorrection.swift; sourceTree = ""; }; + BB000003000000000000001B /* MrzResultMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MrzResultMapper.swift; sourceTree = ""; }; BF1044802DD53540009B3688 /* LiveMRZScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMRZScannerView.swift; sourceTree = ""; }; BF1044822DD5354F009B3688 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; - BF1044842DD53570009B3688 /* MRZScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MRZScanner.swift; sourceTree = ""; }; + BF1044842DD53570009B3688 /* MrzScanEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MrzScanEngine.swift; sourceTree = ""; }; BF5649252F43B1E700DE07A1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; BF6F0D542E38ED81008EA85C /* SelfAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfAnalytics.swift; sourceTree = ""; }; BFBA0C762E339D2B00E82A52 /* NativeLoggerBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeLoggerBridge.swift; sourceTree = ""; }; @@ -110,7 +114,9 @@ BF6F0D542E38ED81008EA85C /* SelfAnalytics.swift */, BFBA0C782E33A01F00E82A52 /* NativeLoggerBridge.m */, BFBA0C762E339D2B00E82A52 /* NativeLoggerBridge.swift */, - BF1044842DD53570009B3688 /* MRZScanner.swift */, + BF1044842DD53570009B3688 /* MrzScanEngine.swift */, + BB000002000000000000001B /* MrzOcrCorrection.swift */, + BB000003000000000000001B /* MrzResultMapper.swift */, BF1044822DD5354F009B3688 /* CameraView.swift */, BF1044802DD53540009B3688 /* LiveMRZScannerView.swift */, AE6147EB2DC95A8D00445C0F /* GoogleService-Info.plist */, @@ -435,7 +441,9 @@ 905B70072A72774000AFA232 /* PassportReader.m in Sources */, BFBA0C792E33A01F00E82A52 /* NativeLoggerBridge.m in Sources */, 16E6646E2B8D292500FDD6A0 /* QKMRZScannerViewRepresentable.swift in Sources */, - BF1044852DD53570009B3688 /* MRZScanner.swift in Sources */, + BF1044852DD53570009B3688 /* MrzScanEngine.swift in Sources */, + BB000002000000000000001A /* MrzOcrCorrection.swift in Sources */, + BB000003000000000000001A /* MrzResultMapper.swift in Sources */, 165E76BF2B8DC53A0000FA90 /* MRZScannerModule.m in Sources */, 905B70052A72767900AFA232 /* PassportReader.swift in Sources */, BF5649262F43B1EB00DE07A1 /* AppDelegate.swift in Sources */, diff --git a/app/jest.setup.js b/app/jest.setup.js index 6a93ee2f4..5f3197413 100644 --- a/app/jest.setup.js +++ b/app/jest.setup.js @@ -30,7 +30,6 @@ const NativeModules = { scanPassport: jest.fn(), trackEvent: jest.fn(), flush: jest.fn(), - reset: jest.fn(), }, ReactNativeBiometrics: { isSensorAvailable: jest.fn().mockResolvedValue({ @@ -716,8 +715,56 @@ jest.mock('@sentry/react-native', () => ({ }), })); +jest.mock('@react-native-google-signin/google-signin', () => ({ + GoogleSignin: { + configure: jest.fn(), + hasPlayServices: jest.fn().mockResolvedValue(true), + signIn: jest.fn().mockResolvedValue({ + type: 'success', + data: { + user: { + id: 'mock-google-user-id', + name: 'Mock User', + email: 'mock@example.com', + }, + }, + }), + signOut: jest.fn().mockResolvedValue(null), + getCurrentUser: jest.fn().mockResolvedValue(null), + getTokens: jest.fn().mockResolvedValue({ idToken: 'mock-token' }), + }, + GoogleSigninButton: 'GoogleSigninButton', + statusCodes: { + SIGN_IN_CANCELLED: 'SIGN_IN_CANCELLED', + IN_PROGRESS: 'IN_PROGRESS', + PLAY_SERVICES_NOT_AVAILABLE: 'PLAY_SERVICES_NOT_AVAILABLE', + }, +})); + +jest.mock('@invertase/react-native-apple-authentication', () => ({ + __esModule: true, + default: { + performRequest: jest.fn().mockResolvedValue({ + user: 'mock-apple-user-id', + fullName: { givenName: 'Mock', familyName: 'User' }, + email: 'mock@example.com', + }), + getCredentialStateForUser: jest.fn().mockResolvedValue(1), + onCredentialRevoked: jest.fn(() => jest.fn()), + isSupported: true, + State: { AUTHORIZED: 1 }, + Error: { CANCELED: 1001 }, + }, + AppleButton: 'AppleButton', + AppleRequestScope: { EMAIL: 0, FULL_NAME: 1 }, + AppleRequestOperation: { LOGIN: 1 }, +})); + jest.mock('@env', () => ({ ENABLE_DEBUG_LOGS: 'false', + GOOGLE_SIGNIN_ANDROID_CLIENT_ID: 'mock-google-client-id', + GOOGLE_SIGNIN_IOS_CLIENT_ID: 'mock-google-ios-client-id', + GOOGLE_SIGNIN_WEB_CLIENT_ID: 'mock-google-web-client-id', MIXPANEL_NFC_PROJECT_TOKEN: 'test-token', })); @@ -912,8 +959,8 @@ jest.mock('react-native-nfc-manager', () => ({ // Mock react-native-passport-reader jest.mock('react-native-passport-reader', () => { const mockScanPassport = jest.fn(); - // Mock the parameter count for scanPassport (iOS native method takes 9 parameters) - Object.defineProperty(mockScanPassport, 'length', { value: 9 }); + // Mock the parameter count for scanPassport (iOS native method takes 10 parameters) + Object.defineProperty(mockScanPassport, 'length', { value: 10 }); const mockPassportReader = { configure: jest.fn(), @@ -939,15 +986,14 @@ jest.mock('react-native-passport-reader', () => { // Mock @/integrations/nfc/passportReader to properly expose the interface expected by tests jest.mock('./src/integrations/nfc/passportReader', () => { const mockScanPassport = jest.fn(); - // Mock the parameter count for scanPassport (iOS native method takes 9 parameters) - Object.defineProperty(mockScanPassport, 'length', { value: 9 }); + // Mock the parameter count for scanPassport (iOS native method takes 10 parameters) + Object.defineProperty(mockScanPassport, 'length', { value: 10 }); const mockPassportReader = { configure: jest.fn(), scanPassport: mockScanPassport, trackEvent: jest.fn(), flush: jest.fn(), - reset: jest.fn(), }; return { diff --git a/app/package.json b/app/package.json index a8e4d51da..55bfb3f23 100644 --- a/app/package.json +++ b/app/package.json @@ -6,11 +6,11 @@ "scripts": { "analyze:bundle:android": "yarn build:deps && node ./scripts/bundle-analyze-ci.cjs android", "analyze:bundle:ios": "yarn build:deps && node ./scripts/bundle-analyze-ci.cjs ios", - "animations:convert": "node ./scripts/convert-to-dotlottie.mjs", "analyze:tree-shaking": "node ./scripts/analyze-tree-shaking.cjs imports", "analyze:tree-shaking:web": "yarn web:build && node ./scripts/analyze-tree-shaking.cjs web", "android": "yarn build:deps && yarn setup:android-deps && react-native run-android", "android:ci": "./scripts/mobile-ci-build-android.sh", + "animations:convert": "node ./scripts/convert-to-dotlottie.mjs", "build:deps": "yarn workspaces foreach --from @selfxyz/mobile-app --topological --recursive run build", "bump-version:major": "npm version major && yarn sync-versions", "bump-version:minor": "npm version minor && yarn sync-versions", @@ -88,20 +88,20 @@ "dependencies": { "@babel/runtime": "^7.28.6", "@ethersproject/shims": "^5.8.0", + "@invertase/react-native-apple-authentication": "^2.5.1", "@lottiefiles/dotlottie-react": "^0.17.15", "@lottiefiles/dotlottie-react-native": "0.5.0", "@noble/hashes": "^1.5.0", - "@openpassport/zk-kit-imt": "^0.0.5", "@openpassport/zk-kit-lean-imt": "^0.0.6", - "@openpassport/zk-kit-smt": "^0.0.1", "@peculiar/x509": "^1.14.3", "@react-native-async-storage/async-storage": "^2.2.0", "@react-native-clipboard/clipboard": "1.16.3", "@react-native-community/blur": "^4.4.1", "@react-native-community/netinfo": "^11.4.1", - "@react-native-firebase/app": "^19.0.1", - "@react-native-firebase/messaging": "^19.0.1", - "@react-native-firebase/remote-config": "^19.0.1", + "@react-native-firebase/app": "^21.14.0", + "@react-native-firebase/messaging": "^21.14.0", + "@react-native-firebase/remote-config": "^21.14.0", + "@react-native-google-signin/google-signin": "^16.1.1", "@react-navigation/native": "^7.0.14", "@react-navigation/native-stack": "^7.2.0", "@robinbobin/react-native-google-drive-api-wrapper": "^2.2.3", @@ -133,11 +133,6 @@ "ethers": "^6.11.0", "expo": "~52.0.40", "expo-application": "~6.0.2", - "hash.js": "^1.1.7", - "js-sha1": "^0.7.0", - "js-sha256": "^0.11.1", - "js-sha512": "^0.9.0", - "lottie-react": "^2.4.1", "node-forge": "^1.3.3", "pkijs": "^3.3.3", "poseidon-lite": "^0.2.0", @@ -166,7 +161,7 @@ "react-native-safe-area-context": "^5.6.2", "react-native-screens": "4.15.3", "react-native-sqlite-storage": "^6.0.1", - "react-native-svg": "15.12.1", + "react-native-svg": "15.14.0", "react-native-svg-web": "1.0.9", "react-native-url-polyfill": "^3.0.0", "react-native-web": "^0.21.2", @@ -198,19 +193,16 @@ "@tamagui/vite-plugin": "1.126.14", "@testing-library/react-native": "^13.3.3", "@tsconfig/react-native": "^3.0.6", - "@types/bn.js": "^5.2.0", "@types/dompurify": "^3.2.0", "@types/elliptic": "^6.4.18", "@types/jest": "^30.0.0", "@types/node": "^22.18.3", "@types/node-forge": "^1.3.14", - "@types/path-browserify": "^1", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", "@types/react-native-dotenv": "^0.2.0", "@types/react-native-sqlite-storage": "^6.0.5", "@types/react-native-web": "^0", - "@types/react-test-renderer": "^18", "@typescript-eslint/eslint-plugin": "^8.39.0", "@typescript-eslint/parser": "^8.39.0", "@vitejs/plugin-react-swc": "^4.2.2", @@ -228,9 +220,8 @@ "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-sort-exports": "^0.9.1", - "hermes-eslint": "^0.19.1", + "hermes-eslint": "^0.33.3", "jest": "^30.2.0", - "path-browserify": "^1.0.1", "prettier": "^3.5.3", "prop-types": "^15.8.1", "react-native-svg-transformer": "^1.5.2", @@ -238,7 +229,6 @@ "rollup-plugin-visualizer": "^6.0.5", "stream-browserify": "^3.0.0", "ts-morph": "^22.0.0", - "ts-node": "^10.9.2", "typescript": "^5.9.3", "vite": "^7.3.1", "vite-plugin-svgr": "^4.5.0" diff --git a/app/src/assets/icons/google.svg b/app/src/assets/icons/google.svg new file mode 100644 index 000000000..7cd50f0de --- /dev/null +++ b/app/src/assets/icons/google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/navigation/devTools.tsx b/app/src/navigation/devTools.tsx index b5cc92e91..dafe7b612 100644 --- a/app/src/navigation/devTools.tsx +++ b/app/src/navigation/devTools.tsx @@ -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 SocialLoginDemoScreen from '@/screens/dev/SocialLoginDemoScreen'; const devHeaderOptions: NativeStackNavigationOptions = { headerStyle: { @@ -81,6 +82,13 @@ const devScreens = { title: 'Dev Loading Screen', } as NativeStackNavigationOptions, }, + SocialLoginDemo: { + screen: SocialLoginDemoScreen, + options: { + ...devHeaderOptions, + title: 'Social Login Demo', + } as NativeStackNavigationOptions, + }, }; export default devScreens; diff --git a/app/src/navigation/types.ts b/app/src/navigation/types.ts index bf1087715..fc00bf79b 100644 --- a/app/src/navigation/types.ts +++ b/app/src/navigation/types.ts @@ -71,6 +71,7 @@ export type AppRoutesParamList = { export type DevRoutesParamList = { CreateMock: undefined; MockDataDeepLink: undefined; + SocialLoginDemo: undefined; }; // ============================================================================= diff --git a/app/src/screens/dev/SocialLoginDemoScreen.tsx b/app/src/screens/dev/SocialLoginDemoScreen.tsx new file mode 100644 index 000000000..5f0187625 --- /dev/null +++ b/app/src/screens/dev/SocialLoginDemoScreen.tsx @@ -0,0 +1,388 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useEffect, useRef, useState } from 'react'; +import { Alert, Platform } from 'react-native'; +import { Button, ScrollView, Text, XStack, YStack } from 'tamagui'; +import { GOOGLE_SIGNIN_IOS_CLIENT_ID, GOOGLE_SIGNIN_WEB_CLIENT_ID } from '@env'; +import appleAuth, { + AppleButton, + AppleRequestOperation, + AppleRequestScope, +} from '@invertase/react-native-apple-authentication'; +import { + GoogleSignin, + statusCodes, +} from '@react-native-google-signin/google-signin'; + +import { + red500, + slate100, + slate200, + slate500, + slate600, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import GoogleIcon from '@/assets/icons/google.svg'; + +type SocialUser = { + provider: 'google' | 'apple'; + id?: string; + name?: string; + email?: string; + tokensRetrieved?: boolean; +}; + +const formatFullName = (fullName?: { + givenName?: string | null; + familyName?: string | null; + middleName?: string | null; +}) => { + if (!fullName) { + return undefined; + } + + const nameParts = [ + fullName.givenName, + fullName.middleName, + fullName.familyName, + ].filter(Boolean); + + return nameParts.length > 0 ? nameParts.join(' ') : undefined; +}; + +const SocialLoginDemoScreen: React.FC = () => { + const authInFlightRef = useRef(false); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const isNative = Platform.OS === 'ios' || Platform.OS === 'android'; + const appleAvailable = Platform.OS === 'ios' && appleAuth.isSupported; + + useEffect(() => { + if (!isNative) { + return; + } + + GoogleSignin.configure({ + webClientId: GOOGLE_SIGNIN_WEB_CLIENT_ID, + iosClientId: GOOGLE_SIGNIN_IOS_CLIENT_ID, + }); + + let isMounted = true; + + const loadCurrentUser = async () => { + try { + const currentUser = await GoogleSignin.getCurrentUser(); + if (currentUser?.user && isMounted) { + setUser({ + provider: 'google', + id: currentUser.user.id, + name: currentUser.user.name ?? undefined, + email: currentUser.user.email ?? undefined, + }); + } + } catch (error) { + const code = (error as { code?: string }).code; + console.warn('Silent Google sign-in failed', code ?? 'unknown'); + } + }; + + loadCurrentUser().catch(() => {}); + + return () => { + isMounted = false; + }; + }, [isNative]); + + useEffect(() => { + if (!appleAvailable) { + return undefined; + } + + return appleAuth.onCredentialRevoked(async () => { + setUser(null); + }); + }, [appleAvailable]); + + const handleError = (title: string, message: string) => { + setErrorMessage(message); + Alert.alert(title, message); + }; + + const handleGoogleSignIn = async () => { + if (loading || authInFlightRef.current) { + return; + } + authInFlightRef.current = true; + setLoading(true); + setErrorMessage(null); + + try { + if (Platform.OS === 'android') { + await GoogleSignin.hasPlayServices(); + } + const response = await GoogleSignin.signIn(); + + if (response.type !== 'success') { + return; + } + + const tokens = await GoogleSignin.getTokens(); + + setUser({ + provider: 'google', + id: response.data.user.id, + name: response.data.user.name ?? undefined, + email: response.data.user.email ?? undefined, + tokensRetrieved: Boolean(tokens.accessToken), + }); + } catch (error: unknown) { + const code = (error as { code?: string }).code; + + if (code === statusCodes.SIGN_IN_CANCELLED) { + return; + } + + if (code === statusCodes.IN_PROGRESS) { + handleError('Google Sign-In', 'Sign-in already in progress.'); + return; + } + + if (code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) { + handleError('Google Sign-In', 'Google Play Services not available.'); + return; + } + + handleError( + 'Google Sign-In', + 'Unable to sign in with Google. Please try again.', + ); + } finally { + authInFlightRef.current = false; + setLoading(false); + } + }; + + const handleAppleSignIn = async () => { + if (loading || authInFlightRef.current) { + return; + } + if (!appleAvailable) { + handleError('Apple Sign-In', 'Apple Sign-In is not supported here.'); + return; + } + + authInFlightRef.current = true; + setLoading(true); + setErrorMessage(null); + + try { + const appleAuthRequestResponse = await appleAuth.performRequest({ + requestedOperation: AppleRequestOperation.LOGIN, + requestedScopes: [AppleRequestScope.EMAIL, AppleRequestScope.FULL_NAME], + }); + + const credentialState = await appleAuth.getCredentialStateForUser( + appleAuthRequestResponse.user, + ); + + if (credentialState !== appleAuth.State.AUTHORIZED) { + handleError( + 'Apple Sign-In', + 'Apple credential state is no longer valid.', + ); + return; + } + + // Apple identity token retrieved successfully - available for backend integration + + setUser({ + provider: 'apple', + id: appleAuthRequestResponse.user, + name: formatFullName(appleAuthRequestResponse.fullName ?? undefined), + email: appleAuthRequestResponse.email ?? undefined, + }); + } catch (error: unknown) { + const code = (error as { code?: string }).code; + if (code === appleAuth.Error.CANCELED) { + return; + } + + handleError( + 'Apple Sign-In', + 'Unable to sign in with Apple. Please try again.', + ); + } finally { + authInFlightRef.current = false; + setLoading(false); + } + }; + + const handleSignOut = async () => { + if (loading || authInFlightRef.current) { + return; + } + setLoading(true); + setErrorMessage(null); + + try { + if (user?.provider === 'google') { + await GoogleSignin.signOut(); + } + setUser(null); + } catch { + handleError('Sign Out', 'Unable to sign out. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + Social Login Demo + + + Use these buttons to test Google and Apple sign-in flows. Tokens can + be retrieved for backend integration. + + + + + + + {appleAvailable ? ( + + + + ) : ( + + Apple Sign-In is only available on iOS devices. + + )} + + {user && ( + + )} + + + + + Status + + {loading && ( + + Signing in... + + )} + {errorMessage && ( + + {errorMessage} + + )} + {user ? ( + + + Provider: {user.provider} + + + Name: {user.name ?? 'Not provided'} + + + Email: {user.email ?? 'Not provided'} + + + ID: {user.id ?? 'Not provided'} + + {user.tokensRetrieved && ( + + Tokens: Retrieved + + )} + + ) : ( + + No user signed in yet. + + )} + + + + ); +}; + +export default SocialLoginDemoScreen; diff --git a/app/src/screens/dev/sections/DebugShortcutsSection.tsx b/app/src/screens/dev/sections/DebugShortcutsSection.tsx index 95b26c0fe..fe42d56e3 100644 --- a/app/src/screens/dev/sections/DebugShortcutsSection.tsx +++ b/app/src/screens/dev/sections/DebugShortcutsSection.tsx @@ -53,6 +53,29 @@ export const DebugShortcutsSection: React.FC = ({ + {IS_DEV_MODE && (