mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 22:58:20 -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:
|
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
|
||||||
|
|||||||
8
.github/workflows/circuits-build.yml
vendored
8
.github/workflows/circuits-build.yml
vendored
@@ -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/**"
|
||||||
|
|||||||
11
.github/workflows/circuits.yml
vendored
11
.github/workflows/circuits.yml
vendored
@@ -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
|
||||||
|
|||||||
8
.github/workflows/contracts.yml
vendored
8
.github/workflows/contracts.yml
vendored
@@ -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/**"
|
||||||
|
|||||||
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
|
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
|
||||||
|
|||||||
328
.github/workflows/mobile-deploy.yml
vendored
328
.github/workflows/mobile-deploy.yml
vendored
@@ -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
|
||||||
|
|||||||
9
.github/workflows/mobile-e2e.yml
vendored
9
.github/workflows/mobile-e2e.yml
vendored
@@ -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"
|
||||||
|
|||||||
9
.github/workflows/qrcode-sdk-ci.yml
vendored
9
.github/workflows/qrcode-sdk-ci.yml
vendored
@@ -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
|
||||||
|
|||||||
6
.github/workflows/web.yml
vendored
6
.github/workflows/web.yml
vendored
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
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(
|
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;
|
||||||
@@ -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';
|
||||||
|
|||||||
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',
|
'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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe('navigation', () => {
|
|||||||
'DeferredLinkingInfo',
|
'DeferredLinkingInfo',
|
||||||
'DevFeatureFlags',
|
'DevFeatureFlags',
|
||||||
'DevHapticFeedback',
|
'DevHapticFeedback',
|
||||||
|
'DevPrivateKey',
|
||||||
'DevSettings',
|
'DevSettings',
|
||||||
'Disclaimer',
|
'Disclaimer',
|
||||||
'DocumentCamera',
|
'DocumentCamera',
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user