mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
Merge pull request #1802 from selfxyz/release/staging-2026-03-03
Release to Staging v2.9.16 - 2026-03-03
This commit is contained in:
76
.github/workflows/mobile-deploy.yml
vendored
76
.github/workflows/mobile-deploy.yml
vendored
@@ -1304,6 +1304,16 @@ jobs:
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Copy version-manager from build branch
|
||||
run: |
|
||||
BUILD_REF="${{ github.ref_name }}"
|
||||
echo "📋 Using version-manager.cjs from build branch: $BUILD_REF"
|
||||
SCRIPT_CONTENT=$(git show "origin/${BUILD_REF}:app/scripts/version-manager.cjs") || {
|
||||
echo "❌ Failed to read version-manager.cjs from origin/${BUILD_REF}"
|
||||
exit 1
|
||||
}
|
||||
echo "$SCRIPT_CONTENT" > "${{ env.APP_PATH }}/scripts/version-manager.cjs"
|
||||
|
||||
- name: Apply version bump from outputs
|
||||
run: |
|
||||
cd ${{ env.APP_PATH }}
|
||||
@@ -1314,56 +1324,39 @@ jobs:
|
||||
IOS_SUCCESS="${{ needs.build-ios.result }}"
|
||||
ANDROID_SUCCESS="${{ needs.build-android.result }}"
|
||||
|
||||
echo "📝 Applying version bump: $VERSION (iOS: $IOS_BUILD, Android: $ANDROID_BUILD)"
|
||||
echo "📝 Applying version bump: $VERSION (iOS: $IOS_BUILD [$IOS_SUCCESS], Android: $ANDROID_BUILD [$ANDROID_SUCCESS])"
|
||||
|
||||
# Update package.json version
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
pkg.version = '$VERSION';
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
console.log('✅ Updated package.json');
|
||||
"
|
||||
# Update package.json, version.json, and platform build files
|
||||
# Pass build results so the script only updates platforms that succeeded
|
||||
node scripts/version-manager.cjs apply "$VERSION" "$IOS_BUILD" "$ANDROID_BUILD" "$IOS_SUCCESS" "$ANDROID_SUCCESS"
|
||||
|
||||
# Update version.json build numbers and deployment timestamps
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const version = JSON.parse(fs.readFileSync('version.json', 'utf8'));
|
||||
const timestamp = new Date().toISOString();
|
||||
- name: Load version-managed files
|
||||
run: |
|
||||
cd ${{ env.APP_PATH }}
|
||||
|
||||
// Only bump build numbers for successful builds
|
||||
if ('$IOS_SUCCESS' === 'success') {
|
||||
version.ios.build = $IOS_BUILD;
|
||||
version.ios.lastDeployed = timestamp;
|
||||
console.log('✅ Updated iOS build number to $IOS_BUILD and lastDeployed timestamp');
|
||||
} else {
|
||||
console.log('⏭️ Skipped iOS build number bump (build did not succeed)');
|
||||
}
|
||||
VERSION_MANAGED_FILES="$(node scripts/version-manager.cjs files --shell)"
|
||||
if [ -z "$VERSION_MANAGED_FILES" ]; then
|
||||
echo "❌ Failed to load version-managed files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ('$ANDROID_SUCCESS' === 'success') {
|
||||
version.android.build = $ANDROID_BUILD;
|
||||
version.android.lastDeployed = timestamp;
|
||||
console.log('✅ Updated Android build number to $ANDROID_BUILD and lastDeployed timestamp');
|
||||
} else {
|
||||
console.log('⏭️ Skipped Android build number bump (build did not succeed)');
|
||||
}
|
||||
|
||||
fs.writeFileSync('version.json', JSON.stringify(version, null, 2) + '\n');
|
||||
console.log('✅ Updated version.json');
|
||||
"
|
||||
|
||||
echo "✅ Versions applied successfully"
|
||||
{
|
||||
echo "VERSION_MANAGED_FILES<<EOF"
|
||||
echo "$VERSION_MANAGED_FILES"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_ENV
|
||||
echo "🗂️ Version-managed files: $VERSION_MANAGED_FILES"
|
||||
|
||||
- name: Verify version changes
|
||||
run: |
|
||||
cd ${{ env.APP_PATH }}
|
||||
|
||||
# Check that version files actually changed
|
||||
if ! git diff --quiet package.json version.json; then
|
||||
# Check that version-managed files actually changed
|
||||
if ! git diff --quiet -- $VERSION_MANAGED_FILES; then
|
||||
echo "✅ Version changes detected"
|
||||
git diff package.json version.json
|
||||
git diff -- $VERSION_MANAGED_FILES
|
||||
else
|
||||
echo "⚠️ No version changes detected in package.json or version.json"
|
||||
echo "⚠️ No version changes detected in version-managed files"
|
||||
echo "This may indicate a problem with version application"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1430,14 +1423,14 @@ jobs:
|
||||
|
||||
# Commit the version changes
|
||||
cd ${{ env.APP_PATH }}
|
||||
git add package.json version.json
|
||||
git add $VERSION_MANAGED_FILES
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "⚠️ No version changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "chore: bump mobile app version to $VERSION" -m "Update build numbers and deployment timestamps after successful deployment."
|
||||
git commit -m "chore: bump mobile app version to $VERSION" -m "Update build numbers, platform build files, and deployment timestamps after successful deployment."
|
||||
|
||||
# Create new branch from current HEAD (bump target branch with version bump)
|
||||
git checkout -b ${BRANCH_NAME}
|
||||
@@ -1462,6 +1455,7 @@ jobs:
|
||||
|
||||
This PR updates:
|
||||
- Build numbers for deployed platforms
|
||||
- Platform build files (Android `versionCode`, iOS `CURRENT_PROJECT_VERSION` and `MARKETING_VERSION`)
|
||||
- Deployment timestamps (\`lastDeployed\`) for successful builds
|
||||
|
||||
This PR was automatically created by the mobile deployment workflow." \
|
||||
|
||||
131
.github/workflows/mobile-e2e.yml
vendored
131
.github/workflows/mobile-e2e.yml
vendored
@@ -3,7 +3,7 @@ name: Mobile E2E
|
||||
env:
|
||||
# Build environment versions
|
||||
JAVA_VERSION: 17
|
||||
ANDROID_API_LEVEL: 33
|
||||
ANDROID_API_LEVEL: 34
|
||||
ANDROID_NDK_VERSION: 27.0.12077973
|
||||
XCODE_VERSION: 26
|
||||
# Cache versions
|
||||
@@ -41,14 +41,15 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
android-build-test:
|
||||
# Currently build-only for Android with private repos. E2E steps are preserved but skipped (if: false).
|
||||
# To re-enable full E2E: change `if: false` to `if: true` on Maestro and emulator steps.
|
||||
e2e-android:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-android-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
timeout-minutes: 120
|
||||
runs-on: ubuntu-latest
|
||||
runs-on:
|
||||
- "self-hosted"
|
||||
- "selfxyz-org"
|
||||
- "ubuntu-24-04"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Read and sanitize Node.js version
|
||||
@@ -109,36 +110,45 @@ jobs:
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 5
|
||||
command: yarn install --immutable --silent
|
||||
|
||||
- name: Validate Maestro test file
|
||||
if: false # Skip for build-only test - keep logic for future E2E
|
||||
run: |
|
||||
[ -f app/tests/e2e/launch.android.flow.yaml ] || { echo "❌ Android E2E test file missing"; exit 1; }
|
||||
- name: Cache Maestro
|
||||
if: false # Skip for build-only test - keep logic for future E2E
|
||||
id: cache-maestro
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.maestro
|
||||
key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}
|
||||
- name: Install Maestro
|
||||
if: false # Skip for build-only test - keep logic for future E2E
|
||||
run: curl -Ls "https://get.maestro.mobile.dev" | bash
|
||||
- name: Add Maestro to path
|
||||
if: false # Skip for build-only test - keep logic for future E2E
|
||||
run: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
|
||||
- name: Free up disk space
|
||||
uses: ./.github/actions/free-disk-space
|
||||
- name: Check Java installation
|
||||
run: |
|
||||
echo "INSTALL_JAVA=false" >> "$GITHUB_ENV"
|
||||
if command -v java &> /dev/null && java -version &> /dev/null; then
|
||||
echo "Java already installed: $(java -version 2>&1 | head -n 1)"
|
||||
else
|
||||
echo "Java not found or not working, will install..."
|
||||
echo "INSTALL_JAVA=true" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Setup Java environment
|
||||
if: env.INSTALL_JAVA == 'true'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
- name: Install Maestro
|
||||
run: curl -Ls "https://get.maestro.mobile.dev" | bash
|
||||
- name: Add Maestro to path
|
||||
run: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
|
||||
- name: Free up disk space
|
||||
if: false # We should be fine for self-hosted
|
||||
uses: ./.github/actions/free-disk-space
|
||||
- name: Cache Gradle packages
|
||||
uses: ./.github/actions/cache-gradle
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
with:
|
||||
accept-android-sdk-licenses: true
|
||||
|
||||
- name: Install NDK
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
@@ -146,6 +156,7 @@ jobs:
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 10
|
||||
command: sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}"
|
||||
|
||||
- name: Build dependencies (outside emulator)
|
||||
run: |
|
||||
echo "Building dependencies..."
|
||||
@@ -165,8 +176,12 @@ jobs:
|
||||
run: |
|
||||
echo "Building Android APK..."
|
||||
chmod +x app/android/gradlew
|
||||
(cd app/android && ./gradlew assembleDebug --quiet --parallel --build-cache --no-configuration-cache) || { echo "❌ Android build failed"; exit 1; }
|
||||
(
|
||||
cd app/android &&
|
||||
E2E_BUILD=true ./gradlew assembleDebug -PbundleInDebug=true --quiet --parallel --build-cache --no-configuration-cache
|
||||
) || { echo "❌ Android build failed"; exit 1; }
|
||||
echo "✅ Android build succeeded"
|
||||
|
||||
- name: Clean up Gradle build artifacts
|
||||
uses: ./.github/actions/cleanup-gradle-artifacts
|
||||
- name: Verify APK and android-passport-nfc-reader integration
|
||||
@@ -208,32 +223,80 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "🎉 Build verification completed successfully!"
|
||||
echo "ℹ️ Emulator testing is temporarily disabled - build testing only"
|
||||
- name: Check KVM access
|
||||
id: kvm
|
||||
run: |
|
||||
if [ ! -e /dev/kvm ]; then
|
||||
echo "::warning::/dev/kvm not found — emulator will run in software mode (slow). Enable nested virtualization for better performance."
|
||||
echo "available=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if [ -w /dev/kvm ]; then
|
||||
echo "✅ /dev/kvm is already writable"
|
||||
elif [ -d /etc/udev/rules.d ]; then
|
||||
echo "Setting KVM permissions via udev..."
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
else
|
||||
echo "No udev available, setting /dev/kvm permissions directly..."
|
||||
sudo chmod 666 /dev/kvm
|
||||
fi
|
||||
ls -la /dev/kvm
|
||||
echo "available=true" >> "$GITHUB_OUTPUT"
|
||||
- name: Install emulator runtime dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
libx11-6 libgl1 libpulse0 libnss3 \
|
||||
libxcomposite1 libxcursor1 libxi6 libxtst6 \
|
||||
libxrandr2 libxss1 libxdamage1 libxfixes3 \
|
||||
libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
|
||||
libgbm1 libgtk-3-0 libpango-1.0-0 libcairo2 \
|
||||
> /dev/null 2>&1
|
||||
- name: Create E2E test script
|
||||
run: |
|
||||
cat > /tmp/run-e2e.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
APP_ID="com.proofofpassportapp"
|
||||
echo "Installing app on emulator..."
|
||||
APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk"
|
||||
[ -f "$APK_PATH" ] || { echo "❌ APK not found at $APK_PATH"; exit 1; }
|
||||
adb uninstall "$APP_ID" >/dev/null 2>&1 || true
|
||||
adb install -r "$APK_PATH" || { echo "❌ App installation failed"; exit 1; }
|
||||
adb shell pm clear "$APP_ID" >/dev/null 2>&1 || true
|
||||
echo "✅ App installed successfully"
|
||||
echo "⏰ Giving the emulator a moment to settle..."
|
||||
sleep 5
|
||||
echo "🎭 Running Maestro tests..."
|
||||
export MAESTRO_DRIVER_STARTUP_TIMEOUT=180000
|
||||
export LAUNCH_WAIT_MS=180000
|
||||
if ! maestro test app/tests/e2e/launch.android.flow.yaml --format junit --output app/maestro-results.xml; then
|
||||
echo "❌ Maestro failed - dumping Android diagnostics"
|
||||
adb shell dumpsys activity activities | sed -n '1,240p' || true
|
||||
adb logcat -d | tail -n 400 || true
|
||||
exit 1
|
||||
fi
|
||||
EOF
|
||||
chmod +x /tmp/run-e2e.sh
|
||||
- name: Install and Test on Android
|
||||
if: false # Skip emulator/E2E for build-only test - keep logic for future E2E
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ env.ANDROID_API_LEVEL }}
|
||||
arch: x86_64
|
||||
target: google_apis
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none -memory 8192 -cores 4 -accel on
|
||||
target: default
|
||||
channel: stable
|
||||
force-avd-creation: true
|
||||
emulator-options: >-
|
||||
-no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
|
||||
-camera-back none -camera-front none -memory 8192 -cores 4
|
||||
${{ steps.kvm.outputs.available == 'true' && '-accel on' || '-accel off' }}
|
||||
disable-animations: true
|
||||
script: |
|
||||
echo "Installing app on emulator..."
|
||||
APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk"
|
||||
[ -f "$APK_PATH" ] || { echo "❌ APK not found at $APK_PATH"; exit 1; }
|
||||
adb install -r "$APK_PATH" || { echo "❌ App installation failed"; exit 1; }
|
||||
echo "✅ App installed successfully"
|
||||
script: /tmp/run-e2e.sh
|
||||
|
||||
echo "⏰ Giving the emulator a moment to settle..."
|
||||
sleep 5
|
||||
|
||||
echo "🎭 Running Maestro tests..."
|
||||
export MAESTRO_DRIVER_STARTUP_TIMEOUT=180000
|
||||
maestro test app/tests/e2e/launch.android.flow.yaml --format junit --output app/maestro-results.xml
|
||||
- name: Upload test results
|
||||
if: false # Skip for build-only test - keep logic for future E2E
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maestro-results-android
|
||||
|
||||
@@ -42,7 +42,7 @@ Before creating a PR, ensure:
|
||||
|
||||
#### AI Review Preparation
|
||||
|
||||
- [ ] Clear commit messages following conventional format
|
||||
- [ ] Clear, imperative commit messages (e.g. `Fix address validation`)
|
||||
- [ ] PR description includes context for AI reviewers
|
||||
- [ ] Complex changes have inline comments explaining intent
|
||||
- [ ] Security-sensitive changes flagged for special review
|
||||
|
||||
16
CLAUDE.md
16
CLAUDE.md
@@ -16,9 +16,19 @@ nvm use && corepack enable && yarn install
|
||||
## Key Rules
|
||||
|
||||
- **Package manager:** Yarn (never npm or pnpm)
|
||||
- **Keep the codebase DRY.** Before writing new code, search for existing utilities/components/flows and reuse or refactor to shared modules. Create new code only if a reusable option does not exist.
|
||||
- **Extract repeated UI.** If the same UI sub-structure appears in 2+ places, extract a shared component.
|
||||
- **Reusable UI belongs in shared libraries.** If a UI primitive is broadly reusable, add it to a shared library (e.g., `@selfxyz/euclid` or another shared package) instead of duplicating in feature code.
|
||||
- **Keep files small.** Aim for <800 LOC per file. If a file approaches 800 LOC, split it into smaller modules.
|
||||
- **Move static data out of UI.** Large static maps/lookups/constants do not belong in screen/components; move them to `utils/` or `data/` modules.
|
||||
- **Prefer design tokens over hex.** Use shared color/font/spacing tokens instead of raw hex values in UI code.
|
||||
- **No `react-native` imports in SDK core.** `packages/mobile-sdk-alpha/src/` must be platform-agnostic outside of `src/adapters/react-native/`.
|
||||
- **Native handlers are thin wrappers.** No business logic in Kotlin or Swift. All logic lives in TypeScript.
|
||||
- **Keychain is always native-managed.** No web fallbacks for secure storage. This is a security boundary.
|
||||
- **No “slop comments.”** Only add comments when they convey non-obvious intent or constraints. Never add generic or chatty comments.
|
||||
- **Test value over mock wiring.** Prefer tests that validate behavior. Avoid tests that only assert mocks were called unless that is the behavior being validated.
|
||||
- **PR size target:** 1k–3k LOC changed. Smaller is fine for focused fixes. If >3k, add a brief justification for why it can’t be split.
|
||||
- **No generated artifacts in source PRs.** Do not commit build outputs or generated assets unless the build system requires them for runtime or distribution.
|
||||
|
||||
## Specs & Planning
|
||||
|
||||
@@ -61,13 +71,13 @@ Quick-start prompts for creating new specs are in [SPEC-GUIDE.md](./specs/SPEC-G
|
||||
|
||||
```bash
|
||||
# SDK core
|
||||
cd packages/mobile-sdk-alpha && npx vitest run && npx tsc --noEmit
|
||||
cd packages/mobile-sdk-alpha && yarn test && yarn types
|
||||
|
||||
# Bridge
|
||||
cd packages/webview-bridge && yarn build && yarn vitest run
|
||||
cd packages/webview-bridge && yarn build && yarn test
|
||||
|
||||
# WebView app
|
||||
cd packages/webview-app && npx tsc --noEmit && npx vite build
|
||||
cd packages/webview-app && yarn build
|
||||
|
||||
# KMP
|
||||
cd packages/kmp-sdk && ./gradlew :shared:jvmTest
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.2.7
|
||||
3.2.8
|
||||
|
||||
@@ -15,7 +15,7 @@ GEM
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
addressable (2.8.8)
|
||||
addressable (2.8.9)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
@@ -23,7 +23,7 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1216.0)
|
||||
aws-partitions (1.1220.0)
|
||||
aws-sdk-core (3.242.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
@@ -133,7 +133,7 @@ GEM
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.232.1)
|
||||
fastlane (2.232.2)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
abbrev (~> 0.1.2)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
@@ -205,7 +205,7 @@ GEM
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.17.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.60.0)
|
||||
google-apis-storage_v1 (0.61.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
@@ -244,7 +244,8 @@ GEM
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (6.0.1)
|
||||
minitest (6.0.2)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.19.1)
|
||||
@@ -329,7 +330,7 @@ DEPENDENCIES
|
||||
nokogiri (~> 1.18)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.7p253
|
||||
ruby 3.2.8p263
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.9
|
||||
|
||||
@@ -24,6 +24,10 @@ react {
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
if (System.env.E2E_BUILD == "true") {
|
||||
// For E2E/CI builds: bundle JS into the debug APK (no Metro server available)
|
||||
debuggableVariants = []
|
||||
}
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
@@ -221,7 +225,7 @@ dependencies {
|
||||
implementation jscFlavor
|
||||
}
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation("net.java.dev.jna:jna:5.16.0@aar")
|
||||
implementation("net.java.dev.jna:jna:5.16.0")
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
|
||||
@@ -24,6 +24,7 @@ buildscript {
|
||||
classpath('com.android.tools.build:gradle:8.11.2')
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlinVersion"
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
// Removed firebase-crashlytics (no usages)—add back 3.x if still applied in modules.
|
||||
// Removed rust-android-gradle plugin; keep only if you re-enable Rust integration.
|
||||
@@ -60,6 +61,9 @@ allprojects {
|
||||
|
||||
subprojects {
|
||||
afterEvaluate { project ->
|
||||
if (project.hasProperty('android') && project.android.buildFeatures.compose) {
|
||||
project.apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
||||
}
|
||||
if (project.hasProperty('android')) {
|
||||
android {
|
||||
def manifestFile = project.file('src/main/AndroidManifest.xml')
|
||||
|
||||
@@ -30,7 +30,7 @@ require File.join(File.dirname(`node --print "require.resolve('expo/package.json
|
||||
project "Self.xcodeproj"
|
||||
|
||||
# Define consistent iOS deployment target
|
||||
IOS_DEPLOYMENT_TARGET = "15.1"
|
||||
IOS_DEPLOYMENT_TARGET = "15.4"
|
||||
|
||||
platform :ios, IOS_DEPLOYMENT_TARGET if !ENV["ACT"]
|
||||
prepare_react_native_project!
|
||||
|
||||
@@ -8,6 +8,28 @@ PODS:
|
||||
- boost (1.84.0)
|
||||
- BVLinearGradient (2.8.3):
|
||||
- React-Core
|
||||
- dotlottie-react-native (0.5.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- LottieFiles-dotLottie-iOS (~> 0.9)
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- DoubleConversion (1.1.6)
|
||||
- EXApplication (6.0.2):
|
||||
- ExpoModulesCore
|
||||
@@ -49,7 +71,7 @@ PODS:
|
||||
- Yoga
|
||||
- fast_float (6.1.4)
|
||||
- FBLazyVector (0.77.0)
|
||||
- FingerprintPro (2.12.0)
|
||||
- FingerprintPro (2.13.0)
|
||||
- Firebase (10.24.0):
|
||||
- Firebase/Core (= 10.24.0)
|
||||
- Firebase/Core (10.24.0):
|
||||
@@ -196,29 +218,8 @@ PODS:
|
||||
- IdensicMobileSDK/Fisherman (1.40.2):
|
||||
- FingerprintPro (~> 2.11)
|
||||
- IdensicMobileSDK/Core
|
||||
- lottie-ios (4.5.0)
|
||||
- lottie-react-native (7.2.2):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- lottie-ios (= 4.5.0)
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- lottie-ios (4.6.0)
|
||||
- LottieFiles-dotLottie-iOS (0.11.1)
|
||||
- Mixpanel-swift (5.0.0):
|
||||
- Mixpanel-swift/Complete (= 5.0.0)
|
||||
- Mixpanel-swift/Complete (5.0.0)
|
||||
@@ -2182,6 +2183,7 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
|
||||
- "dotlottie-react-native (from `../node_modules/@lottiefiles/dotlottie-react-native`)"
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- EXApplication (from `../node_modules/expo-application/ios`)
|
||||
- EXConstants (from `../node_modules/expo-constants/ios`)
|
||||
@@ -2202,7 +2204,6 @@ DEPENDENCIES:
|
||||
- GoogleUtilities
|
||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||
- lottie-ios
|
||||
- lottie-react-native (from `../node_modules/lottie-react-native`)
|
||||
- Mixpanel-swift
|
||||
- "NFCPassportReader (from `git@github.com:selfxyz/NFCPassportReader.git`, commit `9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b`)"
|
||||
- QKMRZScanner
|
||||
@@ -2319,6 +2320,7 @@ SPEC REPOS:
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- lottie-ios
|
||||
- LottieFiles-dotLottie-iOS
|
||||
- Mixpanel-swift
|
||||
- nanopb
|
||||
- OpenSSL-Universal
|
||||
@@ -2334,6 +2336,8 @@ EXTERNAL SOURCES:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
|
||||
BVLinearGradient:
|
||||
:path: "../node_modules/react-native-linear-gradient"
|
||||
dotlottie-react-native:
|
||||
:path: "../node_modules/@lottiefiles/dotlottie-react-native"
|
||||
DoubleConversion:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
EXApplication:
|
||||
@@ -2363,8 +2367,6 @@ EXTERNAL SOURCES:
|
||||
hermes-engine:
|
||||
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||
:tag: hermes-2024-11-25-RNv0.77.0-d4f25d534ab744866448b36ca3bf3d97c08e638c
|
||||
lottie-react-native:
|
||||
:path: "../node_modules/lottie-react-native"
|
||||
NFCPassportReader:
|
||||
:commit: 9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b
|
||||
:git: "git@github.com:selfxyz/NFCPassportReader.git"
|
||||
@@ -2559,6 +2561,7 @@ SPEC CHECKSUMS:
|
||||
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
||||
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
||||
BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70
|
||||
dotlottie-react-native: 056445614fe969f8d8d90a744944089261e6a620
|
||||
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
||||
EXApplication: 4c72f6017a14a65e338c5e74fca418f35141e819
|
||||
EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b
|
||||
@@ -2570,7 +2573,7 @@ SPEC CHECKSUMS:
|
||||
ExpoModulesCore: bcee92d3a2c68c408b2d8da43e3094109340dc17
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 2bc03a5cf64e29c611bbc5d7eb9d9f7431f37ee6
|
||||
FingerprintPro: 035517a1b4e3e4fc073486b53b9956509010f8db
|
||||
FingerprintPro: 2f419138022451a72f783db9c94967f5a68e9977
|
||||
Firebase: 91fefd38712feb9186ea8996af6cbdef41473442
|
||||
FirebaseABTesting: d87f56707159bae64e269757a6e963d490f2eebe
|
||||
FirebaseAnalytics: b5efc493eb0f40ec560b04a472e3e1a15d39ca13
|
||||
@@ -2589,8 +2592,8 @@ SPEC CHECKSUMS:
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
hermes-engine: 1f783c3d53940aed0d2c84586f0b7a85ab7827ef
|
||||
IdensicMobileSDK: 00b13320e1b1e0574e68475bd0fbc7cd30fce26e
|
||||
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||
lottie-react-native: 6cb05b7b4ea463afe657e3b46784f067858e1a5d
|
||||
lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3
|
||||
LottieFiles-dotLottie-iOS: e9b34e7cff6d04f5affd97336c2dab934b86e6fb
|
||||
Mixpanel-swift: e9bef28a9648faff384d5ba6f48ecc2787eb24c0
|
||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||
NFCPassportReader: 48873f856f91215dbfa1eaaec20eae639672862e
|
||||
@@ -2691,6 +2694,6 @@ SPEC CHECKSUMS:
|
||||
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
|
||||
Yoga: c34725819ab0a5962e85455b9e56679b306910ee
|
||||
|
||||
PODFILE CHECKSUM: 819cc2c73ef2429fda27c9f3e3ac757539b52c75
|
||||
PODFILE CHECKSUM: 63b07a2aa49988e11d648524199e37c6a96de7aa
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -569,7 +569,7 @@
|
||||
INFOPLIST_FILE = OpenPassport/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = OpenPassport;
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Needed only if you want to upload QRcodes";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -712,7 +712,7 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = OpenPassport;
|
||||
INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Needed only if you want to upload QRcodes";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -812,7 +812,7 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||
);
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Needed to scan your passport MRZ, you can however enter it manually.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
LD = "";
|
||||
LDPLUSPLUS = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -909,7 +909,7 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||
);
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Needed to scan your passport MRZ, you can however enter it manually.";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
LD = "";
|
||||
LDPLUSPLUS = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
||||
@@ -31,7 +31,8 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
'^@env$': '<rootDir>/tests/__setup__/@env.js',
|
||||
'\\.svg$': '<rootDir>/tests/__setup__/svgMock.js',
|
||||
'\\.(png|jpg|jpeg|gif|webp)$': '<rootDir>/tests/__setup__/imageMock.js',
|
||||
'\\.(png|jpg|jpeg|gif|webp|lottie)$':
|
||||
'<rootDir>/tests/__setup__/imageMock.js',
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^@$': '<rootDir>/src',
|
||||
'^@tests/(.*)$': '<rootDir>/tests/src/$1',
|
||||
@@ -50,6 +51,8 @@ module.exports = {
|
||||
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/flows/disclosing/$1.cjs',
|
||||
'^@selfxyz/mobile-sdk-alpha/(.*)\\.json$':
|
||||
'<rootDir>/../packages/mobile-sdk-alpha/dist/$1.json',
|
||||
'^@selfxyz/mobile-sdk-alpha/(.*)\\.lottie$':
|
||||
'<rootDir>/tests/__setup__/imageMock.js',
|
||||
'^@selfxyz/mobile-sdk-alpha/(.*)$':
|
||||
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/$1.cjs',
|
||||
// Fix snarkjs resolution for @anon-aadhaar/core
|
||||
|
||||
@@ -68,7 +68,7 @@ const config = {
|
||||
new RegExp('packages/mobile-sdk-alpha/node_modules/react-dom(/|$)'),
|
||||
new RegExp('packages/mobile-sdk-alpha/node_modules/react-native(/|$)'),
|
||||
new RegExp(
|
||||
'packages/mobile-sdk-alpha/node_modules/lottie-react-native(/|$)',
|
||||
'packages/mobile-sdk-alpha/node_modules/@lottiefiles/dotlottie-react-native(/|$)',
|
||||
),
|
||||
new RegExp('packages/mobile-sdk-alpha/node_modules/scheduler(/|$)'),
|
||||
new RegExp(
|
||||
@@ -117,8 +117,8 @@ const config = {
|
||||
// Support package exports with conditions
|
||||
unstable_conditionNames: ['react-native', 'import', 'require'],
|
||||
|
||||
// SVG support
|
||||
assetExts: assetExts.filter(ext => ext !== 'svg'),
|
||||
// SVG support + dotLottie binary assets
|
||||
assetExts: [...assetExts.filter(ext => ext !== 'svg'), 'lottie'],
|
||||
sourceExts: [...sourceExts, 'svg'],
|
||||
|
||||
// Custom resolver to handle both .js imports in TypeScript and Node.js modules
|
||||
@@ -133,6 +133,41 @@ const config = {
|
||||
'packages/mobile-sdk-alpha',
|
||||
);
|
||||
|
||||
// Deduplicate SDK animation imports — resolve to app's single copy when possible
|
||||
if (
|
||||
/\.(json|lottie)$/.test(moduleName) &&
|
||||
context.originModulePath?.includes('mobile-sdk-alpha')
|
||||
) {
|
||||
// Extract the animation-relative path from either bare or relative specifiers
|
||||
let animRelPath;
|
||||
if (moduleName.startsWith('src/animations/')) {
|
||||
animRelPath = moduleName.replace('src/animations/', '');
|
||||
} else if (/\/animations\//.test(moduleName)) {
|
||||
animRelPath = moduleName.split('/animations/').pop();
|
||||
}
|
||||
|
||||
if (animRelPath) {
|
||||
// Try app's animations first (deduplication)
|
||||
const appAnimPath = path.resolve(
|
||||
projectRoot,
|
||||
'src/assets/animations',
|
||||
animRelPath,
|
||||
);
|
||||
if (fs.existsSync(appAnimPath)) {
|
||||
return { type: 'assetFiles', filePaths: [appAnimPath] };
|
||||
}
|
||||
// Fall back to SDK's own copy (for SDK-only animations like loading/*)
|
||||
const sdkAnimPath = path.resolve(
|
||||
sdkAlphaPath,
|
||||
'src/animations',
|
||||
animRelPath,
|
||||
);
|
||||
if (fs.existsSync(sdkAnimPath)) {
|
||||
return { type: 'assetFiles', filePaths: [sdkAnimPath] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom resolver to handle Node.js modules and dynamic flow imports
|
||||
if (moduleName.startsWith('@selfxyz/mobile-sdk-alpha/')) {
|
||||
const subPath = moduleName.replace('@selfxyz/mobile-sdk-alpha/', '');
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"analyze:bundle:android": "yarn build:deps && node ./scripts/bundle-analyze-ci.cjs android",
|
||||
"analyze:bundle:ios": "yarn build:deps && node ./scripts/bundle-analyze-ci.cjs ios",
|
||||
"animations:convert": "node ./scripts/convert-to-dotlottie.mjs",
|
||||
"analyze:tree-shaking": "node ./scripts/analyze-tree-shaking.cjs imports",
|
||||
"analyze:tree-shaking:web": "yarn web:build && node ./scripts/analyze-tree-shaking.cjs web",
|
||||
"android": "yarn build:deps && yarn setup:android-deps && react-native run-android",
|
||||
@@ -87,6 +88,8 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"@ethersproject/shims": "^5.8.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.15",
|
||||
"@lottiefiles/dotlottie-react-native": "0.5.0",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
"@openpassport/zk-kit-imt": "^0.0.5",
|
||||
"@openpassport/zk-kit-lean-imt": "^0.0.6",
|
||||
@@ -135,7 +138,6 @@
|
||||
"js-sha256": "^0.11.1",
|
||||
"js-sha512": "^0.9.0",
|
||||
"lottie-react": "^2.4.1",
|
||||
"lottie-react-native": "7.2.2",
|
||||
"node-forge": "^1.3.3",
|
||||
"pkijs": "^3.3.3",
|
||||
"poseidon-lite": "^0.2.0",
|
||||
@@ -185,6 +187,7 @@
|
||||
"@babel/plugin-transform-private-methods": "^7.28.6",
|
||||
"@babel/preset-env": "^7.28.6",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@dotlottie/dotlottie-js": "^1.6.2",
|
||||
"@react-native-community/cli": "^16.0.3",
|
||||
"@react-native/babel-preset": "0.77.0",
|
||||
"@react-native/eslint-config": "0.77.0",
|
||||
|
||||
48
app/scripts/convert-to-dotlottie.mjs
Normal file
48
app/scripts/convert-to-dotlottie.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
// Convert large Lottie JSON animations to compressed dotLottie format.
|
||||
// Usage: node scripts/convert-to-dotlottie.mjs
|
||||
//
|
||||
// Web compatibility: @lottiefiles/dotlottie-react-native supports .lottie
|
||||
// natively, but lottie-web does NOT. If these animations are ever used on
|
||||
// the web, use
|
||||
// @lottiefiles/dotlottie-web (or its React/Vue wrappers) instead of lottie-web.
|
||||
|
||||
import { readFileSync, writeFileSync, statSync } from 'node:fs';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { DotLottie } from '@dotlottie/dotlottie-js';
|
||||
|
||||
const files = process.argv.slice(2);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.error(
|
||||
'Usage: node convert-to-dotlottie.mjs <file1.json> [file2.json ...]',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const jsonData = readFileSync(file, 'utf-8');
|
||||
const animName = basename(file, '.json');
|
||||
const dir = dirname(file);
|
||||
const outFile = join(dir, `${animName}.lottie`);
|
||||
|
||||
const dotlottie = new DotLottie();
|
||||
dotlottie.addAnimation({
|
||||
id: animName,
|
||||
data: JSON.parse(jsonData),
|
||||
});
|
||||
|
||||
const buffer = await dotlottie.build();
|
||||
writeFileSync(outFile, Buffer.from(await buffer.toArrayBuffer()));
|
||||
|
||||
const jsonSize = statSync(file).size;
|
||||
const lottieSize = statSync(outFile).size;
|
||||
const pct = ((1 - lottieSize / jsonSize) * 100).toFixed(1);
|
||||
console.log(
|
||||
`${basename(file)} → ${basename(outFile)}: ${(jsonSize / 1024).toFixed(0)}KB → ${(lottieSize / 1024).toFixed(0)}KB (${pct}% smaller)`,
|
||||
);
|
||||
}
|
||||
@@ -4,11 +4,17 @@
|
||||
|
||||
set -e
|
||||
|
||||
trap 'err "Setup failed at line $LINENO: $BASH_COMMAND"' ERR
|
||||
|
||||
# Config
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
APP_DIR="$REPO_ROOT/app"
|
||||
RUBY_VERSION=$(cat "$APP_DIR/.ruby-version" 2>/dev/null | tr -d '[:space:]')
|
||||
NODE_MAJOR=22
|
||||
NODE_VERSION=$(cat "$REPO_ROOT/.nvmrc" 2>/dev/null | tr -d '[:space:]')
|
||||
NODE_VERSION=${NODE_VERSION:-22.22.0}
|
||||
NODE_MAJOR=${NODE_VERSION%%.*}
|
||||
COCOAPODS_VERSION=$(grep -E '^ cocoapods \(' "$APP_DIR/Gemfile.lock" 2>/dev/null | head -1 | sed -E 's/.*\(([^)]+)\).*/\1/')
|
||||
BUNDLER_VERSION=$(grep -A 1 '^BUNDLED WITH$' "$APP_DIR/Gemfile.lock" 2>/dev/null | tail -n 1 | tr -d '[:space:]')
|
||||
|
||||
# Args (can be overridden interactively)
|
||||
CHECK_ONLY=false; AUTO_YES=false
|
||||
@@ -34,32 +40,126 @@ confirm() {
|
||||
[[ ! $REPLY =~ ^[Nn]$ ]]
|
||||
}
|
||||
|
||||
init_rbenv_shell() {
|
||||
command -v rbenv &>/dev/null || return 0
|
||||
# Avoid failing when the project's Ruby version is not installed yet.
|
||||
eval "$(rbenv init - --no-rehash 2>/dev/null)" || true
|
||||
}
|
||||
|
||||
load_shell_env() {
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
init_rbenv_shell
|
||||
}
|
||||
|
||||
# Check functions - return "ok:version" or "missing" or "wrong:version"
|
||||
chk_brew() { command -v brew &>/dev/null && echo "ok:$(brew --version | head -1 | cut -d' ' -f2)" || echo "missing"; }
|
||||
chk_nvm() { [[ -s "$HOME/.nvm/nvm.sh" ]] && echo "ok" || echo "missing"; }
|
||||
chk_node() { command -v node &>/dev/null && { v=$(node -v 2>/dev/null | tr -d 'v'); [[ -n "$v" && "${v%%.*}" -ge $NODE_MAJOR ]] && echo "ok:$v" || echo "wrong:$v"; } || echo "missing"; }
|
||||
chk_node() {
|
||||
command -v node &>/dev/null && {
|
||||
v=$(node -v 2>/dev/null | tr -d 'v')
|
||||
if [[ "$NODE_VERSION" == *.* ]]; then
|
||||
[[ -n "$v" && "$v" == "$NODE_VERSION" ]] && echo "ok:$v" || echo "wrong:$v"
|
||||
else
|
||||
[[ -n "$v" && "${v%%.*}" -ge $NODE_MAJOR ]] && echo "ok:$v" || echo "wrong:$v"
|
||||
fi
|
||||
} || echo "missing"
|
||||
}
|
||||
chk_watch() { command -v watchman &>/dev/null && echo "ok:$(watchman --version 2>/dev/null)" || echo "missing"; }
|
||||
chk_rbenv() { command -v rbenv &>/dev/null && echo "ok" || echo "missing"; }
|
||||
chk_ruby() { command -v ruby &>/dev/null && { v=$(ruby -v 2>/dev/null | cut -d' ' -f2); [[ "$v" == "$RUBY_VERSION"* ]] && echo "ok:$v" || echo "wrong:$v"; } || echo "missing"; }
|
||||
chk_pods() { command -v pod &>/dev/null && echo "ok:$(pod --version 2>/dev/null)" || echo "missing"; }
|
||||
chk_bundler() { command -v bundle &>/dev/null && echo "ok" || echo "missing"; }
|
||||
chk_ruby() {
|
||||
if command -v rbenv &>/dev/null; then
|
||||
rbenv versions --bare 2>/dev/null | grep -q "^${RUBY_VERSION}" && echo "ok:$RUBY_VERSION" || echo "missing"
|
||||
else
|
||||
command -v ruby &>/dev/null && { v=$(ruby -v 2>/dev/null | cut -d' ' -f2); [[ "$v" == "$RUBY_VERSION"* ]] && echo "ok:$v" || echo "wrong:$v"; } || echo "missing"
|
||||
fi
|
||||
}
|
||||
chk_pods() {
|
||||
command -v pod &>/dev/null || { echo "missing"; return; }
|
||||
v=$(pod --version 2>/dev/null) || { echo "missing"; return; }
|
||||
[[ -n "$v" ]] || { echo "missing"; return; }
|
||||
[[ -n "$COCOAPODS_VERSION" && "$v" != "$COCOAPODS_VERSION" ]] && echo "wrong:$v" || echo "ok:$v"
|
||||
}
|
||||
chk_bundler() {
|
||||
command -v bundle &>/dev/null || { echo "missing"; return; }
|
||||
v=$(bundle -v 2>/dev/null | awk '{print $3}') || { echo "missing"; return; }
|
||||
[[ -n "$v" ]] || { echo "missing"; return; }
|
||||
[[ -n "$BUNDLER_VERSION" && "$v" != "$BUNDLER_VERSION" ]] && echo "wrong:$v" || echo "ok:$v"
|
||||
}
|
||||
chk_java() { command -v java &>/dev/null && { v=$(java -version 2>&1 | head -1 | cut -d'"' -f2); [[ "$v" == 17* ]] && echo "ok:$v" || echo "wrong:$v"; } || echo "missing"; }
|
||||
chk_xcode() { xcode-select -p &>/dev/null && [[ "$(xcode-select -p)" == *Xcode.app* ]] && echo "ok" || echo "missing"; }
|
||||
chk_studio() { [[ -d "/Applications/Android Studio.app" ]] && echo "ok" || echo "missing"; }
|
||||
chk_sdk() { [[ -d "${ANDROID_HOME:-$HOME/Library/Android/sdk}" ]] && echo "ok" || echo "missing"; }
|
||||
chk_ndk() { [[ -d "${ANDROID_HOME:-$HOME/Library/Android/sdk}/ndk/28.0.13004108" ]] && echo "ok" || echo "missing"; }
|
||||
chk_shell() { local rc=~/.zshrc; [[ "$SHELL" == *bash* ]] && rc=~/.bashrc; grep -q "ANDROID_HOME" "$rc" 2>/dev/null && echo "ok" || echo "missing"; }
|
||||
chk_yarn() {
|
||||
command -v yarn &>/dev/null || { echo "missing"; return; }
|
||||
v=$(yarn -v 2>/dev/null)
|
||||
[[ -n "$v" && "${v%%.*}" -ge 4 ]] && echo "ok:$v" || echo "wrong:$v"
|
||||
}
|
||||
chk_swiftlint() { command -v swiftlint &>/dev/null && echo "ok:$(swiftlint version 2>/dev/null | head -1)" || echo "missing"; }
|
||||
|
||||
# Install functions
|
||||
inst_brew() { /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"; }
|
||||
inst_nvm() { curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash; export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; }
|
||||
inst_node() { export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm install $NODE_MAJOR; }
|
||||
inst_nvm() { curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash; export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; }
|
||||
inst_node() {
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
if ! command -v nvm &>/dev/null; then
|
||||
err "nvm not available after install"
|
||||
return 1
|
||||
fi
|
||||
nvm install "$NODE_VERSION"
|
||||
nvm alias default "$NODE_VERSION" >/dev/null 2>&1 || true
|
||||
}
|
||||
inst_watch() { brew install watchman; }
|
||||
inst_rbenv() { brew install rbenv; eval "$(rbenv init -)"; }
|
||||
inst_ruby() { eval "$(rbenv init -)" 2>/dev/null; rbenv install "$RUBY_VERSION"; rbenv rehash; }
|
||||
inst_pods() { gem install cocoapods; }
|
||||
inst_bundler() { gem install bundler; }
|
||||
inst_rbenv() { brew install rbenv; init_rbenv_shell; }
|
||||
inst_ruby() { init_rbenv_shell; rbenv install -s "$RUBY_VERSION"; rbenv global "$RUBY_VERSION"; rbenv rehash; }
|
||||
inst_pods() {
|
||||
if command -v rbenv &>/dev/null; then
|
||||
init_rbenv_shell
|
||||
rbenv shell "$RUBY_VERSION" 2>/dev/null || true
|
||||
if [[ -n "$COCOAPODS_VERSION" ]]; then
|
||||
rbenv exec gem install cocoapods -v "$COCOAPODS_VERSION"
|
||||
else
|
||||
rbenv exec gem install cocoapods
|
||||
fi
|
||||
else
|
||||
if [[ -n "$COCOAPODS_VERSION" ]]; then
|
||||
gem install cocoapods -v "$COCOAPODS_VERSION"
|
||||
else
|
||||
gem install cocoapods
|
||||
fi
|
||||
fi
|
||||
}
|
||||
inst_bundler() {
|
||||
if command -v rbenv &>/dev/null; then
|
||||
init_rbenv_shell
|
||||
rbenv shell "$RUBY_VERSION" 2>/dev/null || true
|
||||
if [[ -n "$BUNDLER_VERSION" ]]; then
|
||||
rbenv exec gem install bundler -v "$BUNDLER_VERSION"
|
||||
else
|
||||
rbenv exec gem install bundler
|
||||
fi
|
||||
else
|
||||
if [[ -n "$BUNDLER_VERSION" ]]; then
|
||||
gem install bundler -v "$BUNDLER_VERSION"
|
||||
else
|
||||
gem install bundler
|
||||
fi
|
||||
fi
|
||||
}
|
||||
inst_java() { brew install openjdk@17; sudo ln -sfn "$(brew --prefix openjdk@17)/libexec/openjdk.jdk" /Library/Java/JavaVirtualMachines/openjdk-17.jdk 2>/dev/null || true; }
|
||||
inst_yarn() {
|
||||
if command -v corepack &>/dev/null; then
|
||||
corepack enable
|
||||
corepack prepare yarn@stable --activate
|
||||
else
|
||||
err "corepack not available; ensure Node.js is installed and on PATH"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
inst_swiftlint() { brew install swiftlint; }
|
||||
|
||||
inst_shell() {
|
||||
local rc=~/.zshrc
|
||||
@@ -71,16 +171,20 @@ inst_shell() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
local jdk_path
|
||||
jdk_path="$(brew --prefix openjdk@17 2>/dev/null || echo /opt/homebrew/opt/openjdk@17)"
|
||||
|
||||
info "Adding environment to $rc..."
|
||||
cat >> "$rc" << 'EOF'
|
||||
cat >> "$rc" << EOF
|
||||
|
||||
# Self.xyz Dev Environment
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 17 2>/dev/null || echo "")
|
||||
export JAVA_HOME=\$(/usr/libexec/java_home -v 17 2>/dev/null || echo "")
|
||||
export PATH="$jdk_path/bin:\$PATH"
|
||||
export ANDROID_HOME=~/Library/Android/sdk
|
||||
export ANDROID_SDK_ROOT=$ANDROID_HOME
|
||||
export PATH=$PATH:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools
|
||||
command -v rbenv &>/dev/null && eval "$(rbenv init -)"
|
||||
export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
export ANDROID_SDK_ROOT=\$ANDROID_HOME
|
||||
export PATH=\$PATH:\$ANDROID_HOME/emulator:\$ANDROID_HOME/platform-tools
|
||||
command -v rbenv &>/dev/null && eval "\$(rbenv init - --no-rehash)"
|
||||
export NVM_DIR="\$HOME/.nvm"; [ -s "\$NVM_DIR/nvm.sh" ] && . "\$NVM_DIR/nvm.sh"
|
||||
EOF
|
||||
ok "Shell configured. Run: source $rc"
|
||||
}
|
||||
@@ -110,7 +214,8 @@ fi
|
||||
DEPS=(
|
||||
"Homebrew|chk_brew|inst_brew|"
|
||||
"nvm|chk_nvm|inst_nvm|"
|
||||
"Node.js $NODE_MAJOR|chk_node|inst_node|"
|
||||
"Node.js $NODE_VERSION|chk_node|inst_node|"
|
||||
"Yarn|chk_yarn|inst_yarn|"
|
||||
"Watchman|chk_watch|inst_watch|"
|
||||
"rbenv|chk_rbenv|inst_rbenv|"
|
||||
"Ruby $RUBY_VERSION|chk_ruby|inst_ruby|"
|
||||
@@ -121,19 +226,29 @@ DEPS=(
|
||||
"Android Studio|chk_studio||Download: https://developer.android.com/studio"
|
||||
"Android SDK|chk_sdk||Open Android Studio → SDK Manager"
|
||||
"Android NDK|chk_ndk||SDK Manager → SDK Tools → NDK 28.0.13004108"
|
||||
"SwiftLint|chk_swiftlint|inst_swiftlint|"
|
||||
"Shell Config|chk_shell|inst_shell|"
|
||||
)
|
||||
|
||||
MISSING=()
|
||||
MANUAL=()
|
||||
FAILED=()
|
||||
|
||||
info "Checking dependencies...\n"
|
||||
load_shell_env
|
||||
for dep in "${DEPS[@]}"; do
|
||||
IFS='|' read -r name chk inst manual <<< "$dep"
|
||||
status=$($chk)
|
||||
|
||||
if [[ "$status" == ok* ]]; then
|
||||
ver="${status#ok:}"; [[ -n "$ver" && "$ver" != "ok" ]] && ok "$name ($ver)" || ok "$name"
|
||||
elif [[ "$status" == wrong* ]]; then
|
||||
ver="${status#wrong:}"; [[ -n "$ver" ]] && err "$name - wrong version ($ver)" || err "$name - wrong version"
|
||||
if [[ -n "$inst" ]]; then
|
||||
MISSING+=("$name|$inst")
|
||||
elif [[ -n "$manual" ]]; then
|
||||
MANUAL+=("$name|$manual")
|
||||
fi
|
||||
elif [[ -n "$manual" ]]; then
|
||||
warn "$name - manual install required"
|
||||
MANUAL+=("$name|$manual")
|
||||
@@ -155,7 +270,12 @@ if [[ ${#MISSING[@]} -gt 0 ]]; then
|
||||
for m in "${MISSING[@]}"; do
|
||||
IFS='|' read -r name fn <<< "$m"
|
||||
info "Installing $name..."
|
||||
$fn && ok "$name installed" || err "Failed: $name"
|
||||
if $fn; then
|
||||
ok "$name installed"
|
||||
else
|
||||
err "Failed: $name"
|
||||
FAILED+=("$name")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
@@ -166,19 +286,34 @@ fi
|
||||
# Yarn install
|
||||
echo ""
|
||||
if confirm "Run 'yarn install' in repo root?"; then
|
||||
info "Running yarn install..."
|
||||
set +e # Temporarily disable exit-on-error
|
||||
cd "$REPO_ROOT" && yarn install
|
||||
yarn_exit=$?
|
||||
set -e # Re-enable exit-on-error
|
||||
|
||||
if [[ $yarn_exit -eq 0 ]]; then
|
||||
ok "Done!"
|
||||
if ! command -v yarn &>/dev/null; then
|
||||
err "yarn not available; run 'corepack enable && corepack prepare yarn@stable --activate' and retry"
|
||||
else
|
||||
err "Yarn install failed (exit code: $yarn_exit)"
|
||||
warn "This may be due to network issues or registry timeouts"
|
||||
info "Try running manually: cd $REPO_ROOT && yarn install"
|
||||
info "Running yarn install..."
|
||||
set +e # Temporarily disable exit-on-error
|
||||
cd "$REPO_ROOT" && yarn install
|
||||
yarn_exit=$?
|
||||
set -e # Re-enable exit-on-error
|
||||
|
||||
if [[ $yarn_exit -eq 0 ]]; then
|
||||
ok "Done!"
|
||||
else
|
||||
err "Yarn install failed (exit code: $yarn_exit)"
|
||||
warn "This may be due to network issues or registry timeouts"
|
||||
info "Try running manually: cd $REPO_ROOT && yarn install"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "\n${G}${BOLD}Setup complete!${NC} Open a new terminal, then: cd $APP_DIR && yarn ios\n"
|
||||
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
||||
echo -e "\n${R}${BOLD}Setup finished with failures:${NC} ${FAILED[*]}"
|
||||
echo -e "Please fix the above issues and re-run the script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\n${G}${BOLD}Setup complete!${NC}"
|
||||
echo -e "Next steps:"
|
||||
echo -e " 1) Open a new terminal and run: source ~/.zshrc (or ~/.bashrc)"
|
||||
echo -e " 2) Install dependencies: cd $REPO_ROOT && yarn install"
|
||||
echo -e " 3) Run the app: cd $APP_DIR && yarn ios (or yarn android)"
|
||||
echo -e " 4) If Metro cache issues: cd $APP_DIR && yarn start:clean\n"
|
||||
|
||||
@@ -146,7 +146,7 @@ function usingHTTPSGitAuth() {
|
||||
}
|
||||
}
|
||||
|
||||
function clonePrivateRepo(repoName, localPath) {
|
||||
function clonePrivateRepo(repoName, localPath, commit) {
|
||||
log(`Setting up ${repoName}...`, 'info');
|
||||
|
||||
let cloneUrl;
|
||||
@@ -181,7 +181,16 @@ function clonePrivateRepo(repoName, localPath) {
|
||||
const isCredentialedUrl = isCI && (appToken || repoToken);
|
||||
const quietFlag = isCredentialedUrl ? '--quiet' : '';
|
||||
const targetDir = path.basename(localPath);
|
||||
const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`;
|
||||
|
||||
// If commit is specified, clone without branch restriction and checkout commit
|
||||
// Otherwise, clone the branch as before
|
||||
let cloneCommand;
|
||||
if (commit) {
|
||||
log(`Using specific commit: ${commit}`, 'info');
|
||||
cloneCommand = `git clone ${quietFlag} "${cloneUrl}" "${targetDir}"`;
|
||||
} else {
|
||||
cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isCredentialedUrl) {
|
||||
@@ -190,6 +199,18 @@ function clonePrivateRepo(repoName, localPath) {
|
||||
} else {
|
||||
runCommand(cloneCommand);
|
||||
}
|
||||
|
||||
// If commit is specified, checkout that commit
|
||||
if (commit) {
|
||||
const checkoutCommand = `cd "${targetDir}" && git checkout ${commit}`;
|
||||
if (isCredentialedUrl) {
|
||||
runCommand(checkoutCommand, { stdio: 'pipe' });
|
||||
} else {
|
||||
runCommand(checkoutCommand);
|
||||
}
|
||||
log(`Checked out commit ${commit}`, 'success');
|
||||
}
|
||||
|
||||
log(`Successfully cloned ${repoName}`, 'success');
|
||||
return true; // Return true to indicate successful clone
|
||||
} catch (error) {
|
||||
@@ -220,14 +241,14 @@ function validateSetup(modulePath, validationFiles, repoName) {
|
||||
}
|
||||
|
||||
function setupPrivateModule(module) {
|
||||
const { repoName, localPath, validationFiles } = module;
|
||||
const { repoName, localPath, validationFiles, commit } = module;
|
||||
log(`Starting setup of ${repoName}...`, 'info');
|
||||
|
||||
// Remove existing module
|
||||
removeExistingModule(localPath, repoName);
|
||||
|
||||
// Clone the private repository
|
||||
const cloneSuccessful = clonePrivateRepo(repoName, localPath);
|
||||
const cloneSuccessful = clonePrivateRepo(repoName, localPath, commit);
|
||||
|
||||
// If clone was skipped (e.g., in forked PRs), exit gracefully
|
||||
if (cloneSuccessful === false) {
|
||||
|
||||
@@ -28,8 +28,29 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const APP_DIR = path.resolve(__dirname, '..');
|
||||
const PACKAGE_JSON_PATH = path.join(APP_DIR, 'package.json');
|
||||
const VERSION_JSON_PATH = path.join(APP_DIR, 'version.json');
|
||||
const VERSION_MANAGED_RELATIVE_PATHS = [
|
||||
'package.json',
|
||||
'version.json',
|
||||
path.join('android', 'app', 'build.gradle'),
|
||||
path.join('ios', 'Self.xcodeproj', 'project.pbxproj'),
|
||||
];
|
||||
const [
|
||||
PACKAGE_JSON_REL_PATH,
|
||||
VERSION_JSON_REL_PATH,
|
||||
ANDROID_GRADLE_REL_PATH,
|
||||
IOS_PBXPROJ_REL_PATH,
|
||||
] = VERSION_MANAGED_RELATIVE_PATHS;
|
||||
const PACKAGE_JSON_PATH = path.join(APP_DIR, PACKAGE_JSON_REL_PATH);
|
||||
const VERSION_JSON_PATH = path.join(APP_DIR, VERSION_JSON_REL_PATH);
|
||||
const ANDROID_GRADLE_PATH = path.join(APP_DIR, ANDROID_GRADLE_REL_PATH);
|
||||
const IOS_PBXPROJ_PATH = path.join(APP_DIR, IOS_PBXPROJ_REL_PATH);
|
||||
|
||||
/**
|
||||
* Get the list of files managed by applyVersions()
|
||||
*/
|
||||
function getVersionManagedFiles() {
|
||||
return [...VERSION_MANAGED_RELATIVE_PATHS];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read package.json
|
||||
@@ -83,6 +104,30 @@ function writeVersionJson(data) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a file with a regex replacement, ensuring at least one match.
|
||||
*/
|
||||
function updateFileWithRegex(filePath, regex, replacement) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found at ${filePath}`);
|
||||
}
|
||||
|
||||
const contents = fs.readFileSync(filePath, 'utf8');
|
||||
const matches = contents.match(regex);
|
||||
|
||||
if (!matches) {
|
||||
throw new Error(`No matches for ${regex} in ${filePath}`);
|
||||
}
|
||||
|
||||
const updated = contents.replace(regex, replacement);
|
||||
|
||||
if (updated !== contents) {
|
||||
fs.writeFileSync(filePath, updated);
|
||||
}
|
||||
|
||||
return matches.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version information
|
||||
*/
|
||||
@@ -202,8 +247,20 @@ function bumpVersion(bumpType, platform = 'both') {
|
||||
|
||||
/**
|
||||
* Apply version changes to files
|
||||
*
|
||||
* @param {string} version - Semantic version (X.Y.Z)
|
||||
* @param {number|string} iosBuild - iOS build number
|
||||
* @param {number|string} androidBuild - Android build number
|
||||
* @param {object} [options] - Optional settings
|
||||
* @param {boolean} [options.iosSuccess=true] - Whether the iOS build succeeded
|
||||
* @param {boolean} [options.androidSuccess=true] - Whether the Android build succeeded
|
||||
*/
|
||||
function applyVersions(version, iosBuild, androidBuild) {
|
||||
function applyVersions(
|
||||
version,
|
||||
iosBuild,
|
||||
androidBuild,
|
||||
{ iosSuccess = true, androidSuccess = true } = {},
|
||||
) {
|
||||
// Validate version format (semver X.Y.Z)
|
||||
if (
|
||||
!version ||
|
||||
@@ -229,8 +286,12 @@ function applyVersions(version, iosBuild, androidBuild) {
|
||||
|
||||
console.log(`📝 Applying versions to files...`);
|
||||
console.log(` Version: ${version}`);
|
||||
console.log(` iOS Build: ${iosNum}`);
|
||||
console.log(` Android Build: ${androidNum}`);
|
||||
console.log(
|
||||
` iOS Build: ${iosNum} (${iosSuccess ? 'succeeded' : 'skipped'})`,
|
||||
);
|
||||
console.log(
|
||||
` Android Build: ${androidNum} (${androidSuccess ? 'succeeded' : 'skipped'})`,
|
||||
);
|
||||
|
||||
// Update package.json
|
||||
const pkg = readPackageJson();
|
||||
@@ -238,12 +299,81 @@ function applyVersions(version, iosBuild, androidBuild) {
|
||||
writePackageJson(pkg);
|
||||
console.log(`✅ Updated package.json`);
|
||||
|
||||
// Update version.json
|
||||
// Update version.json (conditionally per platform)
|
||||
const versionData = readVersionJson();
|
||||
versionData.ios.build = iosNum;
|
||||
versionData.android.build = androidNum;
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
if (iosSuccess) {
|
||||
versionData.ios.build = iosNum;
|
||||
versionData.ios.lastDeployed = timestamp;
|
||||
console.log(
|
||||
`✅ Updated iOS build number to ${iosNum} and lastDeployed timestamp`,
|
||||
);
|
||||
} else {
|
||||
console.log(`⏭️ Skipped iOS version.json update (build did not succeed)`);
|
||||
}
|
||||
|
||||
if (androidSuccess) {
|
||||
versionData.android.build = androidNum;
|
||||
versionData.android.lastDeployed = timestamp;
|
||||
console.log(
|
||||
`✅ Updated Android build number to ${androidNum} and lastDeployed timestamp`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`⏭️ Skipped Android version.json update (build did not succeed)`,
|
||||
);
|
||||
}
|
||||
|
||||
writeVersionJson(versionData);
|
||||
console.log(`✅ Updated version.json`);
|
||||
|
||||
// Update Android build.gradle versionCode
|
||||
if (androidSuccess) {
|
||||
const androidMatches = updateFileWithRegex(
|
||||
ANDROID_GRADLE_PATH,
|
||||
/versionCode\s+\d+/g,
|
||||
`versionCode ${androidNum}`,
|
||||
);
|
||||
console.log(
|
||||
`✅ Updated Android versionCode (${androidMatches} occurrence${
|
||||
androidMatches === 1 ? '' : 's'
|
||||
})`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`⏭️ Skipped Android build.gradle update (build did not succeed)`,
|
||||
);
|
||||
}
|
||||
|
||||
// Update iOS project version and marketing version
|
||||
if (iosSuccess) {
|
||||
const iosBuildMatches = updateFileWithRegex(
|
||||
IOS_PBXPROJ_PATH,
|
||||
/CURRENT_PROJECT_VERSION = \d+;/g,
|
||||
`CURRENT_PROJECT_VERSION = ${iosNum};`,
|
||||
);
|
||||
console.log(
|
||||
`✅ Updated iOS CURRENT_PROJECT_VERSION (${iosBuildMatches} occurrence${
|
||||
iosBuildMatches === 1 ? '' : 's'
|
||||
})`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`⏭️ Skipped iOS CURRENT_PROJECT_VERSION update (build did not succeed)`,
|
||||
);
|
||||
}
|
||||
|
||||
// Always update MARKETING_VERSION to keep it in sync with package.json
|
||||
const iosMarketingMatches = updateFileWithRegex(
|
||||
IOS_PBXPROJ_PATH,
|
||||
/MARKETING_VERSION = \d+\.\d+\.\d+;/g,
|
||||
`MARKETING_VERSION = ${version};`,
|
||||
);
|
||||
console.log(
|
||||
`✅ Updated iOS MARKETING_VERSION (${iosMarketingMatches} occurrence${
|
||||
iosMarketingMatches === 1 ? '' : 's'
|
||||
})`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,20 +425,57 @@ function main() {
|
||||
}
|
||||
|
||||
case 'apply': {
|
||||
// Apply version: apply <version> <iosBuild> <androidBuild>
|
||||
// Apply version: apply <version> <iosBuild> <androidBuild> [iosResult] [androidResult]
|
||||
const version = args[1];
|
||||
const iosBuild = parseInt(args[2], 10);
|
||||
const androidBuild = parseInt(args[3], 10);
|
||||
|
||||
if (!version || isNaN(iosBuild) || isNaN(androidBuild)) {
|
||||
throw new Error('Usage: apply <version> <iosBuild> <androidBuild>');
|
||||
throw new Error(
|
||||
'Usage: apply <version> <iosBuild> <androidBuild> [iosResult] [androidResult]',
|
||||
);
|
||||
}
|
||||
|
||||
applyVersions(version, iosBuild, androidBuild);
|
||||
// Optional platform result args: "success" means succeeded, anything else means skipped
|
||||
const iosSuccess = args[4] ? args[4] === 'success' : true;
|
||||
const androidSuccess = args[5] ? args[5] === 'success' : true;
|
||||
|
||||
applyVersions(version, iosBuild, androidBuild, {
|
||||
iosSuccess,
|
||||
androidSuccess,
|
||||
});
|
||||
console.log(`\n✅ Versions applied successfully`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'files': {
|
||||
// List files managed by applyVersions()
|
||||
const format = args[1];
|
||||
|
||||
if (format && !['--shell', '--json'].includes(format)) {
|
||||
throw new Error('Usage: files [--shell|--json]');
|
||||
}
|
||||
|
||||
const files = getVersionManagedFiles();
|
||||
|
||||
if (format === '--shell') {
|
||||
console.log(files.join(' '));
|
||||
} else if (format === '--json') {
|
||||
console.log(JSON.stringify(files));
|
||||
} else {
|
||||
console.log(files.join('\n'));
|
||||
}
|
||||
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
fs.appendFileSync(
|
||||
process.env.GITHUB_OUTPUT,
|
||||
`version_managed_files=${files.join(' ')}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`
|
||||
Mobile Version Manager
|
||||
@@ -321,13 +488,22 @@ Commands:
|
||||
bump <type> <platform> Bump version and calculate new build numbers
|
||||
type: major|minor|patch|build (default: build)
|
||||
platform: ios|android|both (default: both)
|
||||
apply <version> <ios> <android> Apply specific version and build numbers
|
||||
apply <version> <ios> <android> [iosResult] [androidResult]
|
||||
Apply specific version and build numbers
|
||||
iosResult/androidResult: "success" to update,
|
||||
any other value to skip (default: "success")
|
||||
files [--shell|--json] List files managed by applyVersions()
|
||||
default output: one path per line
|
||||
--shell: space-separated paths
|
||||
--json: JSON array
|
||||
|
||||
Examples:
|
||||
node version-manager.cjs get
|
||||
node version-manager.cjs bump build both
|
||||
node version-manager.cjs bump patch ios
|
||||
node version-manager.cjs apply 2.7.0 180 109
|
||||
node version-manager.cjs apply 2.7.0 180 109 success failure
|
||||
node version-manager.cjs files --shell
|
||||
`);
|
||||
process.exit(command ? 1 : 0);
|
||||
}
|
||||
@@ -346,6 +522,7 @@ if (require.main === module) {
|
||||
module.exports = {
|
||||
applyVersions,
|
||||
bumpVersion,
|
||||
getVersionManagedFiles,
|
||||
getVersionInfo,
|
||||
readPackageJson,
|
||||
readVersionJson,
|
||||
|
||||
@@ -24,6 +24,16 @@ const mockVersionJson = {
|
||||
ios: { build: 100, lastDeployed: '2024-01-01T00:00:00Z' },
|
||||
android: { build: 200, lastDeployed: '2024-01-01T00:00:00Z' },
|
||||
};
|
||||
const mockBuildGradle = `android {
|
||||
defaultConfig {
|
||||
versionCode 200
|
||||
versionName "1.2.3"
|
||||
}
|
||||
}`;
|
||||
const mockPbxproj = `buildSettings = {
|
||||
CURRENT_PROJECT_VERSION = 100;
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
};`;
|
||||
|
||||
// Use manual mocking instead of jest.mock to avoid hoisting issues
|
||||
const fs = require('fs');
|
||||
@@ -43,6 +53,12 @@ function setupMocks() {
|
||||
if (filePath.includes('version.json')) {
|
||||
return JSON.stringify(mockVersionJson);
|
||||
}
|
||||
if (filePath.includes('build.gradle')) {
|
||||
return mockBuildGradle;
|
||||
}
|
||||
if (filePath.includes('project.pbxproj')) {
|
||||
return mockPbxproj;
|
||||
}
|
||||
return originalReadFileSync(filePath, encoding);
|
||||
};
|
||||
|
||||
@@ -255,8 +271,11 @@ describe('version-manager', () => {
|
||||
|
||||
versionManager.applyVersions('2.0.0', 150, 250);
|
||||
|
||||
// Verify writes occurred
|
||||
expect(writeCalls.length).toBe(2);
|
||||
// Verify writes occurred:
|
||||
// 1. package.json, 2. version.json,
|
||||
// 3. build.gradle (versionCode), 4. pbxproj (CURRENT_PROJECT_VERSION),
|
||||
// 5. pbxproj (MARKETING_VERSION)
|
||||
expect(writeCalls.length).toBe(5);
|
||||
|
||||
// Find and verify package.json write
|
||||
const packageWrite = writeCalls.find(call =>
|
||||
@@ -274,6 +293,53 @@ describe('version-manager', () => {
|
||||
const updatedVersion = JSON.parse(versionWrite.content);
|
||||
expect(updatedVersion.ios.build).toBe(150);
|
||||
expect(updatedVersion.android.build).toBe(250);
|
||||
|
||||
// Find and verify build.gradle write
|
||||
const gradleWrite = writeCalls.find(call =>
|
||||
call.filePath.includes('build.gradle'),
|
||||
);
|
||||
expect(gradleWrite).toBeDefined();
|
||||
expect(gradleWrite.content).toContain('versionCode 250');
|
||||
|
||||
// Find and verify pbxproj writes
|
||||
const pbxprojWrites = writeCalls.filter(call =>
|
||||
call.filePath.includes('project.pbxproj'),
|
||||
);
|
||||
expect(pbxprojWrites.length).toBe(2);
|
||||
expect(pbxprojWrites[0].content).toContain(
|
||||
'CURRENT_PROJECT_VERSION = 150;',
|
||||
);
|
||||
expect(pbxprojWrites[1].content).toContain('MARKETING_VERSION = 2.0.0;');
|
||||
|
||||
// Ensure every managed file is touched by applyVersions.
|
||||
// (pbxproj is written twice due to two replacements.)
|
||||
const managedFiles = versionManager.getVersionManagedFiles();
|
||||
for (const managedFile of managedFiles) {
|
||||
const touched = writeCalls.some(call =>
|
||||
call.filePath.includes(managedFile),
|
||||
);
|
||||
expect(touched).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVersionManagedFiles', () => {
|
||||
it('should return the expected managed file paths', () => {
|
||||
expect(versionManager.getVersionManagedFiles()).toEqual([
|
||||
'package.json',
|
||||
'version.json',
|
||||
path.join('android', 'app', 'build.gradle'),
|
||||
path.join('ios', 'Self.xcodeproj', 'project.pbxproj'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a new array each time', () => {
|
||||
const files = versionManager.getVersionManagedFiles();
|
||||
files.push('unexpected-file');
|
||||
|
||||
expect(versionManager.getVersionManagedFiles()).not.toContain(
|
||||
'unexpected-file',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,8 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export const loadMiscAnimation = () =>
|
||||
import('@selfxyz/mobile-sdk-alpha/animations/loading/misc.json');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
Promise.resolve(require('@selfxyz/mobile-sdk-alpha/animations/loading/misc.lottie'));
|
||||
export const loadPassportAnimation = () =>
|
||||
import('@/assets/animations/passport_verify.json');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
Promise.resolve(require('@/assets/animations/passport_verify.lottie'));
|
||||
|
||||
File diff suppressed because one or more lines are too long
BIN
app/src/assets/animations/passport_onboarding.lottie
Normal file
BIN
app/src/assets/animations/passport_onboarding.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
app/src/assets/animations/passport_scan.lottie
Normal file
BIN
app/src/assets/animations/passport_scan.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
app/src/assets/animations/passport_verify.lottie
Normal file
BIN
app/src/assets/animations/passport_verify.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
app/src/assets/animations/proof_failed.lottie
Normal file
BIN
app/src/assets/animations/proof_failed.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
app/src/assets/animations/proof_success.lottie
Normal file
BIN
app/src/assets/animations/proof_success.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
app/src/assets/animations/qr_scan.lottie
Normal file
BIN
app/src/assets/animations/qr_scan.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
app/src/assets/animations/splash.lottie
Normal file
BIN
app/src/assets/animations/splash.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
app/src/assets/animations/warning.lottie
Normal file
BIN
app/src/assets/animations/warning.lottie
Normal file
Binary file not shown.
@@ -2,12 +2,12 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type LottieView from 'lottie-react-native';
|
||||
import React from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Text, View, XStack, YStack } from 'tamagui';
|
||||
|
||||
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
|
||||
import type { DotLottieSource } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { LottieAnimation } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
black,
|
||||
cyan300,
|
||||
@@ -24,7 +24,7 @@ import Plus from '@/assets/icons/plus_slate600.svg';
|
||||
import { extraYPadding } from '@/utils/styleUtils';
|
||||
|
||||
interface LoadingUIProps {
|
||||
animationSource: LottieView['props']['source'];
|
||||
animationSource: DotLottieSource;
|
||||
shouldLoopAnimation: boolean;
|
||||
actionText: string;
|
||||
actionSubText: string;
|
||||
@@ -117,7 +117,7 @@ const LoadingUI: React.FC<LoadingUIProps> = ({
|
||||
elevation={8}
|
||||
>
|
||||
<YStack alignItems="center" paddingHorizontal={10} flex={1}>
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop={shouldLoopAnimation}
|
||||
source={animationSource}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const PointsNavBar = (props: NativeStackHeaderProps) => {
|
||||
color={black}
|
||||
fontSize={15}
|
||||
fontWeight="500"
|
||||
fontFamily="DIN OT"
|
||||
fontFamily="DINOT-Medium"
|
||||
textAlign="center"
|
||||
style={{
|
||||
letterSpacing: 0.6,
|
||||
|
||||
@@ -93,6 +93,7 @@ const scanAndroid = async (
|
||||
useCan: inputs.useCan ?? false,
|
||||
sessionId: inputs.sessionId,
|
||||
skipReselect: inputs.skipReselect ?? false,
|
||||
skipPACE: inputs.skipPACE ?? false,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ if (Platform.OS === 'android') {
|
||||
useCan = false,
|
||||
quality = 1,
|
||||
skipReselect = false,
|
||||
skipPACE = false,
|
||||
sessionId,
|
||||
} = options;
|
||||
|
||||
@@ -104,6 +105,7 @@ if (Platform.OS === 'android') {
|
||||
useCan,
|
||||
quality,
|
||||
skipReselect,
|
||||
skipPACE,
|
||||
sessionId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,8 +15,7 @@ import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { X } from '@tamagui/lucide-icons';
|
||||
|
||||
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
|
||||
import youWinAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/youWin.json';
|
||||
import { LottieAnimation } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { PrimaryButton } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import {
|
||||
black,
|
||||
@@ -30,6 +29,9 @@ import SelfLogo from '@/assets/logos/self.svg';
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const youWinAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/youWin.lottie');
|
||||
|
||||
const GratificationScreen: React.FC = () => {
|
||||
const { top, bottom } = useSafeAreaInsets();
|
||||
const navigation =
|
||||
@@ -66,7 +68,7 @@ const GratificationScreen: React.FC = () => {
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop={false}
|
||||
source={youWinAnimation}
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type LottieView from 'lottie-react-native';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import type { StaticScreenProps } from '@react-navigation/native';
|
||||
import { useFocusEffect, useIsFocused } from '@react-navigation/native';
|
||||
|
||||
import type { DocumentCategory } from '@selfxyz/common/utils/types';
|
||||
import type { ProvingStateType } from '@selfxyz/mobile-sdk-alpha';
|
||||
import type {
|
||||
DotLottieSource,
|
||||
ProvingStateType,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
advercase,
|
||||
dinot,
|
||||
loadSelectedDocument,
|
||||
useSelfClient,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import failAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/fail.json';
|
||||
import proveLoadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/prove.json';
|
||||
import {
|
||||
black,
|
||||
slate400,
|
||||
@@ -31,6 +31,11 @@ import { getLoadingScreenText } from '@/proving/loadingScreenStateText';
|
||||
import { setupNotifications } from '@/services/notifications/notificationService';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports -- binary assets loaded by Metro */
|
||||
const failAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/fail.lottie');
|
||||
const proveLoadingAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/prove.lottie');
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
type LoadingScreenParams = {
|
||||
documentCategory?: DocumentCategory;
|
||||
signatureAlgorithm?: string;
|
||||
@@ -38,7 +43,6 @@ type LoadingScreenParams = {
|
||||
};
|
||||
|
||||
type LoadingScreenProps = StaticScreenProps<LoadingScreenParams>;
|
||||
|
||||
// Define all terminal states that should stop animations and haptics
|
||||
const terminalStates: ProvingStateType[] = [
|
||||
'completed',
|
||||
@@ -55,9 +59,9 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({ route }) => {
|
||||
const [isInitializing, setIsInitializing] = useState(false);
|
||||
|
||||
// Animation states
|
||||
const [animationSource, setAnimationSource] = useState<
|
||||
LottieView['props']['source']
|
||||
>(proveLoadingAnimation);
|
||||
const [animationSource, setAnimationSource] = useState<DotLottieSource>(
|
||||
proveLoadingAnimation,
|
||||
);
|
||||
|
||||
// Loading text state
|
||||
const [loadingText, setLoadingText] = useState<{
|
||||
|
||||
@@ -8,13 +8,12 @@ import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import {
|
||||
DelayedLottieView,
|
||||
hasAnyValidRegisteredDocument,
|
||||
LottieAnimation,
|
||||
useSelfClient,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import { black } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import splashAnimation from '@/assets/animations/splash.json';
|
||||
import { impactLight } from '@/integrations/haptics';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import {
|
||||
@@ -32,6 +31,9 @@ import {
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { IS_DEV_MODE } from '@/utils/devUtils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const splashAnimation = require('@/assets/animations/splash.lottie');
|
||||
|
||||
const SplashScreen: React.FC = ({}) => {
|
||||
const selfClient = useSelfClient();
|
||||
const navigation =
|
||||
@@ -104,6 +106,16 @@ const SplashScreen: React.FC = ({}) => {
|
||||
}
|
||||
}, [checkBiometricsAvailable, setBiometricsAvailable, selfClient]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setIsAnimationFinished(prev => {
|
||||
if (!prev) console.warn('SplashScreen: animation timeout, proceeding');
|
||||
return true;
|
||||
});
|
||||
}, 5000);
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
const handleAnimationFinish = useCallback(() => {
|
||||
impactLight();
|
||||
setIsAnimationFinished(true);
|
||||
@@ -124,7 +136,7 @@ const SplashScreen: React.FC = ({}) => {
|
||||
}, [isAnimationFinished, nextScreen, queuedDeepLink, navigation, selfClient]);
|
||||
|
||||
return (
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop={false}
|
||||
source={splashAnimation}
|
||||
|
||||
@@ -2,23 +2,26 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type LottieView from 'lottie-react-native';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Adapt, Button, Select, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { Check, ChevronDown } from '@tamagui/lucide-icons';
|
||||
|
||||
import type {
|
||||
DotLottieSource,
|
||||
provingMachineCircuitType,
|
||||
ProvingStateType,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import failAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/fail.json';
|
||||
import proveLoadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/prove.json';
|
||||
import { slate200, slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import LoadingUI from '@/components/LoadingUI';
|
||||
import { getLoadingScreenText } from '@/proving/loadingScreenStateText';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports -- binary assets loaded by Metro */
|
||||
const failAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/fail.lottie');
|
||||
const proveLoadingAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/prove.lottie');
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
const allProvingStates = [
|
||||
'idle',
|
||||
'parsing_id_document',
|
||||
@@ -41,9 +44,9 @@ const DevLoadingScreen: React.FC = () => {
|
||||
const [currentState, setCurrentState] = useState<ProvingStateType>('idle');
|
||||
const [documentType, setDocumentType] =
|
||||
useState<provingMachineCircuitType>('dsc');
|
||||
const [animationSource, setAnimationSource] = useState<
|
||||
LottieView['props']['source']
|
||||
>(proveLoadingAnimation);
|
||||
const [animationSource, setAnimationSource] = useState<DotLottieSource>(
|
||||
proveLoadingAnimation,
|
||||
);
|
||||
const [loadingText, setLoadingText] = useState<{
|
||||
actionText: string;
|
||||
actionSubText: string;
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useIsFocused, useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import {
|
||||
DelayedLottieView,
|
||||
dinot,
|
||||
LottieAnimation,
|
||||
useSelfClient,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
useReadMRZ,
|
||||
} from '@selfxyz/mobile-sdk-alpha/onboarding/read-mrz';
|
||||
|
||||
import passportScanAnimation from '@/assets/animations/passport_scan.json';
|
||||
import Scan from '@/assets/icons/passport_camera_scan.svg';
|
||||
import { PassportCamera } from '@/components/native/PassportCamera';
|
||||
import { useErrorInjection } from '@/hooks/useErrorInjection';
|
||||
@@ -40,6 +39,9 @@ import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { getDocumentScanPrompt } from '@/utils/documentAttributes';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const passportScanAnimation = require('@/assets/animations/passport_scan.lottie');
|
||||
|
||||
const DocumentCameraScreen: React.FC = () => {
|
||||
const isFocused = useIsFocused();
|
||||
const navigation =
|
||||
@@ -87,7 +89,7 @@ const DocumentCameraScreen: React.FC = () => {
|
||||
<ExpandableBottomLayout.Layout backgroundColor={white}>
|
||||
<ExpandableBottomLayout.TopSection roundTop backgroundColor={black}>
|
||||
<PassportCamera onPassportRead={onPassportRead} isMounted={isFocused} />
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop
|
||||
source={passportScanAnimation}
|
||||
|
||||
@@ -54,16 +54,6 @@ const NFC_METHODS = [
|
||||
platform: ['ios'],
|
||||
params: { usePacePolling: true },
|
||||
},
|
||||
{
|
||||
// We try PACE first, but if it fails, we try BAC authentication.
|
||||
// Some chips will invalidate the session if PACE fails.
|
||||
key: 'skipPACE',
|
||||
label: 'Skip PACE',
|
||||
description:
|
||||
'Skip PACE protocol during NFC scan. Useful if your passport does not support PACE.',
|
||||
platform: ['ios'],
|
||||
params: { skipPACE: true },
|
||||
},
|
||||
{
|
||||
key: 'can',
|
||||
label: 'CAN Authentication',
|
||||
@@ -107,6 +97,7 @@ const DocumentNFCMethodSelectionScreen: React.FC = () => {
|
||||
const [selectedMethod, setSelectedMethod] = useState('standard');
|
||||
const [canValue, setCanValue] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [skipPACE, setSkipPACE] = useState(false);
|
||||
|
||||
const selfClient = useSelfClient();
|
||||
const { useMRZStore } = selfClient;
|
||||
@@ -147,6 +138,10 @@ const DocumentNFCMethodSelectionScreen: React.FC = () => {
|
||||
if (selectedMethod === 'can') {
|
||||
params.canNumber = canValue;
|
||||
}
|
||||
|
||||
if (skipPACE) {
|
||||
params.skipPACE = true;
|
||||
}
|
||||
// Type assertion needed because static navigation doesn't infer optional params
|
||||
navigation.navigate('DocumentNFCScan', params as never);
|
||||
};
|
||||
@@ -158,6 +153,28 @@ const DocumentNFCMethodSelectionScreen: React.FC = () => {
|
||||
<YStack paddingTop={20} gap={20}>
|
||||
<Title>Choose NFC Scan Method</Title>
|
||||
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderWidth={1}
|
||||
borderColor="#ccc"
|
||||
borderRadius={10}
|
||||
backgroundColor="#fff"
|
||||
>
|
||||
<Description>Skip PACE</Description>
|
||||
<Switch
|
||||
size="$4"
|
||||
checked={skipPACE}
|
||||
onCheckedChange={setSkipPACE}
|
||||
backgroundColor={skipPACE ? '$green7Light' : '$gray4'}
|
||||
style={{ minWidth: 48, minHeight: 36 }}
|
||||
>
|
||||
<Switch.Thumb animation="quick" backgroundColor="$white" />
|
||||
</Switch>
|
||||
</XStack>
|
||||
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -22,6 +21,7 @@ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import NfcManager from 'react-native-nfc-manager';
|
||||
import { Button, Image, XStack } from 'tamagui';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Dotlottie } from '@lottiefiles/dotlottie-react-native';
|
||||
import type { RouteProp } from '@react-navigation/native';
|
||||
import {
|
||||
useFocusEffect,
|
||||
@@ -32,7 +32,11 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { CircleHelp } from '@tamagui/lucide-icons';
|
||||
|
||||
import type { PassportData } from '@selfxyz/common/types';
|
||||
import { sanitizeErrorMessage, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
LottieAnimation,
|
||||
sanitizeErrorMessage,
|
||||
useSelfClient,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
BodyText,
|
||||
ButtonsContainer,
|
||||
@@ -51,7 +55,6 @@ import {
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import passportVerifyAnimation from '@/assets/animations/passport_verify.json';
|
||||
import NFC_IMAGE from '@/assets/images/nfc.png';
|
||||
import { logNFCEvent } from '@/config/sentry';
|
||||
import { useErrorInjection } from '@/hooks/useErrorInjection';
|
||||
@@ -100,6 +103,9 @@ type DocumentNFCScanRoute = RouteProp<
|
||||
string
|
||||
>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const passportVerifyAnimation = require('@/assets/animations/passport_verify.lottie');
|
||||
|
||||
const DocumentNFCScanScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
const { trackEvent, useMRZStore } = selfClient;
|
||||
@@ -136,7 +142,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
[route.params?.useCan],
|
||||
);
|
||||
|
||||
const animationRef = useRef<LottieView>(null);
|
||||
const animationRef = useRef<Dotlottie | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
animationRef.current?.play();
|
||||
@@ -541,7 +547,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection roundTop backgroundColor={slate100}>
|
||||
<LottieView
|
||||
<LottieAnimation
|
||||
ref={animationRef}
|
||||
autoPlay={false}
|
||||
loop={false}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import type { Dotlottie } from '@lottiefiles/dotlottie-react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { LottieAnimation, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
Additional,
|
||||
ButtonsContainer,
|
||||
@@ -24,12 +24,14 @@ import {
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import passportOnboardingAnimation from '@/assets/animations/passport_onboarding.json';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import { impactLight } from '@/integrations/haptics';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import { getDocumentScanPrompt } from '@/utils/documentAttributes';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const passportOnboardingAnimation = require('@/assets/animations/passport_onboarding.lottie');
|
||||
|
||||
const DocumentOnboardingScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const selfClient = useSelfClient();
|
||||
@@ -37,7 +39,7 @@ const DocumentOnboardingScreen: React.FC = () => {
|
||||
state => state.documentType,
|
||||
);
|
||||
const handleCameraPress = useHapticNavigation('DocumentCamera');
|
||||
const animationRef = useRef<LottieView>(null);
|
||||
const animationRef = useRef<Dotlottie | null>(null);
|
||||
|
||||
const scanPrompt = getDocumentScanPrompt(selectedDocumentType);
|
||||
|
||||
@@ -58,7 +60,7 @@ const DocumentOnboardingScreen: React.FC = () => {
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection roundTop backgroundColor={black}>
|
||||
<LottieView
|
||||
<LottieAnimation
|
||||
ref={animationRef}
|
||||
autoPlay={false}
|
||||
loop={false}
|
||||
|
||||
@@ -241,7 +241,12 @@ const HomeScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<YStack backgroundColor={'#F8FAFC'} flex={1} alignItems="center">
|
||||
<YStack
|
||||
backgroundColor={'#F8FAFC'}
|
||||
flex={1}
|
||||
alignItems="center"
|
||||
testID="home-screen-root"
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
flex={1}
|
||||
|
||||
@@ -11,8 +11,7 @@ import type { StaticScreenProps } from '@react-navigation/native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { DelayedLottieView, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
|
||||
import { LottieAnimation, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
AbstractButton,
|
||||
Description,
|
||||
@@ -32,6 +31,9 @@ import {
|
||||
} from '@/services/notifications/notificationService';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const loadingAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/misc.lottie');
|
||||
|
||||
type KycSuccessRouteParams = StaticScreenProps<
|
||||
| {
|
||||
userId?: string;
|
||||
@@ -112,7 +114,7 @@ const KycSuccessScreen: React.FC<KycSuccessRouteParams> = ({
|
||||
<View style={[styles.container, { paddingBottom: insets.bottom }]}>
|
||||
<View style={styles.centerSection}>
|
||||
<View style={styles.animationContainer}>
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop={true}
|
||||
source={loadingAnimation}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { LottieAnimation } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
Description,
|
||||
PrimaryButton,
|
||||
@@ -16,12 +16,14 @@ import {
|
||||
import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
|
||||
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import proofSuccessAnimation from '@/assets/animations/proof_success.json';
|
||||
import { buttonTap } from '@/integrations/haptics';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { styles } from '@/screens/verification/ProofRequestStatusScreen';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const proofSuccessAnimation = require('@/assets/animations/proof_success.lottie');
|
||||
|
||||
const AccountVerifiedSuccessScreen: React.FC = ({}) => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
@@ -29,7 +31,7 @@ const AccountVerifiedSuccessScreen: React.FC = ({}) => {
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={white}>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={black} roundTop>
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop={false}
|
||||
source={proofSuccessAnimation}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { LottieAnimation } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
Caution,
|
||||
PrimaryButton,
|
||||
@@ -17,12 +17,14 @@ import {
|
||||
import { AppEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
|
||||
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import warningAnimation from '@/assets/animations/warning.json';
|
||||
import { confirmTap, notificationWarning } from '@/integrations/haptics';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const warningAnimation = require('@/assets/animations/warning.lottie');
|
||||
|
||||
const DisclaimerScreen: React.FC = () => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
@@ -35,7 +37,7 @@ const DisclaimerScreen: React.FC = () => {
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={black}>
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop={false}
|
||||
source={warningAnimation}
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { LottieViewProps } from 'lottie-react-native';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Linking, StyleSheet, View } from 'react-native';
|
||||
import { ScrollView, Spinner } from 'tamagui';
|
||||
import { useIsFocused, useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
|
||||
import type { DotLottieSource } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { LottieAnimation, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
BodyText,
|
||||
Description,
|
||||
@@ -22,8 +20,6 @@ import {
|
||||
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
|
||||
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import failAnimation from '@/assets/animations/proof_failed.json';
|
||||
import succesAnimation from '@/assets/animations/proof_success.json';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import {
|
||||
buttonTap,
|
||||
@@ -36,6 +32,13 @@ import { getWhiteListedDisclosureAddresses } from '@/services/points/utils';
|
||||
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
|
||||
import { ProofStatus } from '@/stores/proofTypes';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports -- binary assets loaded by Metro */
|
||||
const loadingAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/misc.lottie');
|
||||
|
||||
const failAnimation = require('@/assets/animations/proof_failed.lottie');
|
||||
const succesAnimation = require('@/assets/animations/proof_success.lottie');
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
const SuccessScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
const { trackEvent } = selfClient;
|
||||
@@ -56,7 +59,7 @@ const SuccessScreen: React.FC = () => {
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const [animationSource, setAnimationSource] =
|
||||
useState<LottieViewProps['source']>(loadingAnimation);
|
||||
useState<DotLottieSource>(loadingAnimation);
|
||||
const [countdown, setCountdown] = useState<number | null>(null);
|
||||
const [countdownStarted, setCountdownStarted] = useState(false);
|
||||
const [whitelistedPoints, setWhitelistedPoints] = useState<number | null>(
|
||||
@@ -214,7 +217,7 @@ const SuccessScreen: React.FC = () => {
|
||||
marginTop={20}
|
||||
backgroundColor={black}
|
||||
>
|
||||
<LottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop={animationSource === loadingAnimation}
|
||||
source={animationSource}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
} from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { LottieAnimation, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
Additional,
|
||||
Description,
|
||||
@@ -27,7 +26,6 @@ import {
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import qrScanAnimation from '@/assets/animations/qr_scan.json';
|
||||
import QRScan from '@/assets/icons/qr_code.svg';
|
||||
import type { QRCodeScannerViewProps } from '@/components/native/QRCodeScanner';
|
||||
import { QRCodeScannerView } from '@/components/native/QRCodeScanner';
|
||||
@@ -39,6 +37,9 @@ import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { parseAndValidateUrlParams } from '@/navigation/deeplinks';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const qrScanAnimation = require('@/assets/animations/qr_scan.lottie');
|
||||
|
||||
const QRCodeViewFinderScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
const { trackEvent } = selfClient;
|
||||
@@ -164,7 +165,7 @@ const QRCodeViewFinderScreen: React.FC = () => {
|
||||
{shouldRenderCamera && (
|
||||
<>
|
||||
<QRCodeScannerView onQRData={onQRData} isMounted={isFocused} />
|
||||
<LottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop
|
||||
source={qrScanAnimation}
|
||||
|
||||
@@ -3,5 +3,5 @@ appId: com.proofofpassportapp
|
||||
- launchApp
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "earn-points-button"
|
||||
timeout: ${LAUNCH_WAIT_MS:60000}
|
||||
id: "home-screen-root"
|
||||
timeout: ${LAUNCH_WAIT_MS:120000}
|
||||
|
||||
@@ -3,5 +3,5 @@ appId: com.warroom.proofofpassport
|
||||
- launchApp
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "earn-points-button"
|
||||
timeout: ${LAUNCH_WAIT_MS:60000}
|
||||
id: "home-screen-root"
|
||||
timeout: ${LAUNCH_WAIT_MS:120000}
|
||||
|
||||
@@ -82,7 +82,7 @@ jest.mock('tamagui', () => {
|
||||
});
|
||||
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
|
||||
DelayedLottieView: ({ onAnimationFinish }: any) => {
|
||||
LottieAnimation: ({ onAnimationFinish }: any) => {
|
||||
// Simulate animation finishing immediately
|
||||
setTimeout(() => {
|
||||
onAnimationFinish?.();
|
||||
|
||||
@@ -102,7 +102,7 @@ jest.mock('@selfxyz/mobile-sdk-alpha/constants/analytics', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha/animations/loading/misc.json', () => ({}));
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha/animations/loading/misc.lottie', () => 1);
|
||||
|
||||
jest.mock('@/integrations/haptics', () => ({
|
||||
buttonTap: jest.fn(),
|
||||
@@ -125,7 +125,7 @@ jest.mock('@/services/analytics', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
|
||||
DelayedLottieView: () => null,
|
||||
LottieAnimation: () => null,
|
||||
useSelfClient: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"ios": {
|
||||
"build": 215,
|
||||
"lastDeployed": "2026-02-16T09:17:41.176Z"
|
||||
"build": 216,
|
||||
"lastDeployed": "2026-02-20T11:17:57.338Z"
|
||||
},
|
||||
"android": {
|
||||
"build": 144,
|
||||
"lastDeployed": "2026-02-16T09:17:41.176Z"
|
||||
"build": 145,
|
||||
"lastDeployed": "2026-02-20T11:17:57.338Z"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export default defineConfig({
|
||||
root: 'web',
|
||||
publicDir: 'web',
|
||||
envDir: '..', // This is the directory where Vite will look for .env files relative to the root
|
||||
assetsInclude: ['**/*.lottie'],
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.web.tsx',
|
||||
@@ -34,7 +35,7 @@ export default defineConfig({
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'@/package.json': resolve(__dirname, 'package.json'),
|
||||
'react-native-svg': 'react-native-svg-web',
|
||||
'lottie-react-native': 'lottie-react',
|
||||
'@lottiefiles/dotlottie-react-native': '@lottiefiles/dotlottie-react',
|
||||
'@react-native-community/blur': resolve(
|
||||
__dirname,
|
||||
'src/devtools/mocks/react-native-community-blur.ts',
|
||||
@@ -164,7 +165,10 @@ export default defineConfig({
|
||||
'vendor-analytics-sentry': ['@sentry/react', '@sentry/react-native'],
|
||||
|
||||
// Animations
|
||||
'vendor-animations-lottie': ['lottie-react-native', 'lottie-react'],
|
||||
'vendor-animations-lottie': [
|
||||
'@lottiefiles/dotlottie-react-native',
|
||||
'@lottiefiles/dotlottie-react',
|
||||
],
|
||||
|
||||
// WebSocket and Socket.IO
|
||||
'vendor-websocket': ['socket.io-client'],
|
||||
@@ -192,7 +196,7 @@ export default defineConfig({
|
||||
|
||||
// Large animations - split out heavy Lottie files
|
||||
'animations-passport-onboarding': [
|
||||
'./src/assets/animations/passport_onboarding.json',
|
||||
'./src/assets/animations/passport_onboarding.lottie',
|
||||
],
|
||||
|
||||
// Other screens
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
- Install nargo via noirup (pin the version used in CI for reproducibility):
|
||||
- curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
|
||||
- noirup -v <noir_version> # e.g., noirup -v v0.30.0
|
||||
- noirup -v <noir_version> # e.g., noirup -v v0.31.0 (>= 0.31.0 required for --package flag)
|
||||
- Verify nargo is on the expected version:
|
||||
- nargo --version
|
||||
- Ensure Rust toolchain is installed and up to date (required by nargo).
|
||||
|
||||
@@ -108,8 +108,8 @@ actual class SelfSdk private constructor(
|
||||
) {
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
// Success
|
||||
val resultDataJson = data?.getStringExtra(SelfVerificationActivity.EXTRA_RESULT_DATA)
|
||||
val resultType = data?.getStringExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE)
|
||||
if (resultDataJson != null) {
|
||||
try {
|
||||
val result = deserializeResult(resultDataJson)
|
||||
@@ -122,6 +122,10 @@ actual class SelfSdk private constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
} else if (resultType != null) {
|
||||
callback.onSuccess(
|
||||
VerificationResult(success = true, type = resultType),
|
||||
)
|
||||
} else {
|
||||
callback.onFailure(
|
||||
SelfSdkError(
|
||||
|
||||
@@ -11,6 +11,7 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
import xyz.self.sdk.bridge.BridgeDomain
|
||||
import xyz.self.sdk.bridge.BridgeHandler
|
||||
import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
import xyz.self.sdk.webview.SelfVerificationActivity
|
||||
|
||||
/**
|
||||
* Android implementation of lifecycle bridge handler.
|
||||
@@ -72,16 +73,16 @@ class LifecycleBridgeHandler(
|
||||
|
||||
if (type != null) {
|
||||
// Flat lifecycle payload (e.g. { type: "proofRequested" }) — treat as success
|
||||
intent.putExtra("xyz.self.sdk.RESULT_TYPE", type)
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE, type)
|
||||
activity.setResult(Activity.RESULT_OK, intent)
|
||||
} else if (success && data != null) {
|
||||
// Success result
|
||||
intent.putExtra("xyz.self.sdk.RESULT_DATA", data)
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_DATA, data)
|
||||
activity.setResult(Activity.RESULT_OK, intent)
|
||||
} else if (!success && errorCode != null) {
|
||||
// Error result
|
||||
intent.putExtra("xyz.self.sdk.ERROR_CODE", errorCode)
|
||||
intent.putExtra("xyz.self.sdk.ERROR_MESSAGE", errorMessage ?: "Unknown error")
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_CODE, errorCode)
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_MESSAGE, errorMessage ?: "Unknown error")
|
||||
activity.setResult(Activity.RESULT_FIRST_USER, intent)
|
||||
} else {
|
||||
// Cancelled or invalid result
|
||||
|
||||
@@ -121,6 +121,7 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
|
||||
// Result extras
|
||||
const val EXTRA_RESULT_DATA = "xyz.self.sdk.RESULT_DATA"
|
||||
const val EXTRA_RESULT_TYPE = "xyz.self.sdk.RESULT_TYPE"
|
||||
const val EXTRA_ERROR_CODE = "xyz.self.sdk.ERROR_CODE"
|
||||
const val EXTRA_ERROR_MESSAGE = "xyz.self.sdk.ERROR_MESSAGE"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class VerificationResult(
|
||||
val success: Boolean,
|
||||
val type: String? = null,
|
||||
val userId: String? = null,
|
||||
val verificationId: String? = null,
|
||||
val proof: String? = null,
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
package xyz.self.sdk.api
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import platform.UIKit.UIApplication
|
||||
import platform.UIKit.UIModalPresentationFullScreen
|
||||
import platform.UIKit.UIViewController
|
||||
import platform.UIKit.UIWindow
|
||||
import platform.UIKit.UIWindowScene
|
||||
import platform.darwin.dispatch_async
|
||||
import platform.darwin.dispatch_get_main_queue
|
||||
import xyz.self.sdk.bridge.MessageRouter
|
||||
import xyz.self.sdk.handlers.AnalyticsBridgeHandler
|
||||
import xyz.self.sdk.handlers.BiometricBridgeHandler
|
||||
@@ -72,9 +75,24 @@ actual class SelfSdk private constructor(
|
||||
|
||||
// Create lifecycle handler with callback and dismiss wiring
|
||||
val lifecycleHandler = LifecycleBridgeHandler()
|
||||
lifecycleHandler.pendingCallback = callback
|
||||
lifecycleHandler.dismissAction = {
|
||||
pendingCallback = null
|
||||
var dismissViewController: UIViewController? = null
|
||||
runBlocking {
|
||||
lifecycleHandler.configure(
|
||||
callback = callback,
|
||||
dismiss = {
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
val viewController = dismissViewController
|
||||
if (viewController == null) {
|
||||
pendingCallback = null
|
||||
return@dispatch_async
|
||||
}
|
||||
|
||||
viewController.dismissViewControllerAnimated(true) {
|
||||
pendingCallback = null
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Register all iOS bridge handlers
|
||||
@@ -91,13 +109,7 @@ actual class SelfSdk private constructor(
|
||||
?: throw IllegalStateException("WebView provider not configured. Call SelfSdkSwift.configure() first.")
|
||||
).getViewController()
|
||||
sdkVC.setModalPresentationStyle(UIModalPresentationFullScreen)
|
||||
|
||||
// Wire up dismiss action to dismiss the VC
|
||||
lifecycleHandler.dismissAction = {
|
||||
sdkVC.dismissViewControllerAnimated(true) {
|
||||
pendingCallback = null
|
||||
}
|
||||
}
|
||||
dismissViewController = sdkVC
|
||||
|
||||
val topVC = findTopViewController()
|
||||
if (topVC == null) {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
package xyz.self.sdk.handlers
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -17,8 +19,24 @@ import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
class LifecycleBridgeHandler : BridgeHandler {
|
||||
override val domain = BridgeDomain.LIFECYCLE
|
||||
|
||||
internal var pendingCallback: SelfSdkCallback? = null
|
||||
internal var dismissAction: (() -> Unit)? = null
|
||||
private val mutex = Mutex()
|
||||
private var pendingCallback: SelfSdkCallback? = null
|
||||
private var dismissAction: (() -> Unit)? = null
|
||||
|
||||
private data class LifecycleState(
|
||||
val callback: SelfSdkCallback?,
|
||||
val dismiss: (() -> Unit)?,
|
||||
)
|
||||
|
||||
internal suspend fun configure(
|
||||
callback: SelfSdkCallback?,
|
||||
dismiss: (() -> Unit)?,
|
||||
) {
|
||||
mutex.withLock {
|
||||
pendingCallback = callback
|
||||
dismissAction = dismiss
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handle(
|
||||
method: String,
|
||||
@@ -36,14 +54,27 @@ class LifecycleBridgeHandler : BridgeHandler {
|
||||
|
||||
private fun ready(): JsonElement? = null
|
||||
|
||||
private fun dismiss(): JsonElement? {
|
||||
pendingCallback?.onCancelled()
|
||||
pendingCallback = null
|
||||
dismissAction?.invoke()
|
||||
private suspend fun consumeLifecycleState(): LifecycleState =
|
||||
mutex.withLock {
|
||||
val state =
|
||||
LifecycleState(
|
||||
callback = pendingCallback,
|
||||
dismiss = dismissAction,
|
||||
)
|
||||
pendingCallback = null
|
||||
dismissAction = null
|
||||
state
|
||||
}
|
||||
|
||||
private suspend fun dismiss(): JsonElement? {
|
||||
val state = consumeLifecycleState()
|
||||
state.callback?.onCancelled()
|
||||
state.dismiss?.invoke()
|
||||
return null
|
||||
}
|
||||
|
||||
private fun setResult(params: Map<String, JsonElement>): JsonElement? {
|
||||
private suspend fun setResult(params: Map<String, JsonElement>): JsonElement? {
|
||||
val state = consumeLifecycleState()
|
||||
val type = params["type"]?.jsonPrimitive?.content
|
||||
val success = params["success"]?.jsonPrimitive?.content?.toBoolean() ?: false
|
||||
val data = params["data"]?.toString()
|
||||
@@ -51,16 +82,17 @@ class LifecycleBridgeHandler : BridgeHandler {
|
||||
val errorMessage = params["errorMessage"]?.jsonPrimitive?.content
|
||||
|
||||
if (type != null) {
|
||||
// Flat lifecycle payload (e.g. { type: "proofRequested" }) — treat as success
|
||||
pendingCallback?.onSuccess(
|
||||
VerificationResult(success = true),
|
||||
// Flat lifecycle payload is a protocol-level success signal.
|
||||
// `type` communicates what completed (e.g. proofRequested).
|
||||
state.callback?.onSuccess(
|
||||
VerificationResult(success = true, type = type),
|
||||
)
|
||||
} else if (success && data != null) {
|
||||
try {
|
||||
val result = Json.decodeFromString(VerificationResult.serializer(), data)
|
||||
pendingCallback?.onSuccess(result)
|
||||
state.callback?.onSuccess(result)
|
||||
} catch (e: Exception) {
|
||||
pendingCallback?.onFailure(
|
||||
state.callback?.onFailure(
|
||||
SelfSdkError(
|
||||
code = "PARSE_ERROR",
|
||||
message = "Failed to parse verification result: ${e.message}",
|
||||
@@ -68,18 +100,17 @@ class LifecycleBridgeHandler : BridgeHandler {
|
||||
)
|
||||
}
|
||||
} else if (!success && errorCode != null) {
|
||||
pendingCallback?.onFailure(
|
||||
state.callback?.onFailure(
|
||||
SelfSdkError(
|
||||
code = errorCode,
|
||||
message = errorMessage ?: "Unknown error",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
pendingCallback?.onCancelled()
|
||||
state.callback?.onCancelled()
|
||||
}
|
||||
|
||||
pendingCallback = null
|
||||
dismissAction?.invoke()
|
||||
state.dismiss?.invoke()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,11 +98,21 @@
|
||||
"import": "./dist/svgs/icons/*.svg",
|
||||
"require": "./dist/svgs/icons/*.svg"
|
||||
},
|
||||
"./animations/*.lottie": {
|
||||
"react-native": "./dist/animations/*.lottie",
|
||||
"import": "./dist/animations/*.lottie",
|
||||
"require": "./dist/animations/*.lottie"
|
||||
},
|
||||
"./animations/*.json": {
|
||||
"react-native": "./dist/animations/*.json",
|
||||
"import": "./dist/animations/*.json",
|
||||
"require": "./dist/animations/*.json"
|
||||
},
|
||||
"./animations/loading/*.lottie": {
|
||||
"react-native": "./dist/animations/loading/*.lottie",
|
||||
"import": "./dist/animations/loading/*.lottie",
|
||||
"require": "./dist/animations/loading/*.lottie"
|
||||
},
|
||||
"./animations/loading/*.json": {
|
||||
"react-native": "./dist/animations/loading/*.json",
|
||||
"import": "./dist/animations/loading/*.json",
|
||||
@@ -170,6 +180,7 @@
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lottiefiles/dotlottie-react-native": "0.5.0",
|
||||
"@openpassport/zk-kit-lean-imt": "^0.0.6",
|
||||
"@react-native-async-storage/async-storage": "^2.1.2",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
@@ -187,7 +198,6 @@
|
||||
"eslint-plugin-sort-exports": "^0.9.1",
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"jsdom": "^25.0.1",
|
||||
"lottie-react-native": "7.2.2",
|
||||
"poseidon-lite": "^0.3.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^18.3.1",
|
||||
@@ -206,8 +216,8 @@
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@lottiefiles/dotlottie-react-native": "*",
|
||||
"@react-native-async-storage/async-storage": ">=1.0.0",
|
||||
"lottie-react-native": "7.2.2",
|
||||
"react": "^18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-blur-effect": "^1.1.3",
|
||||
|
||||
File diff suppressed because one or more lines are too long
BIN
packages/mobile-sdk-alpha/src/animations/loading/fail.lottie
Normal file
BIN
packages/mobile-sdk-alpha/src/animations/loading/fail.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
packages/mobile-sdk-alpha/src/animations/loading/misc.lottie
Normal file
BIN
packages/mobile-sdk-alpha/src/animations/loading/misc.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
packages/mobile-sdk-alpha/src/animations/loading/prove.lottie
Normal file
BIN
packages/mobile-sdk-alpha/src/animations/loading/prove.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
packages/mobile-sdk-alpha/src/animations/loading/success.lottie
Normal file
BIN
packages/mobile-sdk-alpha/src/animations/loading/success.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
packages/mobile-sdk-alpha/src/animations/loading/youWin.lottie
Normal file
BIN
packages/mobile-sdk-alpha/src/animations/loading/youWin.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
packages/mobile-sdk-alpha/src/animations/passport_scan.lottie
Normal file
BIN
packages/mobile-sdk-alpha/src/animations/passport_scan.lottie
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
packages/mobile-sdk-alpha/src/animations/passport_verify.lottie
Normal file
BIN
packages/mobile-sdk-alpha/src/animations/passport_verify.lottie
Normal file
Binary file not shown.
@@ -1,50 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { LottieViewProps } from 'lottie-react-native';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import type React from 'react';
|
||||
import { forwardRef, useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Wrapper around LottieView that fixes iOS native module initialization timing.
|
||||
*
|
||||
* On iOS, the Lottie native module isn't always fully initialized when components
|
||||
* first render during app startup. This causes animations to not appear until
|
||||
* after navigating to another screen that triggers native module initialization.
|
||||
*
|
||||
* This component adds a 100ms delay before starting autoPlay animations, giving
|
||||
* the native module time to initialize properly.
|
||||
*
|
||||
* Usage: Drop-in replacement for LottieView
|
||||
* @example
|
||||
* <DelayedLottieView autoPlay loop source={animation} style={styles.animation} />
|
||||
*/
|
||||
export const DelayedLottieView = forwardRef<LottieView, LottieViewProps>((props, forwardedRef) => {
|
||||
// If LottieView is undefined (peer dependency not installed), return null
|
||||
if (typeof LottieView === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const internalRef = useRef<LottieView>(null);
|
||||
const ref = (forwardedRef as React.RefObject<LottieView>) || internalRef;
|
||||
|
||||
useEffect(() => {
|
||||
// Only auto-trigger for autoPlay animations
|
||||
if (props.autoPlay) {
|
||||
const timer = setTimeout(() => {
|
||||
ref.current?.play();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [props.autoPlay, ref]);
|
||||
|
||||
// For autoPlay animations, disable native autoPlay and control it ourselves
|
||||
const modifiedProps = props.autoPlay ? { ...props, autoPlay: false } : props;
|
||||
|
||||
return <LottieView ref={ref} {...modifiedProps} />;
|
||||
});
|
||||
|
||||
DelayedLottieView.displayName = 'DelayedLottieView';
|
||||
129
packages/mobile-sdk-alpha/src/components/LottieAnimation.tsx
Normal file
129
packages/mobile-sdk-alpha/src/components/LottieAnimation.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { MutableRefObject } from 'react';
|
||||
import { forwardRef, useCallback, useMemo, useRef } from 'react';
|
||||
import { Image, type ViewStyle } from 'react-native';
|
||||
|
||||
import type { Dotlottie, Mode } from '@lottiefiles/dotlottie-react-native';
|
||||
import { DotLottie } from '@lottiefiles/dotlottie-react-native';
|
||||
|
||||
/**
|
||||
* Wrapper around DotLottie with legacy LottieView prop compatibility.
|
||||
*
|
||||
* DotLottie loads .lottie sources asynchronously on the native side, so its
|
||||
* built-in autoplay already waits for load — no manual delay needed.
|
||||
*
|
||||
* @example
|
||||
* <LottieAnimation autoPlay loop source={animation} style={styles.animation} />
|
||||
*/
|
||||
export type DotLottieSource = string | number | { uri: string };
|
||||
|
||||
type DotLottieEvents = {
|
||||
onLoad?: () => void;
|
||||
onComplete?: () => void;
|
||||
onLoadError?: () => void;
|
||||
onPlay?: () => void;
|
||||
onLoop?: (loopCount: number) => void;
|
||||
onDestroy?: () => void;
|
||||
onUnFreeze?: () => void;
|
||||
onFreeze?: () => void;
|
||||
onPause?: () => void;
|
||||
onFrame?: () => void;
|
||||
onStop?: () => void;
|
||||
onRender?: () => void;
|
||||
onTransition?: (state: { previousState: string; newState: string }) => void;
|
||||
onStateExit?: (state: { leavingState: string }) => void;
|
||||
onStateEntered?: (state: { enteringState: string }) => void;
|
||||
};
|
||||
|
||||
type LottieAnimationProps = DotLottieEvents & {
|
||||
source: DotLottieSource;
|
||||
style?: ViewStyle;
|
||||
loop?: boolean;
|
||||
autoplay?: boolean;
|
||||
speed?: number;
|
||||
themeId?: string;
|
||||
marker?: string;
|
||||
segment?: number[];
|
||||
playMode?: Mode;
|
||||
// Legacy LottieView prop kept for mechanical migration
|
||||
autoPlay?: boolean;
|
||||
// Legacy lifecycle callbacks
|
||||
onAnimationLoaded?: () => void;
|
||||
onAnimationFinish?: (isCancelled: boolean) => void;
|
||||
// Legacy compatibility props (ignored by DotLottie)
|
||||
cacheComposition?: boolean;
|
||||
progress?: number;
|
||||
renderMode?: 'AUTOMATIC' | 'HARDWARE' | 'SOFTWARE';
|
||||
resizeMode?: 'cover' | 'contain' | 'center';
|
||||
};
|
||||
|
||||
export const LottieAnimation = forwardRef<Dotlottie, LottieAnimationProps>((props, forwardedRef) => {
|
||||
const {
|
||||
autoPlay,
|
||||
autoplay,
|
||||
source,
|
||||
onAnimationLoaded,
|
||||
onAnimationFinish,
|
||||
onLoad,
|
||||
onComplete,
|
||||
cacheComposition: _cacheComposition,
|
||||
progress: _progress,
|
||||
renderMode: _renderMode,
|
||||
resizeMode: _resizeMode,
|
||||
style,
|
||||
...rest
|
||||
} = props;
|
||||
const internalRef = useRef<Dotlottie | null>(null);
|
||||
const shouldAutoPlay = useMemo(() => Boolean(autoPlay ?? autoplay), [autoPlay, autoplay]);
|
||||
|
||||
// Metro require() returns a number asset ID — resolve it to a { uri } for DotLottie
|
||||
const resolvedSource = useMemo((): string | { uri: string } => {
|
||||
if (typeof source === 'number') {
|
||||
const asset = Image.resolveAssetSource(source);
|
||||
return { uri: asset.uri };
|
||||
}
|
||||
return source;
|
||||
}, [source]);
|
||||
|
||||
const handleRef = useCallback(
|
||||
(instance: unknown) => {
|
||||
const lottieInstance = instance as Dotlottie | null;
|
||||
internalRef.current = lottieInstance;
|
||||
if (typeof forwardedRef === 'function') {
|
||||
forwardedRef(lottieInstance);
|
||||
return;
|
||||
}
|
||||
if (forwardedRef) {
|
||||
(forwardedRef as MutableRefObject<Dotlottie | null>).current = lottieInstance;
|
||||
}
|
||||
},
|
||||
[forwardedRef],
|
||||
);
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
onAnimationLoaded?.();
|
||||
onLoad?.();
|
||||
}, [onAnimationLoaded, onLoad]);
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
onAnimationFinish?.(false);
|
||||
onComplete?.();
|
||||
}, [onAnimationFinish, onComplete]);
|
||||
|
||||
return (
|
||||
<DotLottie
|
||||
ref={handleRef}
|
||||
{...rest}
|
||||
source={resolvedSource}
|
||||
style={style ?? {}}
|
||||
autoplay={shouldAutoPlay}
|
||||
onLoad={handleLoad}
|
||||
onComplete={handleComplete}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
LottieAnimation.displayName = 'LottieAnimation';
|
||||
@@ -3,8 +3,8 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
/**
|
||||
* DelayedLottieView for web placeholder component.
|
||||
* LottieAnimation web placeholder component.
|
||||
*/
|
||||
export const DelayedLottieView = () => {
|
||||
export const LottieAnimation = () => {
|
||||
return <div />;
|
||||
};
|
||||
@@ -7,9 +7,8 @@ import { StyleSheet } from 'react-native';
|
||||
|
||||
import type { DocumentCategory } from '@selfxyz/common/utils/types';
|
||||
|
||||
import successAnimation from '../../animations/loading/success.json';
|
||||
import { PrimaryButton } from '../../components';
|
||||
import { DelayedLottieView } from '../../components/DelayedLottieView';
|
||||
import { LottieAnimation } from '../../components/LottieAnimation';
|
||||
import Description from '../../components/typography/Description';
|
||||
import { Title } from '../../components/typography/Title';
|
||||
import { PassportEvents, ProofEvents } from '../../constants/analytics';
|
||||
@@ -22,6 +21,9 @@ import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
import { SdkEvents } from '../../types/events';
|
||||
import type { SelfClient } from '../../types/public';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const successAnimation = require('../../animations/loading/success.lottie');
|
||||
|
||||
/*
|
||||
Screen to confirm identification ownership
|
||||
props:
|
||||
@@ -46,7 +48,7 @@ export const ConfirmIdentificationScreen = ({ onBeforeConfirm }: { onBeforeConfi
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={black}>
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop={false}
|
||||
source={successAnimation}
|
||||
|
||||
@@ -7,9 +7,8 @@ import { StyleSheet } from 'react-native';
|
||||
import type { MRZInfo } from 'src/types/public';
|
||||
|
||||
import Scan from '../../../svgs/icons/passport_camera_scan.svg';
|
||||
import passportScanAnimation from '../../animations/passport_scan.json';
|
||||
import { Additional, Description, SecondaryButton, Title, View, XStack, YStack } from '../../components';
|
||||
import { DelayedLottieView } from '../../components/DelayedLottieView';
|
||||
import { LottieAnimation } from '../../components/LottieAnimation';
|
||||
import { MRZScannerView } from '../../components/MRZScannerView';
|
||||
import { PassportEvents } from '../../constants/analytics';
|
||||
import { black, slate400, slate800, white } from '../../constants/colors';
|
||||
@@ -20,6 +19,9 @@ import type { SafeAreaInsets } from '../../layouts/ExpandableBottomLayout';
|
||||
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
import { SdkEvents } from '../../types/events';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const passportScanAnimation = require('../../animations/passport_scan.lottie');
|
||||
|
||||
type Props = {
|
||||
onBack?: () => void;
|
||||
onSuccess?: () => void;
|
||||
@@ -55,7 +57,7 @@ export const DocumentCameraScreen = ({ onBack, onSuccess, safeAreaInsets }: Prop
|
||||
>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={black} safeAreaTop={safeAreaInsets?.top}>
|
||||
<MRZScannerView onMRZDetected={handleMRZDetected} onError={handleScannerError} />
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
autoPlay
|
||||
loop
|
||||
source={passportScanAnimation}
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type LottieView from 'lottie-react-native';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Image, Linking, NativeEventEmitter, NativeModules, Platform, StyleSheet } from 'react-native';
|
||||
import NfcManager from 'react-native-nfc-manager';
|
||||
import passportVerifyAnimation from 'src/animations/passport_verify.json';
|
||||
import { BodyText, PrimaryButton, SecondaryButton, Title, View, XStack } from 'src/components';
|
||||
import ButtonsContainer from 'src/components/ButtonsContainer';
|
||||
import { DelayedLottieView } from 'src/components/DelayedLottieView';
|
||||
import { LottieAnimation } from 'src/components/LottieAnimation';
|
||||
import TextsContainer from 'src/components/TextsContainer';
|
||||
import { PassportEvents } from 'src/constants/analytics';
|
||||
import { black, slate100, slate400, slate500, white } from 'src/constants/colors';
|
||||
@@ -27,6 +25,11 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type { PassportData } from '@selfxyz/common';
|
||||
|
||||
import type { Dotlottie } from '@lottiefiles/dotlottie-react-native';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const passportVerifyAnimation = require('../../animations/passport_verify.lottie');
|
||||
|
||||
const emitter = Platform.OS === 'android' ? new NativeEventEmitter(NativeModules.nativeModule) : null;
|
||||
|
||||
type DocumentNFCScreenProps = {
|
||||
@@ -65,7 +68,7 @@ export const DocumentNFCScreen: React.FC<DocumentNFCScreenProps> = (props: Docum
|
||||
[props.useCan],
|
||||
);
|
||||
|
||||
const animationRef = useRef<LottieView>(null);
|
||||
const animationRef = useRef<Dotlottie | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
animationRef.current?.play();
|
||||
@@ -386,7 +389,7 @@ export const DocumentNFCScreen: React.FC<DocumentNFCScreenProps> = (props: Docum
|
||||
safeAreaBottom={props.safeAreaInsets?.bottom}
|
||||
>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={slate100} safeAreaTop={props.safeAreaInsets?.top}>
|
||||
<DelayedLottieView
|
||||
<LottieAnimation
|
||||
ref={animationRef}
|
||||
autoPlay={false}
|
||||
loop={false}
|
||||
|
||||
@@ -48,6 +48,8 @@ export type { DocumentAttributes } from './documents/validation';
|
||||
|
||||
export type { DocumentData, DocumentMetadata, PassportCameraProps, ScreenProps } from './types/ui';
|
||||
|
||||
export type { DotLottieSource } from './components/LottieAnimation';
|
||||
|
||||
export type { HapticOptions, HapticType } from './haptic/shared';
|
||||
|
||||
export type { MRZScanOptions } from './mrz';
|
||||
@@ -62,8 +64,6 @@ export type { SdkErrorCategory } from './errors';
|
||||
|
||||
export type { provingMachineCircuitType } from './proving/provingMachine';
|
||||
|
||||
export { DelayedLottieView } from './components/DelayedLottieView';
|
||||
|
||||
export { ExpandableBottomLayout } from './layouts/ExpandableBottomLayout';
|
||||
|
||||
export {
|
||||
@@ -79,6 +79,8 @@ export {
|
||||
|
||||
export { default as LogoConfirmationScreen } from './flows/onboarding/logo-confirmation-screen';
|
||||
|
||||
export { LottieAnimation } from './components/LottieAnimation';
|
||||
|
||||
export { NFCScannerScreen } from './components/screens/NFCScannerScreen';
|
||||
|
||||
export { QRCodeScreen } from './components/screens/QRCodeScreen';
|
||||
|
||||
171
packages/mobile-sdk-alpha/tests/LottieAnimation.test.tsx
Normal file
171
packages/mobile-sdk-alpha/tests/LottieAnimation.test.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
import { createRef, type Ref } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LottieAnimation } from '../src/components/LottieAnimation';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
// Shared mutable state — vi.hoisted ensures it exists before the hoisted vi.mock runs
|
||||
const state = vi.hoisted(() => ({
|
||||
capturedProps: {} as Record<string, any>,
|
||||
capturedRef: null as Ref<any> | null,
|
||||
}));
|
||||
|
||||
vi.mock('@lottiefiles/dotlottie-react-native', async () => {
|
||||
const { forwardRef } = await import('react');
|
||||
const MockDotLottie = forwardRef((props: any, ref: any) => {
|
||||
state.capturedProps = { ...props };
|
||||
state.capturedRef = ref;
|
||||
return null;
|
||||
});
|
||||
MockDotLottie.displayName = 'MockDotLottie';
|
||||
return {
|
||||
DotLottie: MockDotLottie,
|
||||
Mode: { FORWARD: 0, REVERSE: 1, BOUNCE: 2, REVERSE_BOUNCE: 3 },
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
state.capturedProps = {};
|
||||
state.capturedRef = null;
|
||||
});
|
||||
|
||||
describe('LottieAnimation', () => {
|
||||
describe('autoPlay → autoplay mapping', () => {
|
||||
it('maps legacy autoPlay={true} to autoplay={true}', () => {
|
||||
render(<LottieAnimation autoPlay source="test.lottie" />);
|
||||
expect(state.capturedProps.autoplay).toBe(true);
|
||||
});
|
||||
|
||||
it('maps legacy autoPlay={false} to autoplay={false}', () => {
|
||||
render(<LottieAnimation autoPlay={false} source="test.lottie" />);
|
||||
expect(state.capturedProps.autoplay).toBe(false);
|
||||
});
|
||||
|
||||
it('passes native autoplay prop through', () => {
|
||||
render(<LottieAnimation autoplay source="test.lottie" />);
|
||||
expect(state.capturedProps.autoplay).toBe(true);
|
||||
});
|
||||
|
||||
it('prefers autoPlay over autoplay when both are set', () => {
|
||||
render(<LottieAnimation autoPlay={false} autoplay={true} source="test.lottie" />);
|
||||
expect(state.capturedProps.autoplay).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults to false when neither is set', () => {
|
||||
render(<LottieAnimation source="test.lottie" />);
|
||||
expect(state.capturedProps.autoplay).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAnimationLoaded → onLoad mapping', () => {
|
||||
it('calls onAnimationLoaded when DotLottie fires onLoad', () => {
|
||||
const onAnimationLoaded = vi.fn();
|
||||
render(<LottieAnimation source="test.lottie" onAnimationLoaded={onAnimationLoaded} />);
|
||||
state.capturedProps.onLoad();
|
||||
expect(onAnimationLoaded).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('calls both onAnimationLoaded and onLoad when both are set', () => {
|
||||
const onAnimationLoaded = vi.fn();
|
||||
const onLoad = vi.fn();
|
||||
render(<LottieAnimation source="test.lottie" onAnimationLoaded={onAnimationLoaded} onLoad={onLoad} />);
|
||||
state.capturedProps.onLoad();
|
||||
expect(onAnimationLoaded).toHaveBeenCalledOnce();
|
||||
expect(onLoad).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAnimationFinish → onComplete mapping', () => {
|
||||
it('calls onAnimationFinish with isCancelled=false on complete', () => {
|
||||
const onAnimationFinish = vi.fn();
|
||||
render(<LottieAnimation source="test.lottie" onAnimationFinish={onAnimationFinish} />);
|
||||
state.capturedProps.onComplete();
|
||||
expect(onAnimationFinish).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('always passes false for isCancelled (cancellation signal is lost)', () => {
|
||||
const onAnimationFinish = vi.fn();
|
||||
render(<LottieAnimation source="test.lottie" onAnimationFinish={onAnimationFinish} />);
|
||||
state.capturedProps.onComplete();
|
||||
state.capturedProps.onComplete();
|
||||
expect(onAnimationFinish).toHaveBeenCalledTimes(2);
|
||||
expect(onAnimationFinish).toHaveBeenNthCalledWith(1, false);
|
||||
expect(onAnimationFinish).toHaveBeenNthCalledWith(2, false);
|
||||
});
|
||||
|
||||
it('calls both onAnimationFinish and onComplete when both are set', () => {
|
||||
const onAnimationFinish = vi.fn();
|
||||
const onComplete = vi.fn();
|
||||
render(<LottieAnimation source="test.lottie" onAnimationFinish={onAnimationFinish} onComplete={onComplete} />);
|
||||
state.capturedProps.onComplete();
|
||||
expect(onAnimationFinish).toHaveBeenCalledWith(false);
|
||||
expect(onComplete).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy props are dropped', () => {
|
||||
it('does not forward cacheComposition, progress, renderMode, or resizeMode', () => {
|
||||
render(
|
||||
<LottieAnimation
|
||||
source="test.lottie"
|
||||
cacheComposition={true}
|
||||
progress={0.5}
|
||||
renderMode="HARDWARE"
|
||||
resizeMode="cover"
|
||||
/>,
|
||||
);
|
||||
expect(state.capturedProps.cacheComposition).toBeUndefined();
|
||||
expect(state.capturedProps.progress).toBeUndefined();
|
||||
expect(state.capturedProps.renderMode).toBeUndefined();
|
||||
expect(state.capturedProps.resizeMode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('forwards non-legacy props like loop and speed', () => {
|
||||
render(<LottieAnimation source="test.lottie" loop speed={2} />);
|
||||
expect(state.capturedProps.loop).toBe(true);
|
||||
expect(state.capturedProps.speed).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ref forwarding', () => {
|
||||
it('forwards function ref with the DotLottie instance', () => {
|
||||
const refFn = vi.fn();
|
||||
render(<LottieAnimation ref={refFn} source="test.lottie" />);
|
||||
// Simulate the native side calling the ref callback
|
||||
const fakeInstance = { play: vi.fn() };
|
||||
if (typeof state.capturedRef === 'function') {
|
||||
state.capturedRef(fakeInstance);
|
||||
}
|
||||
expect(refFn).toHaveBeenCalledWith(fakeInstance);
|
||||
});
|
||||
|
||||
it('forwards object ref with the DotLottie instance', () => {
|
||||
const ref = createRef<any>();
|
||||
render(<LottieAnimation ref={ref} source="test.lottie" />);
|
||||
const fakeInstance = { play: vi.fn() };
|
||||
if (typeof state.capturedRef === 'function') {
|
||||
state.capturedRef(fakeInstance);
|
||||
}
|
||||
expect(ref.current).toBe(fakeInstance);
|
||||
});
|
||||
});
|
||||
|
||||
describe('style handling', () => {
|
||||
it('passes through provided style', () => {
|
||||
const style = { width: 100, height: 100 };
|
||||
render(<LottieAnimation source="test.lottie" style={style} />);
|
||||
expect(state.capturedProps.style).toEqual(style);
|
||||
});
|
||||
|
||||
it('defaults to empty object when no style is provided', () => {
|
||||
render(<LottieAnimation source="test.lottie" />);
|
||||
expect(state.capturedProps.style).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -245,11 +245,17 @@ vi.mock('react-native-svg-circle-country-flags', () => ({
|
||||
default: {},
|
||||
}));
|
||||
|
||||
// Mock lottie-react-native
|
||||
// Mock lottie-react-native (legacy, kept for transitive imports)
|
||||
vi.mock('lottie-react-native', () => ({
|
||||
default: 'div',
|
||||
}));
|
||||
|
||||
// Mock @lottiefiles/dotlottie-react-native (native module unavailable in Node)
|
||||
vi.mock('@lottiefiles/dotlottie-react-native', () => ({
|
||||
DotLottie: 'div',
|
||||
Mode: { FORWARD: 0, REVERSE: 1, BOUNCE: 2, REVERSE_BOUNCE: 3 },
|
||||
}));
|
||||
|
||||
// Mock react-native-haptic-feedback
|
||||
vi.mock('react-native-haptic-feedback', () => ({
|
||||
default: {
|
||||
|
||||
@@ -76,7 +76,7 @@ export default defineConfig([
|
||||
'ethers',
|
||||
// React Native dependencies
|
||||
'react-native-svg-circle-country-flags',
|
||||
'lottie-react-native',
|
||||
'@lottiefiles/dotlottie-react-native',
|
||||
'react-native-haptic-feedback',
|
||||
'react-native-localize',
|
||||
// Optional RN adapter peer dependencies
|
||||
@@ -87,6 +87,8 @@ export default defineConfig([
|
||||
/^@noble\/hashes\/.*/,
|
||||
// SVG files should be handled by React Native's SVG transformer
|
||||
/\.svg$/,
|
||||
// Externalize animation files so Metro can deduplicate them
|
||||
/\/animations\/.*\.(json|lottie)$/,
|
||||
],
|
||||
esbuildOptions(options) {
|
||||
options.supported = {
|
||||
@@ -133,7 +135,7 @@ export default defineConfig([
|
||||
'ethers',
|
||||
// React Native dependencies
|
||||
'react-native-svg-circle-country-flags',
|
||||
'lottie-react-native',
|
||||
'@lottiefiles/dotlottie-react-native',
|
||||
'react-native-haptic-feedback',
|
||||
'react-native-localize',
|
||||
// Optional RN adapter peer dependencies
|
||||
@@ -144,6 +146,8 @@ export default defineConfig([
|
||||
/^@noble\/hashes\/.*/,
|
||||
// SVG files should be handled by React Native's SVG transformer
|
||||
/\.svg$/,
|
||||
// Externalize animation files so Metro can deduplicate them
|
||||
/\/animations\/.*\.(json|lottie)$/,
|
||||
],
|
||||
outExtension: ({ format }) => ({ js: format === 'cjs' ? '.cjs' : '.js' }),
|
||||
esbuildOptions(options) {
|
||||
|
||||
@@ -18,8 +18,8 @@ Host App
|
||||
├─ LifecycleHandler (init, ready, close, error, success)
|
||||
├─ BiometricHandler (authenticate, isAvailable)
|
||||
├─ KeychainHandler (get, set, remove)
|
||||
├─ NfcHandler (scan, cancelScan, isSupported)
|
||||
└─ CameraHandler (isAvailable, scanMRZ — stub)
|
||||
├─ NfcHandler (scan + APDU exchange, cancelScan, isSupported)
|
||||
└─ CameraHandler (isAvailable, scanMRZ via native module)
|
||||
```
|
||||
|
||||
### File List
|
||||
@@ -32,12 +32,12 @@ Host App
|
||||
| `src/handlers/LifecycleHandler.ts` | ~70 | App lifecycle + verification callbacks |
|
||||
| `src/handlers/BiometricHandler.ts` | ~60 | Biometric auth via react-native-biometrics |
|
||||
| `src/handlers/KeychainHandler.ts` | ~65 | Secure storage via react-native-keychain |
|
||||
| `src/handlers/NfcHandler.ts` | ~130 | NFC tag reading via react-native-nfc-manager |
|
||||
| `src/handlers/CameraHandler.ts` | ~25 | Camera stub (isAvailable, scanMRZ not yet impl) |
|
||||
| `src/handlers/NfcHandler.ts` | ~180 | NFC tag reading + APDU exchange via react-native-nfc-manager |
|
||||
| `src/handlers/CameraHandler.ts` | ~90 | MRZ scanning via native SelfMRZScannerModule / MRZScannerModule |
|
||||
| `src/handlers/index.ts` | ~30 | Handler factory (createHandlers) |
|
||||
| `src/index.ts` | ~5 | Public exports |
|
||||
| **Total source** | **~715** | |
|
||||
| **Tests (8 files)** | **~880** | 59 tests |
|
||||
| **Tests (8 files)** | **~950** | 64 tests |
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -85,31 +85,40 @@ into their platform build:
|
||||
|
||||
### NFC Scan Return Shape
|
||||
|
||||
The webview-bridge spec expects `nfc.scan` to return raw APDU response
|
||||
bytes. Our implementation returns a higher-level object:
|
||||
The NFC handler returns tag metadata plus optional APDU exchange results:
|
||||
|
||||
```typescript
|
||||
{ connected: true, tagId: string | null, techType: string, params: {...} }
|
||||
{
|
||||
connected: true,
|
||||
tagId: string | null,
|
||||
techType: string,
|
||||
params: {...},
|
||||
apduResponses?: string[] // hex-encoded responses when apduCommands are provided
|
||||
}
|
||||
```
|
||||
|
||||
**Justification:** `react-native-nfc-manager` provides tag discovery and
|
||||
technology negotiation but does not expose raw APDU transceive at the
|
||||
`getTag()` level without additional low-level calls. The current shape
|
||||
gives the web layer enough information to confirm a tag was found and
|
||||
proceed with the verification flow. When APDU command exchange is needed,
|
||||
a `transceive` method should be added to `NfcHandler` that wraps
|
||||
`NfcManager.transceive()`.
|
||||
When `params.apduCommands` (array of hex strings) is provided, the handler
|
||||
iterates through each command, calls `NfcManager.transceive()`, and returns
|
||||
hex-encoded response bytes in `apduResponses`. Progress events are emitted
|
||||
at `apdu_exchange` (70%) and `apdu_complete` (90%).
|
||||
|
||||
---
|
||||
|
||||
## Deferred Decision
|
||||
## Camera / MRZ Implementation
|
||||
|
||||
**Camera / MRZ scanning** — `CameraHandler.scanMRZ` throws
|
||||
`NOT_IMPLEMENTED`. A full implementation requires choosing a camera
|
||||
library (`react-native-vision-camera` is the modern choice) plus an
|
||||
OCR/MRZ parsing layer. `isAvailable` currently returns `true`
|
||||
unconditionally. This should be wired to a real permission check once
|
||||
a camera library is chosen.
|
||||
`CameraHandler` loads the native MRZ scanner module at init time,
|
||||
checking for `SelfMRZScannerModule` (preferred) or `MRZScannerModule`
|
||||
(fallback) from React Native's `NativeModules`.
|
||||
|
||||
- `isAvailable()` returns whether a native MRZ module was found.
|
||||
- `scanMRZ()` calls `scanner.startScanning()`, normalizes the result
|
||||
(extracts `documentNumber`, `dateOfBirth`, `dateOfExpiry`, plus optional
|
||||
`documentType` and `countryCode`), and throws `MRZ_SCAN_FAILED` on
|
||||
scanner errors or `MRZ_SCAN_INVALID_RESULT` if required fields are missing.
|
||||
- If no native module is present, `scanMRZ()` throws `NOT_AVAILABLE`.
|
||||
|
||||
The host app must provide a native MRZ scanner module (e.g., via
|
||||
`react-native-vision-camera` + OCR) that exposes `startScanning()`.
|
||||
|
||||
---
|
||||
|
||||
@@ -119,7 +128,7 @@ a camera library is chosen.
|
||||
|
||||
```bash
|
||||
cd packages/rn-sdk
|
||||
npx vitest run # 59 tests across 8 files
|
||||
npx vitest run # 64 tests across 8 files
|
||||
```
|
||||
|
||||
### Device Testing Checklist
|
||||
@@ -180,16 +189,15 @@ import { SelfVerification } from '@selfxyz/rn-sdk';
|
||||
| Lifecycle handler | Done | init, ready, close, error, success |
|
||||
| Biometric handler | Done | authenticate, isAvailable via react-native-biometrics |
|
||||
| Keychain handler | Done | get, set, remove via react-native-keychain |
|
||||
| NFC handler | Done | scan, cancelScan, isSupported via react-native-nfc-manager |
|
||||
| NFC handler | Done | scan + APDU exchange, cancelScan, isSupported via react-native-nfc-manager |
|
||||
| iOS asset path | Done | Absolute path via react-native-fs, relative fallback |
|
||||
| Android asset path | Done | `file:///android_asset/` |
|
||||
| Dev server override | Done | `devServerUrl` prop |
|
||||
| Camera / MRZ scan | Stub | `isAvailable` hardcoded true, `scanMRZ` throws NOT_IMPLEMENTED |
|
||||
| Camera / MRZ scan | Done | scanMRZ via native SelfMRZScannerModule with result normalization |
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Camera `scanMRZ` is a stub — needs camera library + OCR (see Deferred Decision)
|
||||
- NFC returns tag metadata, not raw APDU bytes (see Spec Deviation)
|
||||
- `CameraHandler.isAvailable` returns `true` unconditionally
|
||||
- Camera/MRZ requires host app to provide a native MRZ scanner module (`SelfMRZScannerModule` or `MRZScannerModule`)
|
||||
- No retry/reconnect logic for WebView crashes
|
||||
- Asset bundling requires manual platform setup by the host app
|
||||
- Physical-device validation breadth for NFC/APDU and camera across host apps is still limited
|
||||
|
||||
79
packages/rn-sdk/assets/self-wallet/assets/index-BZlxLbn7.js
Normal file
79
packages/rn-sdk/assets/self-wallet/assets/index-BZlxLbn7.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
@font-face{font-family:Advercase-Regular;src:url(../fonts/Advercase-Regular.otf) format("opentype");font-display:swap}@font-face{font-family:DINOT-Bold;src:url(../fonts/DINOT-Bold.otf) format("opentype");font-weight:700;font-display:swap}@font-face{font-family:DINOT-Medium;src:url(../fonts/DINOT-Medium.otf) format("opentype");font-weight:500;font-display:swap}@font-face{font-family:IBMPlexMono-Regular;src:url(../fonts/IBMPlexMono-Regular.otf) format("opentype");font-display:swap}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%;overflow:hidden}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}@keyframes spin{to{transform:rotate(360deg)}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user