chore: update dev with staging 09/06/25 (#1007)

* update CI

* bump iOS version

* update readme

* update mobile-deploy ci

* bump version iOS

* update workflow to use workload identity federation (#933)

* update workflow to use workload identity federation

* add token permissions

* correct provider name

* chore: incrementing android build version for version 2.6.4 [github action]

---------

Co-authored-by: Self GitHub Actions <action@github.com>

* update ci

* update ci

* update ci

* update ci

* update ci

* fix ci

* fix ci

* fix ci

* remove fastlane use for android

* bump iOS build version

* update CI python script

* iterate on CI

* iterate on CI

* iterate on CI

* Dev (#941)

* SDK Go version (#920)

* feat: helper functions and constant for go-sdk

* feat: formatRevealedDataPacked in go

* chore: refactor

* feat: define struct for selfBackendVerifier

* feat: verify function for selfBackendVerifier

* feat(wip): custom hasher

* feat: SelfVerifierBacked in go

* test(wip): scope and userContextHash is failing

* test: zk proof verified

* fix: MockConfigStore getactionId function

* chore: refactor

* chore: remove abi duplicate files

* chore: move configStore to utils

* chore: modified VcAndDiscloseProof struct

* chore: more review changes

* feat: impl DefaultConfig and InMemoryConfigStore

* chore: refactor and export functions

* fix: module import and README

* chore: remove example folder

* chore: remove pointers from VerificationConfig

* chore: coderabbit review fixes

* chore: more coderabbit review fix

* chore: add license

* fix: convert attestationIdd to int

* chore: remove duplicate code

---------

Co-authored-by: ayman <aymanshaik1015@gmail.com>

* Moving proving Utils to common (#935)

* remove react dom

* moves proving utils to the common

* need to use rn components

* fix imports

* add proving-utils and dedeuplicate entry configs for esm and cjs.

* must wrap in text component

* fix metro bundling

* fix mock import

* fix builds and tests

* please save me

* solution?

* fix test

* Move proving inputs to the common package (#937)

* create ofactTree type to share

* move proving inputs from app to register inputs in common

* missed reexport

* ok

* add some validations as suggested by our ai overlords

* Fix mock passport flow (#942)

* fix dev screens

* add hint

* rename

* fix path

* fix mobile-ci path

* fix: extractMRZ (#938)

* fix: extractMRZ

* yarn nice && yarn types

* fix test: remove unused

* fix mobile ci

* add script

---------

Co-authored-by: Justin Hernandez <transphorm@gmail.com>

* Move Proving attest and cose (#950)

* moved attest and cose utils to common

with cursor converted tests in common to use vitest and converted coseVerify.test to vitest after moving from app to common

what does cryptoLoader do?

* moved away

* get buff

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* SELF-253 feat: add user email feedback (#889)

* feat: add sentry feedback

* add sentry feedback to web

* feat: add custom feedback modal & fix freeze on IOS

* yarn nice

* update lock

* feat: show feedback widget on NFC scan issues (#948)

* feat: show feedback widget on NFC scan issues

* fix ref

* clean up

* fix report issue screen

* abstract send user feedback email logic

* fixes

* change text to Report Issue

* sanitize email and track event messge

* remove unnecessary sanitization

* add sanitize error message tests

* fix tests

* save wip. almost done

* fix screen test

* fix screen test

* remove non working test

---------

Co-authored-by: Justin Hernandez <transphorm@gmail.com>
Co-authored-by: Justin Hernandez <justin.hernandez@self.xyz>

* chore: centralize license header checks (#952)

* chore: centralize license header scripts

* chore: run license header checks from root

* add header to other files

* add header to bundle

* add migration script and update check license headers

* convert license to mobile sdk

* migrate license headers

* remove headers from common; convert remaining

* fix headers

* add license header checks

* update unsupported passport screen (#953)

* update unsupported passport screen

* yarn nice

---------

Co-authored-by: Vishalkulkarni45 <109329073+Vishalkulkarni45@users.noreply.github.com>
Co-authored-by: ayman <aymanshaik1015@gmail.com>
Co-authored-by: Aaron DeRuvo <aaron.deruvo@clabs.co>
Co-authored-by: Justin Hernandez <justin.hernandez@self.xyz>
Co-authored-by: Seshanth.S🐺 <35675963+seshanthS@users.noreply.github.com>
Co-authored-by: Justin Hernandez <transphorm@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* bump version

* bump yarn.lock

* update ci (#966)

* chore: Manually bump and release v2.6.4 (#961)

* update lock files

* bump and build android

* update build artifacts

* show generate mock document button

* update lock

* fix formatting and update failing e2e test

* revert podfile

* fixes

* fix cold start of the app with deeplink

* update ci

* update ci

* Sync MARKETING_VERSION to iOS project files after version bump

* chore: incrementing android build version for version 2.6.4 [github action] (#976)

Co-authored-by: remicolin <98749896+remicolin@users.noreply.github.com>

* chore: add build dependencies step for iOS and Android in mobile deploy workflow

* chore: enhance mobile deploy workflow by adding CMake installation step

* bump android build version

* chore: incrementing android build version for version 2.6.4 [github action] (#985)

Co-authored-by: remicolin <98749896+remicolin@users.noreply.github.com>

* chore: configure Metro bundler for production compatibility in mobile deploy workflow

* chore: incrementing android build version for version 2.6.4 [github action] (#987)

Co-authored-by: remicolin <98749896+remicolin@users.noreply.github.com>

* Revert "chore: configure Metro bundler for production compatibility in mobile deploy workflow"

This reverts commit 60fc1f2580.

* reduce max old space size in mobile-deploy ci

* fix android french id card (#957)

* fix android french id card

* fix common ci cache

* feat: log apdu (#988)

---------

Co-authored-by: Justin Hernandez <transphorm@gmail.com>
Co-authored-by: Seshanth.S🐺 <35675963+seshanthS@users.noreply.github.com>

* unblock ci

* fix merge

* merge fixes

* fix tests

* make ci happy

---------

Co-authored-by: turnoffthiscomputer <colin.remi07@gmail.com>
Co-authored-by: pputman-clabs <99900942+pputman-clabs@users.noreply.github.com>
Co-authored-by: Self GitHub Actions <action@github.com>
Co-authored-by: turnoffthiscomputer <98749896+remicolin@users.noreply.github.com>
Co-authored-by: Vishalkulkarni45 <109329073+Vishalkulkarni45@users.noreply.github.com>
Co-authored-by: ayman <aymanshaik1015@gmail.com>
Co-authored-by: Aaron DeRuvo <aaron.deruvo@clabs.co>
Co-authored-by: Seshanth.S🐺 <35675963+seshanthS@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Justin Hernandez
2025-09-07 11:19:59 -07:00
committed by GitHub
parent ec93ad564a
commit b6d526e5f8
31 changed files with 1040 additions and 232 deletions

View File

@@ -15,7 +15,7 @@ reviews:
auto_review: auto_review:
enabled: true enabled: true
drafts: false drafts: false
base_branches: ["main", "dev"] base_branches: ["main", "dev", "staging"]
tools: tools:
github-checks: github-checks:
timeout_ms: 300000 timeout_ms: 300000

View File

@@ -1,13 +1,9 @@
name: Circuits Build name: Circuits Build
on: on:
push:
branches:
- main
paths:
- "circuits/circuits/**"
- ".github/workflows/artifacts.yml"
pull_request: pull_request:
branches: branches:
- dev
- staging
- main - main
paths: paths:
- "circuits/circuits/**" - "circuits/circuits/**"

View File

@@ -1,21 +1,12 @@
name: Circuits CI name: Circuits CI
on: on:
push:
branches:
- dev
- main
- openpassportv2
paths:
- "circuits/**"
- "common/**"
pull_request: pull_request:
branches: branches:
- dev - dev
- staging
- main - main
- openpassportv2
paths: paths:
- "circuits/**" - "circuits/**"
- "common/**"
jobs: jobs:
run_circuit_tests: run_circuit_tests:
if: github.event.pull_request.draft == false if: github.event.pull_request.draft == false

View File

@@ -1,15 +1,9 @@
name: Contracts CI name: Contracts CI
on: on:
push:
branches:
- dev
- main
paths:
- "contracts/**"
- "common/**"
pull_request: pull_request:
branches: branches:
- dev - dev
- staging
- main - main
paths: paths:
- "contracts/**" - "contracts/**"

View File

@@ -16,7 +16,11 @@ env:
GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=4 -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.caching=true GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=4 -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.caching=true
CI: true CI: true
on: on:
push: pull_request:
branches:
- dev
- staging
- main
paths: paths:
- "common/**" - "common/**"
- "app/**" - "app/**"
@@ -98,7 +102,7 @@ jobs:
working-directory: ./ working-directory: ./
test: test:
runs-on: macos-latest runs-on: macos-latest-large
needs: build-deps needs: build-deps
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@@ -30,6 +30,7 @@ env:
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
id-token: write
on: on:
workflow_dispatch: workflow_dispatch:
@@ -106,6 +107,9 @@ jobs:
echo "📦 Version bump: ${{ inputs.version_bump }}" echo "📦 Version bump: ${{ inputs.version_bump }}"
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
fetch-depth: 0
ref: staging
- name: Read and sanitize Node.js version - name: Read and sanitize Node.js version
shell: bash shell: bash
run: | run: |
@@ -120,6 +124,24 @@ jobs:
echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV" echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV"
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV" echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- name: Verify branch and commit (iOS)
if: inputs.platform != 'android'
run: |
echo "🔍 Verifying we're building from the correct branch and commit..."
echo "Current branch: $(git branch --show-current || git symbolic-ref --short HEAD 2>/dev/null || echo 'detached')"
echo "Current commit: $(git rev-parse HEAD)"
echo "Current commit message: $(git log -1 --pretty=format:'%s')"
echo "Staging HEAD commit: $(git rev-parse origin/staging)"
echo "Staging HEAD message: $(git log -1 --pretty=format:'%s' origin/staging)"
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/staging)" ]; then
echo "⚠️ WARNING: Current commit differs from latest staging commit"
echo "This might indicate we're not building from the latest staging branch"
git log --oneline HEAD..origin/staging || true
else
echo "✅ Building from latest staging commit"
fi
- name: Set up Xcode - name: Set up Xcode
if: inputs.platform != 'android' if: inputs.platform != 'android'
uses: maxim-lobanov/setup-xcode@v1 uses: maxim-lobanov/setup-xcode@v1
@@ -148,8 +170,9 @@ jobs:
.yarn/cache .yarn/cache
node_modules node_modules
${{ env.APP_PATH }}/node_modules ${{ env.APP_PATH }}/node_modules
key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }} key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }}-${{ github.sha }}
restore-keys: | restore-keys: |
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }}-
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}- ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}- ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-
@@ -160,6 +183,7 @@ jobs:
path: ${{ env.APP_PATH }}/ios/vendor/bundle path: ${{ env.APP_PATH }}/ios/vendor/bundle
key: ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }} key: ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }}-
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}- ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}- ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-
@@ -171,7 +195,7 @@ jobs:
${{ env.APP_PATH }}/ios/Pods ${{ env.APP_PATH }}/ios/Pods
~/Library/Caches/CocoaPods ~/Library/Caches/CocoaPods
lock-file: app/ios/Podfile.lock lock-file: app/ios/Podfile.lock
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_PODS_CACHE_VERSION }} cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_PODS_CACHE_VERSION }}-${{ github.sha }}
- name: Log cache status - name: Log cache status
run: | run: |
@@ -415,6 +439,14 @@ jobs:
echo "✅ Provisioning profile installation steps completed." echo "✅ Provisioning profile installation steps completed."
- name: Build Dependencies (iOS)
if: inputs.platform != 'android'
run: |
echo "🏗️ Building SDK dependencies..."
cd ${{ env.APP_PATH }}
yarn workspace @selfxyz/mobile-app run build:deps --silent || { echo "❌ Dependency build failed"; exit 1; }
echo "✅ Dependencies built successfully"
# act won't work with macos, but you can test with `bundle exec fastlane ios ...` # act won't work with macos, but you can test with `bundle exec fastlane ios ...`
- name: Build and upload to App Store Connect/TestFlight - name: Build and upload to App Store Connect/TestFlight
if: inputs.platform != 'android' && !env.ACT if: inputs.platform != 'android' && !env.ACT
@@ -524,12 +556,17 @@ jobs:
with: with:
app_path: ${{ env.APP_PATH }} app_path: ${{ env.APP_PATH }}
- name: Commit updated build number - name: Open PR for iOS build number bump
if: ${{ !env.ACT && success() }} if: ${{ !env.ACT && success() }}
uses: ./.github/actions/push-changes uses: peter-evans/create-pull-request@v6
with: with:
commit_message: "incrementing ios build number for version ${{ env.VERSION }}" title: "chore: bump iOS build for ${{ env.VERSION }}"
commit_paths: "./app/version.json" body: "Automated bump of iOS build number by CI"
commit-message: "chore: incrementing ios build number for version ${{ env.VERSION }} [github action]"
branch: ci/bump-ios-build-${{ github.run_id }}
base: staging
add-paths: |
app/version.json
- name: Monitor cache usage - name: Monitor cache usage
if: always() if: always()
@@ -568,6 +605,71 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
if: inputs.platform != 'ios' if: inputs.platform != 'ios'
with:
fetch-depth: 0
ref: staging
- uses: 'google-github-actions/auth@v2'
with:
project_id: "plucky-tempo-454713-r0"
workload_identity_provider: "projects/852920390127/locations/global/workloadIdentityPools/gh-self/providers/github-by-repos"
service_account: "self-xyz@plucky-tempo-454713-r0.iam.gserviceaccount.com"
# Fail fast: set up JDK for keytool and verify Android secrets early
- name: Setup Java environment
if: inputs.platform != 'ios'
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Decode Android Secrets
if: inputs.platform != 'ios'
run: |
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}
- name: Verify Android Secrets
if: inputs.platform != 'ios'
run: |
# Verify Google Cloud auth via Workload Identity Federation (ADC)
if [ -z "$GOOGLE_APPLICATION_CREDENTIALS" ] || [ ! -f "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
echo "❌ Error: GOOGLE_APPLICATION_CREDENTIALS not set or file missing. Ensure google-github-actions/auth ran."
exit 1
fi
# Verify keystore file exists and is valid
if [ ! -f "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" ]; then
echo "❌ Error: Keystore file was not created successfully"
exit 1
fi
# Try to verify the keystore with the provided password
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >/dev/null 2>&1; then
echo "❌ Error: Invalid keystore password"
exit 1
fi
# Verify the key alias exists
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" >/dev/null 2>&1; then
echo "❌ Error: Key alias '${{ secrets.ANDROID_KEY_ALIAS }}' not found in keystore"
exit 1
fi
# Verify the key password
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" -keypass "${{ secrets.ANDROID_KEY_PASSWORD }}" >/dev/null 2>&1; then
echo "❌ Error: Invalid key password"
exit 1
fi
# Detect keystore type and export for later steps
KEYSTORE_TYPE=$(keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" 2>/dev/null | awk -F': ' '/Keystore type:/ {print $2; exit}')
if [ -z "$KEYSTORE_TYPE" ]; then
echo "❌ Error: Unable to determine keystore type"
exit 1
fi
echo "ANDROID_KEYSTORE_TYPE=$KEYSTORE_TYPE" >> "$GITHUB_ENV"
echo "Detected keystore type: $KEYSTORE_TYPE"
# Ensure the alias holds a PrivateKeyEntry (required for signing)
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" -keypass "${{ secrets.ANDROID_KEY_PASSWORD }}" | grep -q "Entry type: PrivateKeyEntry"; then
echo "❌ Error: Alias '${{ secrets.ANDROID_KEY_ALIAS }}' is not a PrivateKeyEntry"
exit 1
fi
echo "✅ All Android secrets verified successfully!"
- name: Read and sanitize Node.js version - name: Read and sanitize Node.js version
shell: bash shell: bash
run: | run: |
@@ -582,6 +684,24 @@ jobs:
echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV" echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV"
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV" echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- name: Verify branch and commit (Android)
if: inputs.platform != 'ios'
run: |
echo "🔍 Verifying we're building from the correct branch and commit..."
echo "Current branch: $(git branch --show-current || git symbolic-ref --short HEAD 2>/dev/null || echo 'detached')"
echo "Current commit: $(git rev-parse HEAD)"
echo "Current commit message: $(git log -1 --pretty=format:'%s')"
echo "Staging HEAD commit: $(git rev-parse origin/staging)"
echo "Staging HEAD message: $(git log -1 --pretty=format:'%s' origin/staging)"
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/staging)" ]; then
echo "⚠️ WARNING: Current commit differs from latest staging commit"
echo "This might indicate we're not building from the latest staging branch"
git log --oneline HEAD..origin/staging || true
else
echo "✅ Building from latest staging commit"
fi
- name: Cache Yarn dependencies - name: Cache Yarn dependencies
id: yarn-cache id: yarn-cache
uses: ./.github/actions/cache-yarn uses: ./.github/actions/cache-yarn
@@ -590,8 +710,9 @@ jobs:
.yarn/cache .yarn/cache
node_modules node_modules
${{ env.APP_PATH }}/node_modules ${{ env.APP_PATH }}/node_modules
key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }} key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }}-${{ github.sha }}
restore-keys: | restore-keys: |
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }}-
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}- ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}- ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-
@@ -602,6 +723,7 @@ jobs:
path: ${{ env.APP_PATH }}/ios/vendor/bundle path: ${{ env.APP_PATH }}/ios/vendor/bundle
key: ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }} key: ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }}-
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}- ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}- ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-
@@ -609,14 +731,14 @@ jobs:
id: gradle-cache id: gradle-cache
uses: ./.github/actions/cache-gradle uses: ./.github/actions/cache-gradle
with: with:
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GRADLE_CACHE_VERSION }} cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GRADLE_CACHE_VERSION }}-${{ github.sha }}
- name: Cache Android NDK - name: Cache Android NDK
id: ndk-cache id: ndk-cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.ANDROID_NDK_VERSION }} path: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.ANDROID_NDK_VERSION }}
key: ${{ runner.os }}-ndk-${{ env.ANDROID_NDK_VERSION }} key: ${{ runner.os }}-ndk-${{ env.ANDROID_NDK_VERSION }}-${{ github.sha }}
- name: Log cache status - name: Log cache status
run: | run: |
@@ -656,12 +778,6 @@ jobs:
workspace: ${{ env.WORKSPACE }} workspace: ${{ env.WORKSPACE }}
# android specific steps # android specific steps
- name: Setup Java environment
if: inputs.platform != 'ios'
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Setup Android SDK - name: Setup Android SDK
if: inputs.platform != 'ios' if: inputs.platform != 'ios'
@@ -669,16 +785,18 @@ jobs:
with: with:
accept-android-sdk-licenses: true accept-android-sdk-licenses: true
- name: Install NDK - name: Install NDK and CMake
if: inputs.platform != 'ios' && steps.ndk-cache.outputs.cache-hit != 'true' if: inputs.platform != 'ios' && steps.ndk-cache.outputs.cache-hit != 'true'
run: | run: |
max_attempts=5 max_attempts=5
attempt=1 attempt=1
# Install NDK
while [ $attempt -le $max_attempts ]; do while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt of $max_attempts to install NDK..." echo "Attempt $attempt of $max_attempts to install NDK..."
if sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}"; then if sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}"; then
echo "Successfully installed NDK" echo "Successfully installed NDK"
exit 0 break
fi fi
echo "Failed to install NDK on attempt $attempt" echo "Failed to install NDK on attempt $attempt"
if [ $attempt -eq $max_attempts ]; then if [ $attempt -eq $max_attempts ]; then
@@ -692,54 +810,47 @@ jobs:
attempt=$((attempt + 1)) attempt=$((attempt + 1))
done done
# Install CMake (required for native module builds)
echo "Installing CMake..."
attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt of $max_attempts to install CMake..."
if sdkmanager "cmake;3.22.1"; then
echo "Successfully installed CMake"
break
fi
echo "Failed to install CMake on attempt $attempt"
if [ $attempt -eq $max_attempts ]; then
echo "All attempts to install CMake failed"
exit 1
fi
# Exponential backoff: 2^attempt seconds
wait_time=$((2 ** attempt))
echo "Waiting $wait_time seconds before retrying..."
sleep $wait_time
attempt=$((attempt + 1))
done
- name: Set Gradle JVM options - name: Set Gradle JVM options
if: inputs.platform != 'ios' && env.ACT if: inputs.platform != 'ios' # Apply to CI builds (not just ACT)
run: | run: |
echo "org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8" >> ${{ env.APP_PATH }}/android/gradle.properties echo "org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8" >> ${{ env.APP_PATH }}/android/gradle.properties
- name: Decode Android Secrets - name: Install Python dependencies for Play Store upload
if: inputs.platform != 'ios' if: inputs.platform != 'ios'
run: | run: |
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }} python -m pip install --upgrade pip
echo "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_PLAY_STORE_JSON_KEY_PATH }} pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client
# run secrets check after keytool has been setup - name: Build Dependencies (Android)
- name: Verify Android Secrets
if: inputs.platform != 'ios' if: inputs.platform != 'ios'
run: | run: |
# Verify Play Store JSON key base64 secret exists and is valid echo "🏗️ Building SDK dependencies..."
if [ -z "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" ]; then cd ${{ env.APP_PATH }}
echo "❌ Error: Play Store JSON key base64 secret cannot be empty" yarn workspace @selfxyz/mobile-app run build:deps --silent || { echo "❌ Dependency build failed"; exit 1; }
exit 1 echo "✅ Dependencies built successfully"
fi
# Verify the base64 can be decoded
if ! echo "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" | base64 --decode >/dev/null 2>&1; then
echo "❌ Error: Invalid Play Store JSON key base64 format"
exit 1
fi
# Verify keystore file exists and is valid
if [ ! -f "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" ]; then
echo "❌ Error: Keystore file was not created successfully"
exit 1
fi
# Try to verify the keystore with the provided password
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >/dev/null 2>&1; then
echo "❌ Error: Invalid keystore password"
exit 1
fi
# Verify the key alias exists
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" >/dev/null 2>&1; then
echo "❌ Error: Key alias '${{ secrets.ANDROID_KEY_ALIAS }}' not found in keystore"
exit 1
fi
# Verify the key password
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" -keypass "${{ secrets.ANDROID_KEY_PASSWORD }}" >/dev/null 2>&1; then
echo "❌ Error: Invalid key password"
exit 1
fi
echo "✅ All Android secrets verified successfully!"
- name: Build and upload to Google Play Internal Testing - name: Build AAB with Fastlane
if: inputs.platform != 'ios' if: inputs.platform != 'ios'
env: env:
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }} ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
@@ -748,11 +859,7 @@ jobs:
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_PACKAGE_NAME: ${{ secrets.ANDROID_PACKAGE_NAME }} ANDROID_PACKAGE_NAME: ${{ secrets.ANDROID_PACKAGE_NAME }}
ANDROID_PLAY_STORE_JSON_KEY_PATH: ${{ env.APP_PATH }}${{ env.ANDROID_PLAY_STORE_JSON_KEY_PATH }} NODE_OPTIONS: "--max-old-space-size=6144"
NODE_OPTIONS: "--max-old-space-size=8192"
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
run: | run: |
cd ${{ env.APP_PATH }} cd ${{ env.APP_PATH }}
@@ -761,25 +868,34 @@ jobs:
VERSION_BUMP="${{ inputs.version_bump || 'build' }}" VERSION_BUMP="${{ inputs.version_bump || 'build' }}"
TEST_MODE="${{ inputs.test_mode || false }}" TEST_MODE="${{ inputs.test_mode || false }}"
echo "🤖 Deployment Configuration:" echo "🤖 Build Configuration:"
echo " - Track: $DEPLOYMENT_TRACK" echo " - Track: $DEPLOYMENT_TRACK"
echo " - Version Bump: $VERSION_BUMP" echo " - Version Bump: $VERSION_BUMP"
echo " - Test Mode: $TEST_MODE" echo " - Test Mode: $TEST_MODE"
if [ "$TEST_MODE" = "true" ]; then echo "🔨 Building AAB with Fastlane..."
echo "🧪 Running in TEST MODE - will skip upload to Play Store" bundle exec fastlane android build_only \
bundle exec fastlane android deploy_auto \ deployment_track:$DEPLOYMENT_TRACK \
deployment_track:$DEPLOYMENT_TRACK \ version_bump:$VERSION_BUMP \
version_bump:$VERSION_BUMP \ --verbose
test_mode:true \
--verbose - name: Upload to Google Play Store using WIF
else if: inputs.platform != 'ios' && inputs.test_mode != true
echo "🚀 Deploying to Google Play Store..." env:
bundle exec fastlane android deploy_auto \ SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
deployment_track:$DEPLOYMENT_TRACK \ SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
version_bump:$VERSION_BUMP \ SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
--verbose run: |
fi cd ${{ env.APP_PATH }}
# Determine deployment track
DEPLOYMENT_TRACK="${{ inputs.deployment_track || 'internal' }}"
echo "🚀 Uploading to Google Play Store using Workload Identity Federation..."
python scripts/upload_to_play_store.py \
--aab "android/app/build/outputs/bundle/release/app-release.aab" \
--package-name "${{ secrets.ANDROID_PACKAGE_NAME }}" \
--track "$DEPLOYMENT_TRACK"
# Version updates moved to separate job to avoid race conditions # Version updates moved to separate job to avoid race conditions
@@ -789,12 +905,17 @@ jobs:
with: with:
app_path: ${{ env.APP_PATH }} app_path: ${{ env.APP_PATH }}
- name: Commit updated build version - name: Open PR for Android build number bump
if: ${{ !env.ACT && success() }} if: ${{ !env.ACT && success() }}
uses: ./.github/actions/push-changes uses: peter-evans/create-pull-request@v6
with: with:
commit_message: "incrementing android build version for version ${{ env.VERSION }}" title: "chore: bump Android build for ${{ env.VERSION }}"
commit_paths: "./app/version.json" body: "Automated bump of Android build number by CI"
commit-message: "chore: incrementing android build version for version ${{ env.VERSION }} [github action]"
branch: ci/bump-android-build-${{ github.run_id }}
base: staging
add-paths: |
app/version.json
- name: Monitor cache usage - name: Monitor cache usage
if: always() if: always()
@@ -841,6 +962,7 @@ jobs:
with: with:
token: ${{ github.token }} token: ${{ github.token }}
fetch-depth: 0 fetch-depth: 0
ref: staging
- name: Read and sanitize Node.js version - name: Read and sanitize Node.js version
shell: bash shell: bash
run: | run: |
@@ -886,37 +1008,20 @@ jobs:
echo " Version already up to date or no version field in version.json" echo " Version already up to date or no version field in version.json"
fi fi
- name: Commit and push version files - name: Open PR to update version files
run: | uses: peter-evans/create-pull-request@v6
cd ${{ github.workspace }} with:
title: "chore: update version files after deployment"
# Configure git body: |
git config user.name "github-actions[bot]" Automated update of version files after successful deployment.
git config user.email "github-actions[bot]@users.noreply.github.com" Includes updates to `app/version.json`, `app/package.json`, and `yarn.lock`.
commit-message: "chore: update version files after deployment [skip ci]"
# Check if there are any changes to commit branch: ci/update-version-${{ github.run_id }}
if git diff --quiet app/version.json app/package.json yarn.lock 2>/dev/null; then base: staging
echo "No changes to version files, skipping commit" add-paths: |
else app/version.json
# Stage the changes app/package.json
git add app/version.json app/package.json yarn.lock 2>/dev/null || true yarn.lock
# Create commit message based on which platforms were deployed
COMMIT_MSG="chore: update version files after"
if [ "${{ needs.build-ios.result }}" = "success" ] && [ "${{ needs.build-android.result }}" = "success" ]; then
COMMIT_MSG="$COMMIT_MSG iOS and Android deployment"
elif [ "${{ needs.build-ios.result }}" = "success" ]; then
COMMIT_MSG="$COMMIT_MSG iOS deployment"
else
COMMIT_MSG="$COMMIT_MSG Android deployment"
fi
COMMIT_MSG="$COMMIT_MSG [skip ci]"
# Commit and push
git commit -m "$COMMIT_MSG"
git push
echo "✅ Committed version file changes"
fi
# Create git tags after successful deployment # Create git tags after successful deployment
create-release-tags: create-release-tags:
@@ -931,6 +1036,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: staging
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure Git - name: Configure Git

View File

@@ -17,12 +17,11 @@ env:
MAESTRO_VERSION: 1.41.0 MAESTRO_VERSION: 1.41.0
on: on:
push:
branches: [main, release/**]
paths:
- "app/**"
- ".github/workflows/mobile-e2e.yml"
pull_request: pull_request:
branches:
- dev
- staging
- main
paths: paths:
- "app/**" - "app/**"
- ".github/workflows/mobile-e2e.yml" - ".github/workflows/mobile-e2e.yml"

View File

@@ -10,16 +10,15 @@ env:
on: on:
pull_request: pull_request:
branches:
- dev
- staging
- main
paths: paths:
- "sdk/qrcode/**" - "sdk/qrcode/**"
- "common/**" - "common/**"
- ".github/workflows/qrcode-sdk-ci.yml" - ".github/workflows/qrcode-sdk-ci.yml"
- ".github/actions/**" - ".github/actions/**"
push:
branches: [main, develop]
paths:
- "sdk/qrcode/**"
- "common/**"
jobs: jobs:
# Build dependencies once and cache for other jobs # Build dependencies once and cache for other jobs

View File

@@ -1,7 +1,11 @@
name: Web CI name: Web CI
on: on:
push: pull_request:
branches:
- dev
- staging
- main
paths: paths:
- "app/**" - "app/**"
- ".github/workflows/web.yml" - ".github/workflows/web.yml"

View File

@@ -84,7 +84,9 @@ These guides provide comprehensive context for AI-assisted development with Chat
## Contributing ## Contributing
We are actively looking for contributors. Please check the [open issues](https://github.com/selfxyz/self/issues) if you don't know were to start! We offer bounties for significant contributions. We are actively looking for contributors. Please check the [open issues](https://github.com/selfxyz/self/issues) if you don't know where to start! We offer bounties for significant contributions.
> **Important:** Please open your pull request from the `staging` branch. Pull requests from other branches will be automatically closed.
## Contact us ## Contact us

View File

@@ -25,7 +25,7 @@ GEM
artifactory (3.0.17) artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.4.0) aws-eventstream (1.4.0)
aws-partitions (1.1154.0) aws-partitions (1.1155.0)
aws-sdk-core (3.232.0) aws-sdk-core (3.232.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
@@ -315,4 +315,4 @@ RUBY VERSION
ruby 3.2.7p253 ruby 3.2.7p253
BUNDLED WITH BUNDLED WITH
2.4.19 2.6.9

View File

@@ -121,7 +121,7 @@ android {
applicationId "com.proofofpassportapp" applicationId "com.proofofpassportapp"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 85 versionCode 90
versionName "2.6.4" versionName "2.6.4"
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp'] manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
externalNativeBuild { externalNativeBuild {

View File

@@ -0,0 +1,114 @@
package io.tradle.nfc
import net.sf.scuba.smartcards.APDUEvent
import net.sf.scuba.smartcards.APDUListener
import net.sf.scuba.smartcards.CommandAPDU
import net.sf.scuba.smartcards.ResponseAPDU
import org.jmrtd.WrappedAPDUEvent
import android.util.Log
class APDULogger : APDUListener {
private var moduleReference: RNPassportReaderModule? = null
private val sessionContext = mutableMapOf<String, Any>()
fun setModuleReference(module: RNPassportReaderModule) {
moduleReference = module
}
fun setContext(key: String, value: Any) {
sessionContext[key] = value
}
fun clearContext() {
sessionContext.clear()
}
override fun exchangedAPDU(event: APDUEvent) {
try {
val entry = createLogEntry(event)
logToAnalytics(entry)
} catch (e: Exception) {
Log.e("APDULogger", "Error exchanging APDU", e)
}
}
private fun createLogEntry(event: APDUEvent): APDULogEntry {
val command = event.commandAPDU
val response = event.responseAPDU
val timestamp = System.currentTimeMillis()
val entry = APDULogEntry(
timestamp = timestamp,
commandHex = command.bytes.toHexString(),
responseHex = response.bytes.toHexString(),
statusWord = response.sw,
statusWordHex = "0x${response.sw.toString(16).uppercase().padStart(4, '0')}",
commandLength = command.bytes.size,
responseLength = response.bytes.size,
dataLength = response.data.size,
isWrapped = event is WrappedAPDUEvent,
plainCommandHex = if (event is WrappedAPDUEvent) event.plainTextCommandAPDU.bytes.toHexString() else null,
plainResponseHex = if (event is WrappedAPDUEvent) event.plainTextResponseAPDU.bytes.toHexString() else null,
plainCommandLength = if (event is WrappedAPDUEvent) event.plainTextCommandAPDU.bytes.size else null,
plainResponseLength = if (event is WrappedAPDUEvent) event.plainTextResponseAPDU.bytes.size else null,
plainDataLength = if (event is WrappedAPDUEvent) event.plainTextResponseAPDU.data.size else null,
context = sessionContext.toMap()
)
return entry
}
private fun ByteArray.toHexString(): String {
return joinToString("") { "%02X".format(it) }
}
private fun logToAnalytics(entry: APDULogEntry) {
try {
val params = mutableMapOf<String, Any>().apply {
put("timestamp", entry.timestamp)
put("command_hex", entry.commandHex)
put("response_hex", entry.responseHex)
put("status_word", entry.statusWord)
put("status_word_hex", entry.statusWordHex)
put("command_length", entry.commandLength)
put("response_length", entry.responseLength)
put("data_length", entry.dataLength)
put("is_wrapped", entry.isWrapped)
put("context", entry.context)
entry.plainCommandHex?.let { put("plain_command_hex", it) }
entry.plainResponseHex?.let { put("plain_response_hex", it) }
entry.plainCommandLength?.let { put("plain_command_length", it) }
entry.plainResponseLength?.let { put("plain_response_length", it) }
entry.plainDataLength?.let { put("plain_data_length", it) }
}
moduleReference?.logAnalyticsEvent("nfc_apdu_exchange", params)
} catch (e: Exception) {
Log.e("APDULogger", "Error logging to analytics", e)
}
}
}
data class APDULogEntry(
val timestamp: Long,
val commandHex: String,
val responseHex: String,
val statusWord: Int,
val statusWordHex: String,
val commandLength: Int,
val responseLength: Int,
val dataLength: Int,
val isWrapped: Boolean,
val plainCommandHex: String?,
val plainResponseHex: String?,
val plainCommandLength: Int?,
val plainResponseLength: Int?,
val plainDataLength: Int?,
val context: Map<String, Any>
)

View File

@@ -157,7 +157,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
// private var encodePhotoToBase64 = false // private var encodePhotoToBase64 = false
private var scanPromise: Promise? = null private var scanPromise: Promise? = null
private var opts: ReadableMap? = null private var opts: ReadableMap? = null
private val apduLogger = APDULogger()
private var currentSessionId: String? = null
data class Data(val id: String, val digest: String, val signature: String, val publicKey: String) data class Data(val id: String, val digest: String, val signature: String, val publicKey: String)
data class PassportData( data class PassportData(
@@ -173,6 +175,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
init { init {
instance = this instance = this
reactContext.addLifecycleEventListener(this) reactContext.addLifecycleEventListener(this)
apduLogger.setModuleReference(this)
} }
override fun onCatalystInstanceDestroy() { override fun onCatalystInstanceDestroy() {
@@ -197,6 +200,10 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
@ReactMethod @ReactMethod
fun scan(opts: ReadableMap, promise: Promise) { fun scan(opts: ReadableMap, promise: Promise) {
currentSessionId = generateSessionId()
apduLogger.setContext("session_id", currentSessionId!!)
// Log scan start // Log scan start
logAnalyticsEvent("nfc_scan_started", mapOf( logAnalyticsEvent("nfc_scan_started", mapOf(
"use_can" to (opts.getBoolean(PARAM_USE_CAN) ?: false), "use_can" to (opts.getBoolean(PARAM_USE_CAN) ?: false),
@@ -228,7 +235,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
this.opts = opts this.opts = opts
this.scanPromise = promise this.scanPromise = promise
Log.d("RNPassportReaderModule", "opts set to: " + opts.toString()) // Log.d("RNPassportReaderModule", "opts set to: " + opts.toString())
} }
private fun resetState() { private fun resetState() {
@@ -293,7 +300,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private inner class ReadTask( private inner class ReadTask(
private val isoDep: IsoDep, private val isoDep: IsoDep,
private val authKey: AccessKeySpec private val authKey: AccessKeySpec
) : AsyncTask<Void?, Void?, Exception?>() { ) : AsyncTask<Void?, Void?, Exception?>() {
@@ -320,7 +327,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
Log.e("MY_LOGS", "Failed to get CardService instance", e) Log.e("MY_LOGS", "Failed to get CardService instance", e)
throw e throw e
} }
try { try {
cardService.open() cardService.open()
} catch (e: Exception) { } catch (e: Exception) {
@@ -341,10 +348,14 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
false, false,
) )
Log.e("MY_LOGS", "service gotten") Log.e("MY_LOGS", "service gotten")
service.addAPDUListener(apduLogger)
service.open() service.open()
Log.e("MY_LOGS", "service opened") Log.e("MY_LOGS", "service opened")
logAnalyticsEvent("nfc_passport_service_opened") logAnalyticsEvent("nfc_passport_service_opened")
var paceSucceeded = false var paceSucceeded = false
var bacSucceeded = false
try { try {
Log.e("MY_LOGS", "trying to get cardAccessFile...") Log.e("MY_LOGS", "trying to get cardAccessFile...")
val cardAccessFile = CardAccessFile(service.getInputStream(PassportService.EF_CARD_ACCESS)) val cardAccessFile = CardAccessFile(service.getInputStream(PassportService.EF_CARD_ACCESS))
@@ -355,16 +366,31 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
if (securityInfo is PACEInfo) { if (securityInfo is PACEInfo) {
Log.e("MY_LOGS", "trying PACE...") Log.e("MY_LOGS", "trying PACE...")
eventMessageEmitter(Messages.PACE_STARTED) eventMessageEmitter(Messages.PACE_STARTED)
service.doPACE( apduLogger.setContext("operation", "pace_authentication")
authKey, apduLogger.setContext("auth_key_type", authKey.javaClass.simpleName)
securityInfo.objectIdentifier,
PACEInfo.toParameterSpec(securityInfo.parameterId), // Determine proper PACE key: use CAN key if provided; otherwise derive PACE MRZ key from BAC
null, val paceKeyToUse: PACEKeySpec? = when (authKey) {
) is PACEKeySpec -> authKey
is BACKey -> PACEKeySpec.createMRZKey(authKey)
else -> null
}
if (paceKeyToUse != null) {
service.doPACE(
paceKeyToUse,
securityInfo.objectIdentifier,
PACEInfo.toParameterSpec(securityInfo.parameterId),
null,
)
} else {
throw IllegalStateException("Unsupported auth key for PACE: ${authKey::class.java.simpleName}")
}
Log.e("MY_LOGS", "PACE succeeded") Log.e("MY_LOGS", "PACE succeeded")
paceSucceeded = true paceSucceeded = true
logAnalyticsEvent("nfc_pace_succeeded") logAnalyticsEvent("nfc_pace_succeeded")
eventMessageEmitter(Messages.PACE_SUCCEEDED) eventMessageEmitter(Messages.PACE_SUCCEEDED)
// Stop iterating once PACE succeeds to avoid disrupting session with another attempt
break
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -376,35 +402,31 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
Log.w("MY_LOGS", e) Log.w("MY_LOGS", e)
eventMessageEmitter(Messages.PACE_FAILED) eventMessageEmitter(Messages.PACE_FAILED)
} }
Log.e("MY_LOGS", "Sending select applet command with paceSucceeded: ${paceSucceeded}") // this is false so PACE doesn't succeed // (Reverted) Do not select applet before authentication; proceed to BAC if needed
service.sendSelectApplet(paceSucceeded)
// Attempt BAC fallback if PACE failed
if (!paceSucceeded && authKey is BACKeySpec) { if (!paceSucceeded && authKey is BACKeySpec) {
var bacSucceeded = false
var attempts = 0 var attempts = 0
val maxAttempts = 3 val maxAttempts = 3
eventMessageEmitter(Messages.BAC_STARTED) eventMessageEmitter(Messages.BAC_STARTED)
apduLogger.setContext("operation", "bac_authentication")
apduLogger.setContext("auth_key_type", authKey.javaClass.simpleName)
while (!bacSucceeded && attempts < maxAttempts) { while (!bacSucceeded && attempts < maxAttempts) {
try { try {
attempts++ attempts++
Log.e("MY_LOGS", "BAC attempt $attempts of $maxAttempts") Log.e("MY_LOGS", "BAC attempt $attempts of $maxAttempts")
if (attempts > 1) Thread.sleep(500)
if (attempts > 1) { // Try to read EF_COM first; if it fails, do BAC
// Wait before retry
Thread.sleep(500)
}
// Try to read EF_COM first
try { try {
eventMessageEmitter(Messages.READING_COM) eventMessageEmitter(Messages.READING_COM)
service.getInputStream(PassportService.EF_COM).read() service.getInputStream(PassportService.EF_COM).read()
} catch (e: Exception) { } catch (e: Exception) {
// EF_COM failed, do BAC
service.doBAC(authKey) service.doBAC(authKey)
} }
bacSucceeded = true bacSucceeded = true
logAnalyticsEvent("nfc_bac_succeeded", mapOf("attempts" to attempts)) logAnalyticsEvent("nfc_bac_succeeded", mapOf("attempts" to attempts))
logAnalyticsEvent("nfc_bac_attempted", mapOf( logAnalyticsEvent("nfc_bac_attempted", mapOf(
@@ -414,23 +436,61 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
Log.e("MY_LOGS", "BAC succeeded on attempt $attempts") Log.e("MY_LOGS", "BAC succeeded on attempt $attempts")
eventMessageEmitter(Messages.BAC_SUCCEEDED) eventMessageEmitter(Messages.BAC_SUCCEEDED)
} catch (e: Exception) { } catch (e: Exception) {
val errClass = e.javaClass.simpleName
val errMsg = e.message ?: ""
logAnalyticsError("nfc_bac_attempt_failed", "BAC attempt $attempts failed: ${e.message}") logAnalyticsError("nfc_bac_attempt_failed", "BAC attempt $attempts failed: ${e.message}")
logAnalyticsEvent("nfc_bac_attempted", mapOf( logAnalyticsEvent("nfc_bac_attempted", mapOf(
"success" to false, "success" to false,
"attempt" to attempts, "attempt" to attempts,
"error_type" to e.javaClass.simpleName "error_type" to errClass
)) ))
Log.e("MY_LOGS", "BAC attempt $attempts failed: ${e.message}") Log.e("MY_LOGS", "BAC attempt $attempts failed: $errClass - $errMsg")
if (e is org.jmrtd.CardServiceProtocolException) {
// Provide additional structured diagnostics without sensitive data
logAnalyticsEvent("nfc_bac_protocol_error", mapOf(
"attempt" to attempts,
"message_contains_sw" to (errMsg.contains("SW = ")),
"message_length" to errMsg.length
))
}
if (attempts == maxAttempts) { if (attempts == maxAttempts) {
eventMessageEmitter(Messages.BAC_FAILED) eventMessageEmitter(Messages.BAC_FAILED)
throw e // Re-throw on final attempt throw e
} }
} }
} }
} }
// Ensure we have established authentication before reading
if (!paceSucceeded && !bacSucceeded) {
throw IOException("Authentication not established; cannot read data groups")
}
// Select applet after authentication established; handle 0x6982 gracefully
try {
Log.e("MY_LOGS", "Sending select applet command after auth. paceSucceeded=$paceSucceeded, bacSucceeded=$bacSucceeded")
service.sendSelectApplet(paceSucceeded)
logAnalyticsEvent("nfc_select_applet_succeeded", mapOf(
"pace_succeeded" to paceSucceeded,
"bac_succeeded" to bacSucceeded
))
} catch (e: Exception) {
val msg = e.message ?: ""
logAnalyticsError("nfc_select_applet_failed", "Select applet failed: ${e.message}")
if (msg.contains("6982") || msg.contains("SECURITY STATUS NOT SATISFIED", ignoreCase = true)) {
Log.w(TAG, "Select applet returned 6982; proceeding after established auth")
} else {
throw e
}
}
logAnalyticsEvent("nfc_reading_data_groups") logAnalyticsEvent("nfc_reading_data_groups")
apduLogger.setContext("operation", "reading_data_groups")
apduLogger.setContext("pace_succeeded", paceSucceeded)
apduLogger.setContext("bac_succeeded", bacSucceeded)
eventMessageEmitter(Messages.READING_DG1) eventMessageEmitter(Messages.READING_DG1)
logAnalyticsEvent("nfc_reading_dg1_started") logAnalyticsEvent("nfc_reading_dg1_started")
val dg1In = service.getInputStream(PassportService.EF_DG1) val dg1In = service.getInputStream(PassportService.EF_DG1)
@@ -509,6 +569,8 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
private fun doChipAuth(service: PassportService) { private fun doChipAuth(service: PassportService) {
try { try {
apduLogger.setContext("operation", "chip_authentication")
logAnalyticsEvent("nfc_reading_dg14_started") logAnalyticsEvent("nfc_reading_dg14_started")
eventMessageEmitter(Messages.READING_DG14) eventMessageEmitter(Messages.READING_DG14)
val dg14In = service.getInputStream(PassportService.EF_DG14) val dg14In = service.getInputStream(PassportService.EF_DG14)
@@ -538,6 +600,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
private fun doPassiveAuth() { private fun doPassiveAuth() {
try { try {
apduLogger.setContext("operation", "passive_authentication")
apduLogger.setContext("chip_auth_succeeded", chipAuthSucceeded)
logAnalyticsEvent("nfc_passive_auth_started") logAnalyticsEvent("nfc_passive_auth_started")
Log.d(TAG, "Starting passive authentication...") Log.d(TAG, "Starting passive authentication...")
val digest = MessageDigest.getInstance(sodFile.digestAlgorithm) val digest = MessageDigest.getInstance(sodFile.digestAlgorithm)
@@ -675,7 +740,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
scanPromise?.reject("E_SCAN_FAILED", result) scanPromise?.reject("E_SCAN_FAILED", result)
} }
resetState() apduLogger.clearContext()
resetState()
return return
} }
@@ -785,6 +852,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
eventMessageEmitter(Messages.COMPLETED) eventMessageEmitter(Messages.COMPLETED)
scanPromise?.resolve(passport) scanPromise?.resolve(passport)
eventMessageEmitter(Messages.RESET) eventMessageEmitter(Messages.RESET)
apduLogger.clearContext()
resetState() resetState()
} }
} }
@@ -811,7 +881,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
} }
} }
private fun logAnalyticsEvent(eventName: String, params: Map<String, Any> = emptyMap()) { fun logAnalyticsEvent(eventName: String, params: Map<String, Any> = emptyMap()) {
try { try {
val logData = JSONObject() val logData = JSONObject()
logData.put("level", "info") logData.put("level", "info")
@@ -863,8 +933,17 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
@ReactMethod @ReactMethod
fun reset() { fun reset() {
logAnalyticsEvent("nfc_scan_reset") logAnalyticsEvent("nfc_scan_reset")
apduLogger.clearContext()
resetState() resetState()
} }
/**
* Generate a unique session ID for tracking passport reading sessions
*/
private fun generateSessionId(): String {
return "nfc_${System.currentTimeMillis()}_${UUID.randomUUID().toString().take(8)}"
}
companion object { companion object {
private val TAG = RNPassportReaderModule::class.java.simpleName private val TAG = RNPassportReaderModule::class.java.simpleName

View File

@@ -119,6 +119,10 @@ platform :ios do
# VersionManager doesn't handle semantic versions, use npm # VersionManager doesn't handle semantic versions, use npm
sh("cd .. && npm version #{version_bump} --no-git-tag-version") sh("cd .. && npm version #{version_bump} --no-git-tag-version")
UI.success("✅ Bumped #{version_bump} version") UI.success("✅ Bumped #{version_bump} version")
# Sync the new version to iOS project files
sync_version
UI.success("✅ Synced MARKETING_VERSION to iOS project")
when "build" when "build"
# Build number is handled in prepare_ios_build # Build number is handled in prepare_ios_build
UI.message("📦 Build number will be incremented during build") UI.message("📦 Build number will be incremented during build")
@@ -297,6 +301,18 @@ platform :android do
upload_android_build(track: "production") upload_android_build(track: "production")
end end
desc "Build Android app without uploading"
lane :build_only do |options|
deployment_track = options[:deployment_track] || "internal"
version_bump = options[:version_bump] || "build"
UI.message("🔨 Building Android app (build only)")
UI.message(" Track: #{deployment_track}")
UI.message(" Version bump: #{version_bump}")
upload_android_build(options.merge(skip_upload: true))
end
desc "Deploy Android app with automatic version management" desc "Deploy Android app with automatic version management"
lane :deploy_auto do |options| lane :deploy_auto do |options|
deployment_track = options[:deployment_track] || "internal" deployment_track = options[:deployment_track] || "internal"
@@ -338,6 +354,7 @@ platform :android do
private_lane :upload_android_build do |options| private_lane :upload_android_build do |options|
test_mode = options[:test_mode] == true || options[:test_mode] == "true" test_mode = options[:test_mode] == true || options[:test_mode] == "true"
skip_upload = options[:skip_upload] == true || options[:skip_upload] == "true"
if local_development if local_development
if ENV["ANDROID_KEYSTORE_PATH"].nil? if ENV["ANDROID_KEYSTORE_PATH"].nil?
ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path) ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path)
@@ -355,8 +372,9 @@ platform :android do
"ANDROID_KEY_ALIAS", "ANDROID_KEY_ALIAS",
"ANDROID_KEY_PASSWORD", "ANDROID_KEY_PASSWORD",
"ANDROID_PACKAGE_NAME", "ANDROID_PACKAGE_NAME",
"ANDROID_PLAY_STORE_JSON_KEY_PATH",
] ]
# Only require JSON key path when not running in CI (local development)
required_env_vars << "ANDROID_PLAY_STORE_JSON_KEY_PATH" if local_development
Fastlane::Helpers.verify_env_vars(required_env_vars) Fastlane::Helpers.verify_env_vars(required_env_vars)
@@ -375,40 +393,69 @@ platform :android do
target_platform = options[:track] == "production" ? "Google Play" : "Internal Testing" target_platform = options[:track] == "production" ? "Google Play" : "Internal Testing"
should_upload = Fastlane::Helpers.should_upload_app(target_platform) should_upload = Fastlane::Helpers.should_upload_app(target_platform)
validate_play_store_json_key( # Validate JSON key only in local development; CI uses Workload Identity Federation (ADC)
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"], if local_development
) validate_play_store_json_key(
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
)
end
Fastlane::Helpers.with_retry(max_retries: 3, delay: 10) do Fastlane::Helpers.with_retry(max_retries: 3, delay: 10) do
gradle( gradle(
task: "clean bundleRelease --stacktrace --info", task: "clean bundleRelease --stacktrace --info",
project_dir: "android/", project_dir: "android/",
properties: { properties: {
"android.injected.signing.store.file" => ENV["ANDROID_KEYSTORE_PATH"], "MYAPP_UPLOAD_STORE_FILE" => ENV["ANDROID_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"], "MYAPP_UPLOAD_STORE_PASSWORD" => ENV["ANDROID_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["ANDROID_KEY_ALIAS"], "MYAPP_UPLOAD_KEY_ALIAS" => ENV["ANDROID_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["ANDROID_KEY_PASSWORD"], "MYAPP_UPLOAD_KEY_PASSWORD" => ENV["ANDROID_KEY_PASSWORD"] == "EMPTY" ? "" : ENV["ANDROID_KEY_PASSWORD"],
}, },
) )
end end
if test_mode if test_mode || skip_upload
UI.important("🧪 TEST MODE: Skipping Play Store upload") if skip_upload
UI.important("🔨 BUILD ONLY: Skipping Play Store upload")
else
UI.important("🧪 TEST MODE: Skipping Play Store upload")
end
UI.success("✅ Build completed successfully!") UI.success("✅ Build completed successfully!")
UI.message("📦 AAB path: #{android_aab_path}") UI.message("📦 AAB path: #{android_aab_path}")
else else
if should_upload if should_upload
begin begin
upload_to_play_store( upload_options = {
track: options[:track], track: options[:track],
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
package_name: ENV["ANDROID_PACKAGE_NAME"], package_name: ENV["ANDROID_PACKAGE_NAME"],
skip_upload_changelogs: true, skip_upload_changelogs: true,
skip_upload_images: true, skip_upload_images: true,
skip_upload_screenshots: true, skip_upload_screenshots: true,
track_promote_release_status: "completed", track_promote_release_status: "completed",
aab: android_aab_path, aab: android_aab_path,
) }
# In local development, use the JSON key file; in CI rely on ADC
if local_development
upload_options[:json_key] = ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"]
else
# In CI, try to use ADC credentials file directly
adc_creds_path = ENV["GOOGLE_APPLICATION_CREDENTIALS"]
if adc_creds_path && File.exist?(adc_creds_path)
UI.message("🔑 Using ADC credentials file: #{adc_creds_path}")
begin
# Try passing the credentials file content as json_key_data
creds_content = File.read(adc_creds_path)
upload_options[:json_key_data] = creds_content
rescue => e
UI.error("Failed to read ADC credentials: #{e.message}")
# Fallback: let supply try to use ADC automatically
UI.message("🔄 Falling back to automatic ADC detection")
end
else
UI.error("❌ ADC credentials not found at: #{adc_creds_path}")
end
end
upload_to_play_store(upload_options)
rescue => e rescue => e
if e.message.include?("forbidden") || e.message.include?("403") || e.message.include?("insufficientPermissions") if e.message.include?("forbidden") || e.message.include?("403") || e.message.include?("insufficientPermissions")
UI.error("❌ Play Store upload failed: Insufficient permissions") UI.error("❌ Play Store upload failed: Insufficient permissions")

View File

@@ -80,6 +80,14 @@ Push a new build to Google Play Internal Testing
Push a new build to Google Play Store Push a new build to Google Play Store
### android build_only
```sh
[bundle exec] fastlane android build_only
```
Build Android app without uploading
### android deploy_auto ### android deploy_auto
```sh ```sh

View File

@@ -423,7 +423,7 @@
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassportDebug.entitlements; CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassportDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 149; CURRENT_PROJECT_VERSION = 169;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 5B29R5LYHQ; DEVELOPMENT_TEAM = 5B29R5LYHQ;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -564,7 +564,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements; CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements;
CURRENT_PROJECT_VERSION = 149; CURRENT_PROJECT_VERSION = 169;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 5B29R5LYHQ; DEVELOPMENT_TEAM = 5B29R5LYHQ;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (

View File

@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
Upload Android AAB to Google Play Store using Workload Identity Federation
This script bypasses Fastlane and uses the Google Play Developer API directly
"""
import os
import sys
import json
import argparse
from pathlib import Path
try:
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from google.auth import default
except ImportError:
print("❌ Error: Required packages not installed.")
print("Run: pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client")
sys.exit(1)
def get_credentials():
"""Get credentials using ADC (Workload Identity Federation)"""
print("🔑 Authenticating using Application Default Credentials...")
try:
# Use the default() function which properly handles WIF
# This should work now that the audience is configured correctly
print("🔄 Using Google's default credential chain...")
credentials, project = default(scopes=['https://www.googleapis.com/auth/androidpublisher'])
print(f"✅ Authentication successful! Project: {project}")
print(f"🔍 Credential type: {type(credentials).__name__}")
# Ensure credentials are ready for use
if hasattr(credentials, 'refresh') and hasattr(credentials, 'valid') and not credentials.valid:
print("🔄 Refreshing credentials...")
import google.auth.transport.requests
request = google.auth.transport.requests.Request()
credentials.refresh(request)
print("✅ Credentials refreshed successfully")
return credentials
except Exception as e:
print(f"❌ Authentication failed: {e}")
print(f"❌ Error type: {type(e).__name__}")
# Debug information
creds_file = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS')
if creds_file:
print(f"🔍 Credentials file: {creds_file}")
if os.path.exists(creds_file):
try:
with open(creds_file, 'r') as f:
creds_info = json.load(f)
print(f"🔍 Credential type in file: {creds_info.get('type', 'unknown')}")
if 'audience' in creds_info:
print(f"🔍 Credential audience: {creds_info['audience']}")
except:
print("🔍 Could not read credentials file content")
else:
print("🔍 Credentials file does not exist")
else:
print("🔍 GOOGLE_APPLICATION_CREDENTIALS not set")
sys.exit(1)
def should_hold_for_manual_review(track):
"""
Determine if changes should be held for manual review based on track type.
Returns True only for production releases or when you need manual control.
For internal, alpha, beta tracks, changes are automatically sent for review.
"""
# Only hold for manual review on production track
# For other tracks (internal, alpha, beta), let changes go for automatic review
return track == 'production'
def upload_to_play_store(aab_path, package_name, track, credentials):
"""Upload AAB to Google Play Store"""
print(f"📤 Uploading {aab_path} to Play Store...")
try:
# Build the service
service = build('androidpublisher', 'v3', credentials=credentials)
# Create an edit
print("🚀 Creating edit transaction...")
edit_request = service.edits().insert(body={}, packageName=package_name)
edit = edit_request.execute()
edit_id = edit['id']
print(f"✅ Edit created: {edit_id}")
# Upload the AAB
print("📦 Uploading AAB file...")
media = MediaFileUpload(aab_path, mimetype='application/octet-stream')
upload_request = service.edits().bundles().upload(
packageName=package_name,
editId=edit_id,
media_body=media
)
bundle_response = upload_request.execute()
version_code = bundle_response['versionCode']
print(f"✅ AAB uploaded. Version code: {version_code}")
# Assign to track
print(f"🎯 Assigning to track: {track}")
track_request = service.edits().tracks().update(
packageName=package_name,
editId=edit_id,
track=track,
body={
'track': track,
'releases': [{
'versionCodes': [str(version_code)],
'status': 'completed'
}]
}
)
track_response = track_request.execute()
print(f"✅ Assigned to track: {track_response['track']}")
# Commit the edit
print("💾 Committing changes...")
# Determine if we should hold changes for manual review
hold_for_manual_review = should_hold_for_manual_review(track)
if hold_for_manual_review:
# For production or when manual review is needed
commit_request = service.edits().commit(
packageName=package_name,
editId=edit_id,
changesNotSentForReview=True
)
commit_response = commit_request.execute()
print(f"✅ Upload completed successfully! Edit ID: {commit_response['id']}")
print(f"📝 Note: Changes committed but held for manual review (production track)")
else:
# For internal, alpha, beta tracks - let changes go for automatic review
commit_request = service.edits().commit(
packageName=package_name,
editId=edit_id
)
commit_response = commit_request.execute()
print(f"✅ Upload completed successfully! Edit ID: {commit_response['id']}")
print(f"📝 Note: Changes committed and sent for automatic review ({track} track)")
return True
except Exception as e:
print(f"❌ Upload failed: {e}")
return False
def main():
parser = argparse.ArgumentParser(description='Upload Android AAB to Google Play Store using WIF')
parser.add_argument('--aab', required=True, help='Path to the AAB file')
parser.add_argument('--package-name', required=True, help='Android package name')
parser.add_argument('--track', default='internal', help='Release track (internal, alpha, beta, production)')
args = parser.parse_args()
# Validate AAB file exists
aab_path = Path(args.aab)
if not aab_path.exists():
print(f"❌ Error: AAB file not found: {aab_path}")
sys.exit(1)
print("🚀 Starting Google Play Store upload with Workload Identity Federation")
print(f"📦 AAB: {aab_path}")
print(f"📱 Package: {args.package_name}")
print(f"🎯 Track: {args.track}")
print()
# Get credentials and upload
credentials = get_credentials()
success = upload_to_play_store(str(aab_path), args.package_name, args.track, credentials)
if success:
print("\n🎉 Upload completed successfully!")
sys.exit(0)
else:
print("\n💥 Upload failed!")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -13,6 +13,9 @@ const DevFeatureFlagsScreen = lazy(
const DevHapticFeedbackScreen = lazy( const DevHapticFeedbackScreen = lazy(
() => import('@/screens/dev/DevHapticFeedbackScreen'), () => import('@/screens/dev/DevHapticFeedbackScreen'),
); );
const DevPrivateKeyScreen = lazy(
() => import('@/screens/dev/DevPrivateKeyScreen'),
);
const DevSettingsScreen = lazy(() => import('@/screens/dev/DevSettingsScreen')); const DevSettingsScreen = lazy(() => import('@/screens/dev/DevSettingsScreen'));
const CreateMockScreen = lazy(() => import('@/screens/dev/CreateMockScreen')); const CreateMockScreen = lazy(() => import('@/screens/dev/CreateMockScreen'));
const CreateMockScreenDeepLink = lazy( const CreateMockScreenDeepLink = lazy(
@@ -71,6 +74,13 @@ const devScreens = {
}, },
} as NativeStackNavigationOptions, } as NativeStackNavigationOptions,
}, },
DevPrivateKey: {
screen: DevPrivateKeyScreen,
options: {
...devHeaderOptions,
title: 'Private Key',
} as NativeStackNavigationOptions,
},
}; };
export default devScreens; export default devScreens;

View File

@@ -16,7 +16,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { DefaultNavBar } from '@/components/NavBar'; import { DefaultNavBar } from '@/components/NavBar';
import AppLayout from '@/layouts/AppLayout'; import AppLayout from '@/layouts/AppLayout';
import { getAesopScreens } from '@/navigation/aesop'; import { getAesopScreens } from '@/navigation/aesop';
import devScreens from '@/navigation/dev'; import devScreens from '@/navigation/devTools';
import documentScreens from '@/navigation/document'; import documentScreens from '@/navigation/document';
import homeScreens from '@/navigation/home'; import homeScreens from '@/navigation/home';
import proveScreens from '@/navigation/prove'; import proveScreens from '@/navigation/prove';

View File

@@ -0,0 +1,110 @@
// 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 { useCallback, useEffect, useState } from 'react';
import { Button, Text, XStack, YStack } from 'tamagui';
import Clipboard from '@react-native-clipboard/clipboard';
import { unsafe_getPrivateKey } from '@/providers/authProvider';
import { black, slate50, slate200, teal500, white } from '@/utils/colors';
import { confirmTap } from '@/utils/haptic';
const DevPrivateKeyScreen: React.FC = () => {
const [privateKey, setPrivateKey] = useState<string | null>(
'Loading private key…',
);
const [isPrivateKeyRevealed, setIsPrivateKeyRevealed] = useState(false);
const [copied, setCopied] = useState(false);
useEffect(() => {
unsafe_getPrivateKey().then(key =>
setPrivateKey(key || 'No private key found'),
);
}, []);
const handleRevealPrivateKey = useCallback(() => {
confirmTap();
if (!isPrivateKeyRevealed) {
setIsPrivateKeyRevealed(true);
}
if (isPrivateKeyRevealed) {
Clipboard.setString(privateKey || '');
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
}, [isPrivateKeyRevealed, privateKey]);
const getRedactedPrivateKey = useCallback(() => {
if (
!privateKey ||
privateKey === 'Loading private key…' ||
privateKey === 'No private key found'
) {
return privateKey;
}
// If it starts with 0x, show 0x followed by asterisks for the rest
if (privateKey.startsWith('0x')) {
const restLength = privateKey.length - 2;
return '0x' + '*'.repeat(restLength);
}
// Otherwise, show asterisks for the entire length
return '*'.repeat(privateKey.length);
}, [privateKey]);
return (
<YStack padding="$4">
<YStack position="relative" alignItems="stretch" gap={0}>
<XStack
borderColor={slate200}
backgroundColor={slate50}
borderWidth="$1"
borderBottomWidth={0}
borderTopLeftRadius="$5"
borderTopRightRadius="$5"
gap={12}
paddingHorizontal={26}
paddingVertical={28}
flexWrap="wrap"
>
<Text>
{isPrivateKeyRevealed ? privateKey : getRedactedPrivateKey()}
</Text>
</XStack>
<XStack
borderTopColor={slate200}
borderTopWidth="$1"
justifyContent="center"
alignItems="stretch"
>
<Button
unstyled
color={isPrivateKeyRevealed ? (copied ? black : white) : black}
borderColor={
isPrivateKeyRevealed ? (copied ? teal500 : black) : slate200
}
backgroundColor={
isPrivateKeyRevealed ? (copied ? teal500 : black) : slate50
}
borderWidth="$1"
borderTopWidth={0}
borderBottomLeftRadius="$5"
borderBottomRightRadius="$5"
paddingVertical="$2"
onPress={handleRevealPrivateKey}
width="100%"
textAlign="center"
>
{isPrivateKeyRevealed
? `${copied ? 'COPIED' : 'COPY'} TO CLIPBOARD`
: 'TAP TO REVEAL'}
</Button>
</XStack>
</YStack>
</YStack>
);
};
export default DevPrivateKeyScreen;

View File

@@ -126,6 +126,7 @@ const items = [
'DevSettings', 'DevSettings',
'DevFeatureFlags', 'DevFeatureFlags',
'DevHapticFeedback', 'DevHapticFeedback',
'DevPrivateKey',
'Splash', 'Splash',
'Launch', 'Launch',
'DocumentOnboarding', 'DocumentOnboarding',
@@ -339,7 +340,32 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
title="Debug Shortcuts" title="Debug Shortcuts"
description="Jump directly to any screen for testing" description="Jump directly to any screen for testing"
> >
<ScreenSelector /> <YStack gap="$2">
<Button
style={{ backgroundColor: 'white' }}
borderColor={slate200}
borderRadius="$2"
height="$5"
padding={0}
onPress={() => {
navigation.navigate('DevPrivateKey');
}}
>
<XStack
width="100%"
justifyContent="space-between"
paddingVertical="$3"
paddingLeft="$4"
paddingRight="$1.5"
>
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
View Private Key
</Text>
<ChevronRight color={slate500} strokeWidth={2.5} />
</XStack>
</Button>
<ScreenSelector />
</YStack>
</ParameterSection> </ParameterSection>
<ParameterSection <ParameterSection

View File

@@ -204,6 +204,18 @@ const DocumentNFCScanScreen: React.FC = () => {
// Add timestamp when scan starts // Add timestamp when scan starts
scanCancelledRef.current = false; scanCancelledRef.current = false;
const scanStartTime = Date.now(); const scanStartTime = Date.now();
if (scanTimeoutRef.current) {
clearTimeout(scanTimeoutRef.current);
scanTimeoutRef.current = null;
}
scanTimeoutRef.current = setTimeout(() => {
scanCancelledRef.current = true;
trackEvent(PassportEvents.NFC_SCAN_FAILED, {
error: 'timeout',
});
openErrorModal('Scan timed out. Please try again.');
setIsNfcSheetOpen(false);
}, 30000);
// Mark NFC scanning as active to prevent analytics flush interference // Mark NFC scanning as active to prevent analytics flush interference
setNfcScanningActive(true); setNfcScanningActive(true);

View File

@@ -316,11 +316,9 @@ const ManageDocumentsScreen: React.FC = () => {
<PrimaryButton onPress={handleScanDocument}> <PrimaryButton onPress={handleScanDocument}>
Scan New ID Document Scan New ID Document
</PrimaryButton> </PrimaryButton>
{__DEV__ && ( <SecondaryButton onPress={handleGenerateMock}>
<SecondaryButton onPress={handleGenerateMock}> Generate Mock Document
Generate Mock Document </SecondaryButton>
</SecondaryButton>
)}
</ButtonsContainer> </ButtonsContainer>
</YStack> </YStack>
</YStack> </YStack>

View File

@@ -24,6 +24,11 @@ import {
} from '@/providers/passportDataProvider'; } from '@/providers/passportDataProvider';
import { useSettingStore } from '@/stores/settingStore'; import { useSettingStore } from '@/stores/settingStore';
import { black } from '@/utils/colors'; import { black } from '@/utils/colors';
import {
getAndClearQueuedUrl,
handleUrl,
setDeeplinkParentScreen,
} from '@/utils/deeplinks';
import { impactLight } from '@/utils/haptic'; import { impactLight } from '@/utils/haptic';
const SplashScreen: React.FC = ({}) => { const SplashScreen: React.FC = ({}) => {
@@ -36,6 +41,7 @@ const SplashScreen: React.FC = ({}) => {
const [nextScreen, setNextScreen] = useState<keyof RootStackParamList | null>( const [nextScreen, setNextScreen] = useState<keyof RootStackParamList | null>(
null, null,
); );
const [queuedDeepLink, setQueuedDeepLink] = useState<string | null>(null);
const dataLoadInitiatedRef = useRef(false); const dataLoadInitiatedRef = useRef(false);
useEffect(() => { useEffect(() => {
@@ -66,9 +72,22 @@ const SplashScreen: React.FC = ({}) => {
} }
const hasValid = await hasAnyValidRegisteredDocument(selfClient); const hasValid = await hasAnyValidRegisteredDocument(selfClient);
setNextScreen(hasValid ? 'Home' : 'Launch'); const parentScreen = hasValid ? 'Home' : 'Launch';
setDeeplinkParentScreen(parentScreen);
const queuedUrl = getAndClearQueuedUrl();
if (queuedUrl) {
if (typeof __DEV__ !== 'undefined' && __DEV__) {
console.log('Processing queued deeplink:', queuedUrl);
}
setQueuedDeepLink(queuedUrl);
} else {
setNextScreen(parentScreen);
}
} catch (error) { } catch (error) {
console.error(`Error in SplashScreen data loading: ${error}`); console.error(`Error in SplashScreen data loading: ${error}`);
setDeeplinkParentScreen('Launch');
setNextScreen('Launch'); setNextScreen('Launch');
} }
}; };
@@ -83,12 +102,18 @@ const SplashScreen: React.FC = ({}) => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (isAnimationFinished && nextScreen) { if (isAnimationFinished) {
requestAnimationFrame(() => { if (queuedDeepLink) {
navigation.navigate(nextScreen as never); requestAnimationFrame(() => {
}); handleUrl(queuedDeepLink);
});
} else if (nextScreen) {
requestAnimationFrame(() => {
navigation.navigate(nextScreen as never);
});
}
} }
}, [isAnimationFinished, nextScreen, navigation]); }, [isAnimationFinished, nextScreen, queuedDeepLink, navigation]);
return ( return (
<LottieView <LottieView

View File

@@ -72,6 +72,29 @@ const validateAndSanitizeParam = (
return decodedValue; return decodedValue;
}; };
/**
* Creates a proper navigation stack for deeplink navigation
* @param targetScreen - The target screen to navigate to
* @param parentScreen - The parent screen that should appear when user goes back (default: 'Home')
*/
const createDeeplinkNavigationState = (
targetScreen: string,
parentScreen: string = 'Home',
) => ({
index: 1, // Current screen index (targetScreen)
routes: [{ name: parentScreen }, { name: targetScreen }],
});
// Store the correct parent screen determined by splash screen
let correctParentScreen: string = 'Home';
// Function for splash screen to get and clear the queued initial URL
export const getAndClearQueuedUrl = (): string | null => {
const url = queuedInitialUrl;
queuedInitialUrl = null;
return url;
};
export const handleUrl = (uri: string) => { export const handleUrl = (uri: string) => {
const validatedParams = parseAndValidateUrlParams(uri); const validatedParams = parseAndValidateUrlParams(uri);
const { sessionId, selfApp: selfAppStr, mock_passport } = validatedParams; const { sessionId, selfApp: selfAppStr, mock_passport } = validatedParams;
@@ -81,19 +104,29 @@ export const handleUrl = (uri: string) => {
const selfAppJson = JSON.parse(selfAppStr); const selfAppJson = JSON.parse(selfAppStr);
useSelfAppStore.getState().setSelfApp(selfAppJson); useSelfAppStore.getState().setSelfApp(selfAppJson);
useSelfAppStore.getState().startAppListener(selfAppJson.sessionId); useSelfAppStore.getState().startAppListener(selfAppJson.sessionId);
navigationRef.navigate('Prove');
// Reset navigation stack with correct parent -> ProveScreen
navigationRef.reset(
createDeeplinkNavigationState('ProveScreen', correctParentScreen),
);
return; return;
} catch (error) { } catch (error) {
if (typeof __DEV__ !== 'undefined' && __DEV__) { if (typeof __DEV__ !== 'undefined' && __DEV__) {
console.error('Error parsing selfApp:', error); console.error('Error parsing selfApp:', error);
} }
navigationRef.navigate('QRCodeTrouble'); navigationRef.reset(
createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen),
);
} }
} else if (sessionId && typeof sessionId === 'string') { } else if (sessionId && typeof sessionId === 'string') {
useSelfAppStore.getState().cleanSelfApp(); useSelfAppStore.getState().cleanSelfApp();
useSelfAppStore.getState().startAppListener(sessionId); useSelfAppStore.getState().startAppListener(sessionId);
navigationRef.navigate('Prove');
// Reset navigation stack with correct parent -> ProveScreen
navigationRef.reset(
createDeeplinkNavigationState('ProveScreen', correctParentScreen),
);
} else if (mock_passport) { } else if (mock_passport) {
try { try {
const data = JSON.parse(mock_passport); const data = JSON.parse(mock_passport);
@@ -120,12 +153,17 @@ export const handleUrl = (uri: string) => {
gender: rawParams.gender, gender: rawParams.gender,
}); });
navigationRef.navigate('MockDataDeepLink'); // Reset navigation stack with correct parent -> MockDataDeepLink
navigationRef.reset(
createDeeplinkNavigationState('MockDataDeepLink', correctParentScreen),
);
} catch (error) { } catch (error) {
if (typeof __DEV__ !== 'undefined' && __DEV__) { if (typeof __DEV__ !== 'undefined' && __DEV__) {
console.error('Error parsing mock_passport data or navigating:', error); console.error('Error parsing mock_passport data or navigating:', error);
} }
navigationRef.navigate('QRCodeTrouble'); navigationRef.reset(
createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen),
);
} }
} else if (Platform.OS === 'web') { } else if (Platform.OS === 'web') {
// TODO: web handle links if we need to idk if we do // TODO: web handle links if we need to idk if we do
@@ -134,7 +172,9 @@ export const handleUrl = (uri: string) => {
if (typeof __DEV__ !== 'undefined' && __DEV__) { if (typeof __DEV__ !== 'undefined' && __DEV__) {
console.error('No sessionId or selfApp found in the data'); console.error('No sessionId or selfApp found in the data');
} }
navigationRef.navigate('QRCodeTrouble'); navigationRef.reset(
createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen),
);
} }
}; };
@@ -166,19 +206,29 @@ export const parseAndValidateUrlParams = (uri: string): ValidatedParams => {
return validatedParams; return validatedParams;
}; };
export const setupUniversalLinkListenerInNavigation = () => { // Store the initial URL for splash screen to handle after initialization
const handleNavigation = (url: string) => { let queuedInitialUrl: string | null = null;
handleUrl(url);
};
/**
* Sets the correct parent screen for deeplink navigation
* This should be called by splash screen after determining the correct screen
*/
export const setDeeplinkParentScreen = (screen: string) => {
correctParentScreen = screen;
};
export const setupUniversalLinkListenerInNavigation = () => {
// Get the initial URL and store it for splash screen handling
Linking.getInitialURL().then(url => { Linking.getInitialURL().then(url => {
if (url) { if (url) {
handleNavigation(url); // Store the initial URL instead of handling it immediately
queuedInitialUrl = url;
} }
}); });
// Handle subsequent URL events normally (when app is already running)
const linkingEventListener = Linking.addEventListener('url', ({ url }) => { const linkingEventListener = Linking.addEventListener('url', ({ url }) => {
handleNavigation(url); handleUrl(url);
}); });
return () => { return () => {

View File

@@ -16,7 +16,7 @@ describe('navigation', () => {
'DeferredLinkingInfo', 'DeferredLinkingInfo',
'DevFeatureFlags', 'DevFeatureFlags',
'DevHapticFeedback', 'DevHapticFeedback',
'DevPrivateKey',
'DevSettings', 'DevSettings',
'Disclaimer', 'Disclaimer',
'DocumentCamera', 'DocumentCamera',

View File

@@ -5,7 +5,11 @@
import { Linking } from 'react-native'; import { Linking } from 'react-native';
jest.mock('@/navigation', () => ({ jest.mock('@/navigation', () => ({
navigationRef: { navigate: jest.fn(), isReady: jest.fn(() => true) }, navigationRef: {
navigate: jest.fn(),
isReady: jest.fn(() => true),
reset: jest.fn(),
},
})); }));
const mockSelfAppStore = { useSelfAppStore: { getState: jest.fn() } }; const mockSelfAppStore = { useSelfAppStore: { getState: jest.fn() } };
@@ -60,7 +64,10 @@ describe('deeplinks', () => {
expect(setSelfApp).toHaveBeenCalledWith(selfApp); expect(setSelfApp).toHaveBeenCalledWith(selfApp);
expect(startAppListener).toHaveBeenCalledWith('abc'); expect(startAppListener).toHaveBeenCalledWith('abc');
const { navigationRef } = require('@/navigation'); const { navigationRef } = require('@/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('Prove'); expect(navigationRef.reset).toHaveBeenCalledWith({
index: 1,
routes: [{ name: 'Home' }, { name: 'ProveScreen' }],
});
}); });
it('handles sessionId parameter', () => { it('handles sessionId parameter', () => {
@@ -70,7 +77,10 @@ describe('deeplinks', () => {
expect(cleanSelfApp).toHaveBeenCalled(); expect(cleanSelfApp).toHaveBeenCalled();
expect(startAppListener).toHaveBeenCalledWith('123'); expect(startAppListener).toHaveBeenCalledWith('123');
const { navigationRef } = require('@/navigation'); const { navigationRef } = require('@/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('Prove'); expect(navigationRef.reset).toHaveBeenCalledWith({
index: 1,
routes: [{ name: 'Home' }, { name: 'ProveScreen' }],
});
}); });
it('handles mock_passport parameter', () => { it('handles mock_passport parameter', () => {
@@ -86,7 +96,10 @@ describe('deeplinks', () => {
gender: undefined, gender: undefined,
}); });
const { navigationRef } = require('@/navigation'); const { navigationRef } = require('@/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('MockDataDeepLink'); expect(navigationRef.reset).toHaveBeenCalledWith({
index: 1,
routes: [{ name: 'Home' }, { name: 'MockDataDeepLink' }],
});
}); });
it('navigates to QRCodeTrouble for invalid data', () => { it('navigates to QRCodeTrouble for invalid data', () => {
@@ -98,7 +111,10 @@ describe('deeplinks', () => {
handleUrl(url); handleUrl(url);
const { navigationRef } = require('@/navigation'); const { navigationRef } = require('@/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('QRCodeTrouble'); expect(navigationRef.reset).toHaveBeenCalledWith({
index: 1,
routes: [{ name: 'Home' }, { name: 'QRCodeTrouble' }],
});
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error parsing selfApp:', 'Error parsing selfApp:',
expect.any(Error), expect.any(Error),
@@ -119,7 +135,10 @@ describe('deeplinks', () => {
handleUrl(url); handleUrl(url);
const { navigationRef } = require('@/navigation'); const { navigationRef } = require('@/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('QRCodeTrouble'); expect(navigationRef.reset).toHaveBeenCalledWith({
index: 1,
routes: [{ name: 'Home' }, { name: 'QRCodeTrouble' }],
});
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
'No sessionId or selfApp found in the data', 'No sessionId or selfApp found in the data',
); );
@@ -137,7 +156,10 @@ describe('deeplinks', () => {
handleUrl(url); handleUrl(url);
const { navigationRef } = require('@/navigation'); const { navigationRef } = require('@/navigation');
expect(navigationRef.navigate).toHaveBeenCalledWith('QRCodeTrouble'); expect(navigationRef.reset).toHaveBeenCalledWith({
index: 1,
routes: [{ name: 'Home' }, { name: 'QRCodeTrouble' }],
});
consoleErrorSpy.mockRestore(); consoleErrorSpy.mockRestore();
}); });

View File

@@ -1,10 +1,10 @@
{ {
"ios": { "ios": {
"build": 163, "build": 169,
"lastDeployed": "2025-08-08T22:35:10Z" "lastDeployed": "2025-08-26T16:35:10Z"
}, },
"android": { "android": {
"build": 85, "build": 96,
"lastDeployed": "2025-08-08T15:13:41Z" "lastDeployed": "2025-08-29T10:59:07Z"
} }
} }

View File

@@ -173,7 +173,7 @@ export default defineConfig({
// Other screens // Other screens
'screens-settings': ['./src/navigation/settings.ts'], 'screens-settings': ['./src/navigation/settings.ts'],
'screens-recovery': ['./src/navigation/recovery.ts'], 'screens-recovery': ['./src/navigation/recovery.ts'],
'screens-dev': ['./src/navigation/dev.ts'], 'screens-dev': ['./src/navigation/devTools.ts'],
'screens-aesop': ['./src/navigation/aesop.ts'], 'screens-aesop': ['./src/navigation/aesop.ts'],
}, },
}, },

View File

@@ -62,6 +62,26 @@ describe('extractMRZInfo', () => {
expect(info.validation?.overall).toBe(false); expect(info.validation?.overall).toBe(false);
}); });
it('parses valid TD1 MRZ', () => {
const info = extractMRZInfo(sampleTD1);
expect(info.documentNumber).toBe('X4RTBPFW4');
expect(info.issuingCountry).toBe('FRA');
expect(info.dateOfBirth).toBe('900713');
expect(info.dateOfExpiry).toBe('300211');
expect(info.validation?.overall).toBe(true);
});
it('rejects invalid TD1 MRZ', () => {
const invalid = `FRAX4RTBPFW46`;
expect(() => extractMRZInfo(invalid)).toThrow();
});
it('Fails overall validation for invalid TD1 MRZ', () => {
const invalid = `IDFRAX4RTBPFW46`;
const info = extractMRZInfo(invalid);
expect(info.validation?.overall).toBe(false);
});
it('rejects malformed MRZ', () => { it('rejects malformed MRZ', () => {
const invalid = 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<'; const invalid = 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<';
expect(() => extractMRZInfo(invalid)).toThrowError(MrzParseError); expect(() => extractMRZInfo(invalid)).toThrowError(MrzParseError);