mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 06:38:09 -05:00
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:
@@ -15,7 +15,7 @@ reviews:
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
base_branches: ["main", "dev"]
|
||||
base_branches: ["main", "dev", "staging"]
|
||||
tools:
|
||||
github-checks:
|
||||
timeout_ms: 300000
|
||||
|
||||
8
.github/workflows/circuits-build.yml
vendored
8
.github/workflows/circuits-build.yml
vendored
@@ -1,13 +1,9 @@
|
||||
name: Circuits Build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "circuits/circuits/**"
|
||||
- ".github/workflows/artifacts.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "circuits/circuits/**"
|
||||
|
||||
11
.github/workflows/circuits.yml
vendored
11
.github/workflows/circuits.yml
vendored
@@ -1,21 +1,12 @@
|
||||
name: Circuits CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
- openpassportv2
|
||||
paths:
|
||||
- "circuits/**"
|
||||
- "common/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
- openpassportv2
|
||||
paths:
|
||||
- "circuits/**"
|
||||
- "common/**"
|
||||
jobs:
|
||||
run_circuit_tests:
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
8
.github/workflows/contracts.yml
vendored
8
.github/workflows/contracts.yml
vendored
@@ -1,15 +1,9 @@
|
||||
name: Contracts CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
paths:
|
||||
- "contracts/**"
|
||||
- "common/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "contracts/**"
|
||||
|
||||
8
.github/workflows/mobile-ci.yml
vendored
8
.github/workflows/mobile-ci.yml
vendored
@@ -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
|
||||
CI: true
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "common/**"
|
||||
- "app/**"
|
||||
@@ -98,7 +102,7 @@ jobs:
|
||||
working-directory: ./
|
||||
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-latest-large
|
||||
needs: build-deps
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
328
.github/workflows/mobile-deploy.yml
vendored
328
.github/workflows/mobile-deploy.yml
vendored
@@ -30,6 +30,7 @@ env:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -106,6 +107,9 @@ jobs:
|
||||
echo "📦 Version bump: ${{ inputs.version_bump }}"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: staging
|
||||
- name: Read and sanitize Node.js version
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -120,6 +124,24 @@ jobs:
|
||||
echo "NODE_VERSION=$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
|
||||
if: inputs.platform != 'android'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
@@ -148,8 +170,9 @@ jobs:
|
||||
.yarn/cache
|
||||
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: |
|
||||
${{ 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 }}-
|
||||
|
||||
@@ -160,6 +183,7 @@ jobs:
|
||||
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') }}
|
||||
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 }}-
|
||||
|
||||
@@ -171,7 +195,7 @@ jobs:
|
||||
${{ env.APP_PATH }}/ios/Pods
|
||||
~/Library/Caches/CocoaPods
|
||||
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
|
||||
run: |
|
||||
@@ -415,6 +439,14 @@ jobs:
|
||||
|
||||
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 ...`
|
||||
- name: Build and upload to App Store Connect/TestFlight
|
||||
if: inputs.platform != 'android' && !env.ACT
|
||||
@@ -524,12 +556,17 @@ jobs:
|
||||
with:
|
||||
app_path: ${{ env.APP_PATH }}
|
||||
|
||||
- name: Commit updated build number
|
||||
- name: Open PR for iOS build number bump
|
||||
if: ${{ !env.ACT && success() }}
|
||||
uses: ./.github/actions/push-changes
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
commit_message: "incrementing ios build number for version ${{ env.VERSION }}"
|
||||
commit_paths: "./app/version.json"
|
||||
title: "chore: bump iOS build for ${{ env.VERSION }}"
|
||||
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
|
||||
if: always()
|
||||
@@ -568,6 +605,71 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
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
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -582,6 +684,24 @@ jobs:
|
||||
echo "NODE_VERSION=$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
|
||||
id: yarn-cache
|
||||
uses: ./.github/actions/cache-yarn
|
||||
@@ -590,8 +710,9 @@ jobs:
|
||||
.yarn/cache
|
||||
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: |
|
||||
${{ 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 }}-
|
||||
|
||||
@@ -602,6 +723,7 @@ jobs:
|
||||
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') }}
|
||||
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 }}-
|
||||
|
||||
@@ -609,14 +731,14 @@ jobs:
|
||||
id: gradle-cache
|
||||
uses: ./.github/actions/cache-gradle
|
||||
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
|
||||
id: ndk-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
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
|
||||
run: |
|
||||
@@ -656,12 +778,6 @@ jobs:
|
||||
workspace: ${{ env.WORKSPACE }}
|
||||
|
||||
# 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
|
||||
if: inputs.platform != 'ios'
|
||||
@@ -669,16 +785,18 @@ jobs:
|
||||
with:
|
||||
accept-android-sdk-licenses: true
|
||||
|
||||
- name: Install NDK
|
||||
- name: Install NDK and CMake
|
||||
if: inputs.platform != 'ios' && steps.ndk-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
max_attempts=5
|
||||
attempt=1
|
||||
|
||||
# Install NDK
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
echo "Attempt $attempt of $max_attempts to install NDK..."
|
||||
if sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}"; then
|
||||
echo "Successfully installed NDK"
|
||||
exit 0
|
||||
break
|
||||
fi
|
||||
echo "Failed to install NDK on attempt $attempt"
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
@@ -692,54 +810,47 @@ jobs:
|
||||
attempt=$((attempt + 1))
|
||||
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
|
||||
if: inputs.platform != 'ios' && env.ACT
|
||||
if: inputs.platform != 'ios' # Apply to CI builds (not just ACT)
|
||||
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'
|
||||
run: |
|
||||
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}
|
||||
echo "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_PLAY_STORE_JSON_KEY_PATH }}
|
||||
python -m pip install --upgrade pip
|
||||
pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client
|
||||
|
||||
# run secrets check after keytool has been setup
|
||||
- name: Verify Android Secrets
|
||||
- name: Build Dependencies (Android)
|
||||
if: inputs.platform != 'ios'
|
||||
run: |
|
||||
# Verify Play Store JSON key base64 secret exists and is valid
|
||||
if [ -z "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" ]; then
|
||||
echo "❌ Error: Play Store JSON key base64 secret cannot be empty"
|
||||
exit 1
|
||||
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!"
|
||||
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"
|
||||
|
||||
- name: Build and upload to Google Play Internal Testing
|
||||
- name: Build AAB with Fastlane
|
||||
if: inputs.platform != 'ios'
|
||||
env:
|
||||
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
|
||||
@@ -748,11 +859,7 @@ jobs:
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
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=8192"
|
||||
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
|
||||
NODE_OPTIONS: "--max-old-space-size=6144"
|
||||
run: |
|
||||
cd ${{ env.APP_PATH }}
|
||||
|
||||
@@ -761,25 +868,34 @@ jobs:
|
||||
VERSION_BUMP="${{ inputs.version_bump || 'build' }}"
|
||||
TEST_MODE="${{ inputs.test_mode || false }}"
|
||||
|
||||
echo "🤖 Deployment Configuration:"
|
||||
echo "🤖 Build Configuration:"
|
||||
echo " - Track: $DEPLOYMENT_TRACK"
|
||||
echo " - Version Bump: $VERSION_BUMP"
|
||||
echo " - Test Mode: $TEST_MODE"
|
||||
|
||||
if [ "$TEST_MODE" = "true" ]; then
|
||||
echo "🧪 Running in TEST MODE - will skip upload to Play Store"
|
||||
bundle exec fastlane android deploy_auto \
|
||||
deployment_track:$DEPLOYMENT_TRACK \
|
||||
version_bump:$VERSION_BUMP \
|
||||
test_mode:true \
|
||||
--verbose
|
||||
else
|
||||
echo "🚀 Deploying to Google Play Store..."
|
||||
bundle exec fastlane android deploy_auto \
|
||||
deployment_track:$DEPLOYMENT_TRACK \
|
||||
version_bump:$VERSION_BUMP \
|
||||
--verbose
|
||||
fi
|
||||
echo "🔨 Building AAB with Fastlane..."
|
||||
bundle exec fastlane android build_only \
|
||||
deployment_track:$DEPLOYMENT_TRACK \
|
||||
version_bump:$VERSION_BUMP \
|
||||
--verbose
|
||||
|
||||
- name: Upload to Google Play Store using WIF
|
||||
if: inputs.platform != 'ios' && inputs.test_mode != true
|
||||
env:
|
||||
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
|
||||
run: |
|
||||
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
|
||||
|
||||
@@ -789,12 +905,17 @@ jobs:
|
||||
with:
|
||||
app_path: ${{ env.APP_PATH }}
|
||||
|
||||
- name: Commit updated build version
|
||||
- name: Open PR for Android build number bump
|
||||
if: ${{ !env.ACT && success() }}
|
||||
uses: ./.github/actions/push-changes
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
commit_message: "incrementing android build version for version ${{ env.VERSION }}"
|
||||
commit_paths: "./app/version.json"
|
||||
title: "chore: bump Android build for ${{ env.VERSION }}"
|
||||
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
|
||||
if: always()
|
||||
@@ -841,6 +962,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
fetch-depth: 0
|
||||
ref: staging
|
||||
- name: Read and sanitize Node.js version
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -886,37 +1008,20 @@ jobs:
|
||||
echo "ℹ️ Version already up to date or no version field in version.json"
|
||||
fi
|
||||
|
||||
- name: Commit and push version files
|
||||
run: |
|
||||
cd ${{ github.workspace }}
|
||||
|
||||
# Configure git
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Check if there are any changes to commit
|
||||
if git diff --quiet app/version.json app/package.json yarn.lock 2>/dev/null; then
|
||||
echo "No changes to version files, skipping commit"
|
||||
else
|
||||
# Stage the changes
|
||||
git add app/version.json app/package.json yarn.lock 2>/dev/null || true
|
||||
|
||||
# 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
|
||||
- name: Open PR to update version files
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
title: "chore: update version files after deployment"
|
||||
body: |
|
||||
Automated update of version files after successful deployment.
|
||||
Includes updates to `app/version.json`, `app/package.json`, and `yarn.lock`.
|
||||
commit-message: "chore: update version files after deployment [skip ci]"
|
||||
branch: ci/update-version-${{ github.run_id }}
|
||||
base: staging
|
||||
add-paths: |
|
||||
app/version.json
|
||||
app/package.json
|
||||
yarn.lock
|
||||
|
||||
# Create git tags after successful deployment
|
||||
create-release-tags:
|
||||
@@ -931,6 +1036,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: staging
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure Git
|
||||
|
||||
9
.github/workflows/mobile-e2e.yml
vendored
9
.github/workflows/mobile-e2e.yml
vendored
@@ -17,12 +17,11 @@ env:
|
||||
MAESTRO_VERSION: 1.41.0
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, release/**]
|
||||
paths:
|
||||
- "app/**"
|
||||
- ".github/workflows/mobile-e2e.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "app/**"
|
||||
- ".github/workflows/mobile-e2e.yml"
|
||||
|
||||
9
.github/workflows/qrcode-sdk-ci.yml
vendored
9
.github/workflows/qrcode-sdk-ci.yml
vendored
@@ -10,16 +10,15 @@ env:
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "sdk/qrcode/**"
|
||||
- "common/**"
|
||||
- ".github/workflows/qrcode-sdk-ci.yml"
|
||||
- ".github/actions/**"
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- "sdk/qrcode/**"
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
# Build dependencies once and cache for other jobs
|
||||
|
||||
6
.github/workflows/web.yml
vendored
6
.github/workflows/web.yml
vendored
@@ -1,7 +1,11 @@
|
||||
name: Web CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "app/**"
|
||||
- ".github/workflows/web.yml"
|
||||
|
||||
@@ -84,7 +84,9 @@ These guides provide comprehensive context for AI-assisted development with Chat
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1154.0)
|
||||
aws-partitions (1.1155.0)
|
||||
aws-sdk-core (3.232.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
@@ -315,4 +315,4 @@ RUBY VERSION
|
||||
ruby 3.2.7p253
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.19
|
||||
2.6.9
|
||||
|
||||
@@ -121,7 +121,7 @@ android {
|
||||
applicationId "com.proofofpassportapp"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 85
|
||||
versionCode 90
|
||||
versionName "2.6.4"
|
||||
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
|
||||
externalNativeBuild {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -157,7 +157,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
// private var encodePhotoToBase64 = false
|
||||
private var scanPromise: Promise? = 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 PassportData(
|
||||
@@ -173,6 +175,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
init {
|
||||
instance = this
|
||||
reactContext.addLifecycleEventListener(this)
|
||||
apduLogger.setModuleReference(this)
|
||||
}
|
||||
|
||||
override fun onCatalystInstanceDestroy() {
|
||||
@@ -197,6 +200,10 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
|
||||
@ReactMethod
|
||||
fun scan(opts: ReadableMap, promise: Promise) {
|
||||
currentSessionId = generateSessionId()
|
||||
|
||||
apduLogger.setContext("session_id", currentSessionId!!)
|
||||
|
||||
// Log scan start
|
||||
logAnalyticsEvent("nfc_scan_started", mapOf(
|
||||
"use_can" to (opts.getBoolean(PARAM_USE_CAN) ?: false),
|
||||
@@ -228,7 +235,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
|
||||
this.opts = opts
|
||||
this.scanPromise = promise
|
||||
Log.d("RNPassportReaderModule", "opts set to: " + opts.toString())
|
||||
// Log.d("RNPassportReaderModule", "opts set to: " + opts.toString())
|
||||
}
|
||||
|
||||
private fun resetState() {
|
||||
@@ -293,7 +300,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private inner class ReadTask(
|
||||
private val isoDep: IsoDep,
|
||||
private val isoDep: IsoDep,
|
||||
private val authKey: AccessKeySpec
|
||||
) : AsyncTask<Void?, Void?, Exception?>() {
|
||||
|
||||
@@ -320,7 +327,7 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
Log.e("MY_LOGS", "Failed to get CardService instance", e)
|
||||
throw e
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
cardService.open()
|
||||
} catch (e: Exception) {
|
||||
@@ -341,10 +348,14 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
false,
|
||||
)
|
||||
Log.e("MY_LOGS", "service gotten")
|
||||
|
||||
service.addAPDUListener(apduLogger)
|
||||
|
||||
service.open()
|
||||
Log.e("MY_LOGS", "service opened")
|
||||
logAnalyticsEvent("nfc_passport_service_opened")
|
||||
var paceSucceeded = false
|
||||
var bacSucceeded = false
|
||||
try {
|
||||
Log.e("MY_LOGS", "trying to get cardAccessFile...")
|
||||
val cardAccessFile = CardAccessFile(service.getInputStream(PassportService.EF_CARD_ACCESS))
|
||||
@@ -355,16 +366,31 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
if (securityInfo is PACEInfo) {
|
||||
Log.e("MY_LOGS", "trying PACE...")
|
||||
eventMessageEmitter(Messages.PACE_STARTED)
|
||||
service.doPACE(
|
||||
authKey,
|
||||
securityInfo.objectIdentifier,
|
||||
PACEInfo.toParameterSpec(securityInfo.parameterId),
|
||||
null,
|
||||
)
|
||||
apduLogger.setContext("operation", "pace_authentication")
|
||||
apduLogger.setContext("auth_key_type", authKey.javaClass.simpleName)
|
||||
|
||||
// Determine proper PACE key: use CAN key if provided; otherwise derive PACE MRZ key from BAC
|
||||
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")
|
||||
paceSucceeded = true
|
||||
logAnalyticsEvent("nfc_pace_succeeded")
|
||||
eventMessageEmitter(Messages.PACE_SUCCEEDED)
|
||||
// Stop iterating once PACE succeeds to avoid disrupting session with another attempt
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -376,35 +402,31 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
Log.w("MY_LOGS", e)
|
||||
eventMessageEmitter(Messages.PACE_FAILED)
|
||||
}
|
||||
Log.e("MY_LOGS", "Sending select applet command with paceSucceeded: ${paceSucceeded}") // this is false so PACE doesn't succeed
|
||||
service.sendSelectApplet(paceSucceeded)
|
||||
// (Reverted) Do not select applet before authentication; proceed to BAC if needed
|
||||
|
||||
// Attempt BAC fallback if PACE failed
|
||||
if (!paceSucceeded && authKey is BACKeySpec) {
|
||||
var bacSucceeded = false
|
||||
var attempts = 0
|
||||
val maxAttempts = 3
|
||||
|
||||
eventMessageEmitter(Messages.BAC_STARTED)
|
||||
|
||||
apduLogger.setContext("operation", "bac_authentication")
|
||||
apduLogger.setContext("auth_key_type", authKey.javaClass.simpleName)
|
||||
|
||||
while (!bacSucceeded && attempts < maxAttempts) {
|
||||
try {
|
||||
attempts++
|
||||
Log.e("MY_LOGS", "BAC attempt $attempts of $maxAttempts")
|
||||
|
||||
if (attempts > 1) {
|
||||
// Wait before retry
|
||||
Thread.sleep(500)
|
||||
}
|
||||
|
||||
// Try to read EF_COM first
|
||||
if (attempts > 1) Thread.sleep(500)
|
||||
// Try to read EF_COM first; if it fails, do BAC
|
||||
try {
|
||||
eventMessageEmitter(Messages.READING_COM)
|
||||
service.getInputStream(PassportService.EF_COM).read()
|
||||
} catch (e: Exception) {
|
||||
// EF_COM failed, do BAC
|
||||
service.doBAC(authKey)
|
||||
}
|
||||
|
||||
|
||||
bacSucceeded = true
|
||||
logAnalyticsEvent("nfc_bac_succeeded", mapOf("attempts" to attempts))
|
||||
logAnalyticsEvent("nfc_bac_attempted", mapOf(
|
||||
@@ -414,23 +436,61 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
Log.e("MY_LOGS", "BAC succeeded on attempt $attempts")
|
||||
eventMessageEmitter(Messages.BAC_SUCCEEDED)
|
||||
} catch (e: Exception) {
|
||||
val errClass = e.javaClass.simpleName
|
||||
val errMsg = e.message ?: ""
|
||||
logAnalyticsError("nfc_bac_attempt_failed", "BAC attempt $attempts failed: ${e.message}")
|
||||
logAnalyticsEvent("nfc_bac_attempted", mapOf(
|
||||
"success" to false,
|
||||
"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) {
|
||||
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")
|
||||
|
||||
apduLogger.setContext("operation", "reading_data_groups")
|
||||
apduLogger.setContext("pace_succeeded", paceSucceeded)
|
||||
apduLogger.setContext("bac_succeeded", bacSucceeded)
|
||||
|
||||
eventMessageEmitter(Messages.READING_DG1)
|
||||
logAnalyticsEvent("nfc_reading_dg1_started")
|
||||
val dg1In = service.getInputStream(PassportService.EF_DG1)
|
||||
@@ -509,6 +569,8 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
|
||||
private fun doChipAuth(service: PassportService) {
|
||||
try {
|
||||
apduLogger.setContext("operation", "chip_authentication")
|
||||
|
||||
logAnalyticsEvent("nfc_reading_dg14_started")
|
||||
eventMessageEmitter(Messages.READING_DG14)
|
||||
val dg14In = service.getInputStream(PassportService.EF_DG14)
|
||||
@@ -538,6 +600,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
|
||||
private fun doPassiveAuth() {
|
||||
try {
|
||||
apduLogger.setContext("operation", "passive_authentication")
|
||||
apduLogger.setContext("chip_auth_succeeded", chipAuthSucceeded)
|
||||
|
||||
logAnalyticsEvent("nfc_passive_auth_started")
|
||||
Log.d(TAG, "Starting passive authentication...")
|
||||
val digest = MessageDigest.getInstance(sodFile.digestAlgorithm)
|
||||
@@ -675,7 +740,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
scanPromise?.reject("E_SCAN_FAILED", result)
|
||||
}
|
||||
|
||||
resetState()
|
||||
apduLogger.clearContext()
|
||||
|
||||
resetState()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -785,6 +852,9 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
eventMessageEmitter(Messages.COMPLETED)
|
||||
scanPromise?.resolve(passport)
|
||||
eventMessageEmitter(Messages.RESET)
|
||||
|
||||
apduLogger.clearContext()
|
||||
|
||||
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 {
|
||||
val logData = JSONObject()
|
||||
logData.put("level", "info")
|
||||
@@ -863,8 +933,17 @@ class RNPassportReaderModule(private val reactContext: ReactApplicationContext)
|
||||
@ReactMethod
|
||||
fun reset() {
|
||||
logAnalyticsEvent("nfc_scan_reset")
|
||||
apduLogger.clearContext()
|
||||
|
||||
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 {
|
||||
private val TAG = RNPassportReaderModule::class.java.simpleName
|
||||
|
||||
@@ -119,6 +119,10 @@ platform :ios do
|
||||
# VersionManager doesn't handle semantic versions, use npm
|
||||
sh("cd .. && npm version #{version_bump} --no-git-tag-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"
|
||||
# Build number is handled in prepare_ios_build
|
||||
UI.message("📦 Build number will be incremented during build")
|
||||
@@ -297,6 +301,18 @@ platform :android do
|
||||
upload_android_build(track: "production")
|
||||
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"
|
||||
lane :deploy_auto do |options|
|
||||
deployment_track = options[:deployment_track] || "internal"
|
||||
@@ -338,6 +354,7 @@ platform :android do
|
||||
|
||||
private_lane :upload_android_build do |options|
|
||||
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
|
||||
skip_upload = options[:skip_upload] == true || options[:skip_upload] == "true"
|
||||
if local_development
|
||||
if ENV["ANDROID_KEYSTORE_PATH"].nil?
|
||||
ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path)
|
||||
@@ -355,8 +372,9 @@ platform :android do
|
||||
"ANDROID_KEY_ALIAS",
|
||||
"ANDROID_KEY_PASSWORD",
|
||||
"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)
|
||||
|
||||
@@ -375,40 +393,69 @@ platform :android do
|
||||
target_platform = options[:track] == "production" ? "Google Play" : "Internal Testing"
|
||||
should_upload = Fastlane::Helpers.should_upload_app(target_platform)
|
||||
|
||||
validate_play_store_json_key(
|
||||
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
|
||||
)
|
||||
# Validate JSON key only in local development; CI uses Workload Identity Federation (ADC)
|
||||
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
|
||||
gradle(
|
||||
task: "clean bundleRelease --stacktrace --info",
|
||||
project_dir: "android/",
|
||||
properties: {
|
||||
"android.injected.signing.store.file" => ENV["ANDROID_KEYSTORE_PATH"],
|
||||
"android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"],
|
||||
"android.injected.signing.key.alias" => ENV["ANDROID_KEY_ALIAS"],
|
||||
"android.injected.signing.key.password" => ENV["ANDROID_KEY_PASSWORD"],
|
||||
"MYAPP_UPLOAD_STORE_FILE" => ENV["ANDROID_KEYSTORE_PATH"],
|
||||
"MYAPP_UPLOAD_STORE_PASSWORD" => ENV["ANDROID_KEYSTORE_PASSWORD"],
|
||||
"MYAPP_UPLOAD_KEY_ALIAS" => ENV["ANDROID_KEY_ALIAS"],
|
||||
"MYAPP_UPLOAD_KEY_PASSWORD" => ENV["ANDROID_KEY_PASSWORD"] == "EMPTY" ? "" : ENV["ANDROID_KEY_PASSWORD"],
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
if test_mode
|
||||
UI.important("🧪 TEST MODE: Skipping Play Store upload")
|
||||
if test_mode || skip_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.message("📦 AAB path: #{android_aab_path}")
|
||||
else
|
||||
if should_upload
|
||||
begin
|
||||
upload_to_play_store(
|
||||
upload_options = {
|
||||
track: options[:track],
|
||||
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
|
||||
package_name: ENV["ANDROID_PACKAGE_NAME"],
|
||||
skip_upload_changelogs: true,
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true,
|
||||
track_promote_release_status: "completed",
|
||||
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
|
||||
if e.message.include?("forbidden") || e.message.include?("403") || e.message.include?("insufficientPermissions")
|
||||
UI.error("❌ Play Store upload failed: Insufficient permissions")
|
||||
|
||||
@@ -80,6 +80,14 @@ Push a new build to Google Play Internal Testing
|
||||
|
||||
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
|
||||
|
||||
```sh
|
||||
|
||||
@@ -423,7 +423,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassportDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 169;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 5B29R5LYHQ;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -564,7 +564,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 169;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 5B29R5LYHQ;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
|
||||
192
app/scripts/upload_to_play_store.py
Normal file
192
app/scripts/upload_to_play_store.py
Normal 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()
|
||||
@@ -13,6 +13,9 @@ const DevFeatureFlagsScreen = lazy(
|
||||
const DevHapticFeedbackScreen = lazy(
|
||||
() => import('@/screens/dev/DevHapticFeedbackScreen'),
|
||||
);
|
||||
const DevPrivateKeyScreen = lazy(
|
||||
() => import('@/screens/dev/DevPrivateKeyScreen'),
|
||||
);
|
||||
const DevSettingsScreen = lazy(() => import('@/screens/dev/DevSettingsScreen'));
|
||||
const CreateMockScreen = lazy(() => import('@/screens/dev/CreateMockScreen'));
|
||||
const CreateMockScreenDeepLink = lazy(
|
||||
@@ -71,6 +74,13 @@ const devScreens = {
|
||||
},
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
DevPrivateKey: {
|
||||
screen: DevPrivateKeyScreen,
|
||||
options: {
|
||||
...devHeaderOptions,
|
||||
title: 'Private Key',
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
};
|
||||
|
||||
export default devScreens;
|
||||
@@ -16,7 +16,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { DefaultNavBar } from '@/components/NavBar';
|
||||
import AppLayout from '@/layouts/AppLayout';
|
||||
import { getAesopScreens } from '@/navigation/aesop';
|
||||
import devScreens from '@/navigation/dev';
|
||||
import devScreens from '@/navigation/devTools';
|
||||
import documentScreens from '@/navigation/document';
|
||||
import homeScreens from '@/navigation/home';
|
||||
import proveScreens from '@/navigation/prove';
|
||||
|
||||
110
app/src/screens/dev/DevPrivateKeyScreen.tsx
Normal file
110
app/src/screens/dev/DevPrivateKeyScreen.tsx
Normal 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;
|
||||
@@ -126,6 +126,7 @@ const items = [
|
||||
'DevSettings',
|
||||
'DevFeatureFlags',
|
||||
'DevHapticFeedback',
|
||||
'DevPrivateKey',
|
||||
'Splash',
|
||||
'Launch',
|
||||
'DocumentOnboarding',
|
||||
@@ -339,7 +340,32 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
title="Debug Shortcuts"
|
||||
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
|
||||
|
||||
@@ -204,6 +204,18 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
// Add timestamp when scan starts
|
||||
scanCancelledRef.current = false;
|
||||
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
|
||||
setNfcScanningActive(true);
|
||||
|
||||
@@ -316,11 +316,9 @@ const ManageDocumentsScreen: React.FC = () => {
|
||||
<PrimaryButton onPress={handleScanDocument}>
|
||||
Scan New ID Document
|
||||
</PrimaryButton>
|
||||
{__DEV__ && (
|
||||
<SecondaryButton onPress={handleGenerateMock}>
|
||||
Generate Mock Document
|
||||
</SecondaryButton>
|
||||
)}
|
||||
<SecondaryButton onPress={handleGenerateMock}>
|
||||
Generate Mock Document
|
||||
</SecondaryButton>
|
||||
</ButtonsContainer>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
@@ -24,6 +24,11 @@ import {
|
||||
} from '@/providers/passportDataProvider';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { black } from '@/utils/colors';
|
||||
import {
|
||||
getAndClearQueuedUrl,
|
||||
handleUrl,
|
||||
setDeeplinkParentScreen,
|
||||
} from '@/utils/deeplinks';
|
||||
import { impactLight } from '@/utils/haptic';
|
||||
|
||||
const SplashScreen: React.FC = ({}) => {
|
||||
@@ -36,6 +41,7 @@ const SplashScreen: React.FC = ({}) => {
|
||||
const [nextScreen, setNextScreen] = useState<keyof RootStackParamList | null>(
|
||||
null,
|
||||
);
|
||||
const [queuedDeepLink, setQueuedDeepLink] = useState<string | null>(null);
|
||||
const dataLoadInitiatedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,9 +72,22 @@ const SplashScreen: React.FC = ({}) => {
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error(`Error in SplashScreen data loading: ${error}`);
|
||||
setDeeplinkParentScreen('Launch');
|
||||
setNextScreen('Launch');
|
||||
}
|
||||
};
|
||||
@@ -83,12 +102,18 @@ const SplashScreen: React.FC = ({}) => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAnimationFinished && nextScreen) {
|
||||
requestAnimationFrame(() => {
|
||||
navigation.navigate(nextScreen as never);
|
||||
});
|
||||
if (isAnimationFinished) {
|
||||
if (queuedDeepLink) {
|
||||
requestAnimationFrame(() => {
|
||||
handleUrl(queuedDeepLink);
|
||||
});
|
||||
} else if (nextScreen) {
|
||||
requestAnimationFrame(() => {
|
||||
navigation.navigate(nextScreen as never);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isAnimationFinished, nextScreen, navigation]);
|
||||
}, [isAnimationFinished, nextScreen, queuedDeepLink, navigation]);
|
||||
|
||||
return (
|
||||
<LottieView
|
||||
|
||||
@@ -72,6 +72,29 @@ const validateAndSanitizeParam = (
|
||||
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) => {
|
||||
const validatedParams = parseAndValidateUrlParams(uri);
|
||||
const { sessionId, selfApp: selfAppStr, mock_passport } = validatedParams;
|
||||
@@ -81,19 +104,29 @@ export const handleUrl = (uri: string) => {
|
||||
const selfAppJson = JSON.parse(selfAppStr);
|
||||
useSelfAppStore.getState().setSelfApp(selfAppJson);
|
||||
useSelfAppStore.getState().startAppListener(selfAppJson.sessionId);
|
||||
navigationRef.navigate('Prove');
|
||||
|
||||
// Reset navigation stack with correct parent -> ProveScreen
|
||||
navigationRef.reset(
|
||||
createDeeplinkNavigationState('ProveScreen', correctParentScreen),
|
||||
);
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
||||
console.error('Error parsing selfApp:', error);
|
||||
}
|
||||
navigationRef.navigate('QRCodeTrouble');
|
||||
navigationRef.reset(
|
||||
createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen),
|
||||
);
|
||||
}
|
||||
} else if (sessionId && typeof sessionId === 'string') {
|
||||
useSelfAppStore.getState().cleanSelfApp();
|
||||
useSelfAppStore.getState().startAppListener(sessionId);
|
||||
navigationRef.navigate('Prove');
|
||||
|
||||
// Reset navigation stack with correct parent -> ProveScreen
|
||||
navigationRef.reset(
|
||||
createDeeplinkNavigationState('ProveScreen', correctParentScreen),
|
||||
);
|
||||
} else if (mock_passport) {
|
||||
try {
|
||||
const data = JSON.parse(mock_passport);
|
||||
@@ -120,12 +153,17 @@ export const handleUrl = (uri: string) => {
|
||||
gender: rawParams.gender,
|
||||
});
|
||||
|
||||
navigationRef.navigate('MockDataDeepLink');
|
||||
// Reset navigation stack with correct parent -> MockDataDeepLink
|
||||
navigationRef.reset(
|
||||
createDeeplinkNavigationState('MockDataDeepLink', correctParentScreen),
|
||||
);
|
||||
} catch (error) {
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
||||
console.error('Error parsing mock_passport data or navigating:', error);
|
||||
}
|
||||
navigationRef.navigate('QRCodeTrouble');
|
||||
navigationRef.reset(
|
||||
createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen),
|
||||
);
|
||||
}
|
||||
} else if (Platform.OS === 'web') {
|
||||
// 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__) {
|
||||
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;
|
||||
};
|
||||
|
||||
export const setupUniversalLinkListenerInNavigation = () => {
|
||||
const handleNavigation = (url: string) => {
|
||||
handleUrl(url);
|
||||
};
|
||||
// Store the initial URL for splash screen to handle after initialization
|
||||
let queuedInitialUrl: string | null = null;
|
||||
|
||||
/**
|
||||
* 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 => {
|
||||
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 }) => {
|
||||
handleNavigation(url);
|
||||
handleUrl(url);
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('navigation', () => {
|
||||
'DeferredLinkingInfo',
|
||||
'DevFeatureFlags',
|
||||
'DevHapticFeedback',
|
||||
|
||||
'DevPrivateKey',
|
||||
'DevSettings',
|
||||
'Disclaimer',
|
||||
'DocumentCamera',
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
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() } };
|
||||
@@ -60,7 +64,10 @@ describe('deeplinks', () => {
|
||||
expect(setSelfApp).toHaveBeenCalledWith(selfApp);
|
||||
expect(startAppListener).toHaveBeenCalledWith('abc');
|
||||
const { navigationRef } = require('@/navigation');
|
||||
expect(navigationRef.navigate).toHaveBeenCalledWith('Prove');
|
||||
expect(navigationRef.reset).toHaveBeenCalledWith({
|
||||
index: 1,
|
||||
routes: [{ name: 'Home' }, { name: 'ProveScreen' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles sessionId parameter', () => {
|
||||
@@ -70,7 +77,10 @@ describe('deeplinks', () => {
|
||||
expect(cleanSelfApp).toHaveBeenCalled();
|
||||
expect(startAppListener).toHaveBeenCalledWith('123');
|
||||
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', () => {
|
||||
@@ -86,7 +96,10 @@ describe('deeplinks', () => {
|
||||
gender: undefined,
|
||||
});
|
||||
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', () => {
|
||||
@@ -98,7 +111,10 @@ describe('deeplinks', () => {
|
||||
handleUrl(url);
|
||||
|
||||
const { navigationRef } = require('@/navigation');
|
||||
expect(navigationRef.navigate).toHaveBeenCalledWith('QRCodeTrouble');
|
||||
expect(navigationRef.reset).toHaveBeenCalledWith({
|
||||
index: 1,
|
||||
routes: [{ name: 'Home' }, { name: 'QRCodeTrouble' }],
|
||||
});
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error parsing selfApp:',
|
||||
expect.any(Error),
|
||||
@@ -119,7 +135,10 @@ describe('deeplinks', () => {
|
||||
handleUrl(url);
|
||||
|
||||
const { navigationRef } = require('@/navigation');
|
||||
expect(navigationRef.navigate).toHaveBeenCalledWith('QRCodeTrouble');
|
||||
expect(navigationRef.reset).toHaveBeenCalledWith({
|
||||
index: 1,
|
||||
routes: [{ name: 'Home' }, { name: 'QRCodeTrouble' }],
|
||||
});
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'No sessionId or selfApp found in the data',
|
||||
);
|
||||
@@ -137,7 +156,10 @@ describe('deeplinks', () => {
|
||||
handleUrl(url);
|
||||
|
||||
const { navigationRef } = require('@/navigation');
|
||||
expect(navigationRef.navigate).toHaveBeenCalledWith('QRCodeTrouble');
|
||||
expect(navigationRef.reset).toHaveBeenCalledWith({
|
||||
index: 1,
|
||||
routes: [{ name: 'Home' }, { name: 'QRCodeTrouble' }],
|
||||
});
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"ios": {
|
||||
"build": 163,
|
||||
"lastDeployed": "2025-08-08T22:35:10Z"
|
||||
"build": 169,
|
||||
"lastDeployed": "2025-08-26T16:35:10Z"
|
||||
},
|
||||
"android": {
|
||||
"build": 85,
|
||||
"lastDeployed": "2025-08-08T15:13:41Z"
|
||||
"build": 96,
|
||||
"lastDeployed": "2025-08-29T10:59:07Z"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ export default defineConfig({
|
||||
// Other screens
|
||||
'screens-settings': ['./src/navigation/settings.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'],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -62,6 +62,26 @@ describe('extractMRZInfo', () => {
|
||||
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', () => {
|
||||
const invalid = 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<';
|
||||
expect(() => extractMRZInfo(invalid)).toThrowError(MrzParseError);
|
||||
|
||||
Reference in New Issue
Block a user