mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
Merge pull request #1637 from selfxyz/staging
Release to Production - 2026-01-22
This commit is contained in:
@@ -20,9 +20,9 @@
|
||||
## Core Components
|
||||
|
||||
1. Identity Verification Hub
|
||||
- Manages multi-step verification process for passports and EU ID cards
|
||||
- Manages multi-step verification process for passports, EU ID cards, Aadhaar, and Selfrica ID cards
|
||||
- Handles document attestation through zero-knowledge proofs
|
||||
- Implements verification paths: E-PASSPORT and EU_ID_CARD
|
||||
- Implements verification paths: E-PASSPORT, EU_ID_CARD, AADHAAR, and SELFRICA_ID_CARD
|
||||
- File: contracts/contracts/IdentityVerificationHubImplV2.sol
|
||||
|
||||
2. Document Verification Processing
|
||||
@@ -40,10 +40,10 @@
|
||||
- Files: noir/crates/dg1/src/ofac/*.nr
|
||||
|
||||
4. Identity Registry Management
|
||||
- Maintains separate registries for passports and ID cards
|
||||
- Maintains separate registries for passports, EU ID cards, Aadhaar, and Selfrica
|
||||
- Handles DSC key commitment registration
|
||||
- Implements nullifier tracking for duplicate prevention
|
||||
- File: contracts/contracts/registry/IdentityRegistryImplV1.sol
|
||||
- Files: contracts/contracts/registry/IdentityRegistryImplV1.sol, IdentityRegistryIdCardImplV1.sol, IdentityRegistryAadhaarImplV1.sol, IdentityRegistrySelfricaImplV1.sol
|
||||
|
||||
## Core Workflows
|
||||
|
||||
|
||||
47
.github/actions/cache-core-sdk-build/action.yml
vendored
Normal file
47
.github/actions/cache-core-sdk-build/action.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Cache Core SDK Build
|
||||
description: Cache core SDK build artifacts (common, sdk/core)
|
||||
|
||||
inputs:
|
||||
mode:
|
||||
description: "save or restore"
|
||||
required: true
|
||||
cache-version:
|
||||
description: Cache version string
|
||||
required: false
|
||||
default: v1
|
||||
fail-on-cache-miss:
|
||||
description: Fail if cache not found (restore mode only)
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: Whether cache was hit
|
||||
value: ${{ steps.restore.outputs.cache-hit }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: restore
|
||||
if: inputs.mode == 'restore'
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/core/dist
|
||||
node_modules
|
||||
sdk/core/node_modules
|
||||
common/node_modules
|
||||
key: core-sdk-build-${{ inputs.cache-version }}-${{ github.sha }}
|
||||
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
|
||||
- id: save
|
||||
if: inputs.mode == 'save'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/core/dist
|
||||
node_modules
|
||||
sdk/core/node_modules
|
||||
common/node_modules
|
||||
key: core-sdk-build-${{ inputs.cache-version }}-${{ github.sha }}
|
||||
47
.github/actions/cache-mobile-sdk-build/action.yml
vendored
Normal file
47
.github/actions/cache-mobile-sdk-build/action.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Cache Mobile SDK Build
|
||||
description: Cache mobile SDK build artifacts (common, mobile-sdk-alpha)
|
||||
|
||||
inputs:
|
||||
mode:
|
||||
description: "save or restore"
|
||||
required: true
|
||||
cache-version:
|
||||
description: Cache version string
|
||||
required: false
|
||||
default: v1
|
||||
fail-on-cache-miss:
|
||||
description: Fail if cache not found (restore mode only)
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: Whether cache was hit
|
||||
value: ${{ steps.restore.outputs.cache-hit }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: restore
|
||||
if: inputs.mode == 'restore'
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
packages/mobile-sdk-alpha/dist
|
||||
node_modules
|
||||
packages/mobile-sdk-alpha/node_modules
|
||||
common/node_modules
|
||||
key: mobile-sdk-alpha-build-${{ inputs.cache-version }}-${{ github.sha }}
|
||||
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
|
||||
- id: save
|
||||
if: inputs.mode == 'save'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
packages/mobile-sdk-alpha/dist
|
||||
node_modules
|
||||
packages/mobile-sdk-alpha/node_modules
|
||||
common/node_modules
|
||||
key: mobile-sdk-alpha-build-${{ inputs.cache-version }}-${{ github.sha }}
|
||||
43
.github/actions/cache-sdk-build/action.yml
vendored
Normal file
43
.github/actions/cache-sdk-build/action.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Cache SDK Build
|
||||
description: Cache SDK build artifacts (common, sdk-common, qrcode)
|
||||
|
||||
inputs:
|
||||
mode:
|
||||
description: "save or restore"
|
||||
required: true
|
||||
cache-version:
|
||||
description: Cache version string
|
||||
required: false
|
||||
default: v1
|
||||
fail-on-cache-miss:
|
||||
description: Fail if cache not found (restore mode only)
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: Whether cache was hit
|
||||
value: ${{ steps.restore.outputs.cache-hit }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: restore
|
||||
if: inputs.mode == 'restore'
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ inputs.cache-version }}-${{ github.sha }}
|
||||
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
|
||||
- id: save
|
||||
if: inputs.mode == 'save'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ inputs.cache-version }}-${{ github.sha }}
|
||||
61
.github/workflows/circuits-build.yml
vendored
61
.github/workflows/circuits-build.yml
vendored
@@ -32,7 +32,12 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ["128ram"]
|
||||
runs-on:
|
||||
- "32ram"
|
||||
- "self-hosted"
|
||||
- "selfxyz-org"
|
||||
# GitHub-hosted runners cap at 360 min (6h); 720 applies if using self-hosted
|
||||
timeout-minutes: 720
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -164,7 +169,7 @@ jobs:
|
||||
path: output/
|
||||
run_id: ${{ inputs.run-id }}
|
||||
|
||||
- name: Build cpp circuits
|
||||
- name: Prepare build scripts
|
||||
run: |
|
||||
chmod +x circuits/scripts/build/build_cpp.sh
|
||||
chmod +x circuits/scripts/build/build_single_circuit.sh
|
||||
@@ -172,46 +177,58 @@ jobs:
|
||||
# Validate inputs - only one should be provided
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
if [[ "${{ inputs.circuit-type }}" != "" && "${{ inputs.circuit-name }}" != "" ]]; then
|
||||
echo " Error: Cannot provide both circuit-type and circuit-name. Use only one."
|
||||
echo "Error: Cannot provide both circuit-type and circuit-name. Use only one."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check what type of build to perform
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.circuit-name }}" != "" ]]; then
|
||||
# Build circuits by name
|
||||
- name: Build cpp circuits (workflow_dispatch by name/type)
|
||||
if: github.event_name == 'workflow_dispatch' && (inputs.circuit-name != '' || inputs.circuit-type != '')
|
||||
run: |
|
||||
if [[ "${{ inputs.circuit-name }}" != "" ]]; then
|
||||
INPUT_CIRCUITS="${{ inputs.circuit-name }}"
|
||||
INPUT_CIRCUITS=$(echo "$INPUT_CIRCUITS" | tr -d ' ')
|
||||
IFS=',' read -ra CIRCUITS_ARRAY <<< "$INPUT_CIRCUITS"
|
||||
|
||||
echo "Building selected circuits: ${{ inputs.circuit-name }}"
|
||||
echo "Building selected circuits by name: ${{ inputs.circuit-name }}"
|
||||
for circuit_name in "${CIRCUITS_ARRAY[@]}"; do
|
||||
echo "Building circuit: $circuit_name"
|
||||
./circuits/scripts/build/build_single_circuit.sh "$circuit_name"
|
||||
done
|
||||
|
||||
elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.circuit-type }}" != "" ]]; then
|
||||
# Build circuits by type
|
||||
else
|
||||
INPUT_CIRCUITS="${{ inputs.circuit-type }}"
|
||||
INPUT_CIRCUITS=$(echo "$INPUT_CIRCUITS" | tr -d ' ')
|
||||
IFS=',' read -ra CIRCUITS_ARRAY <<< "$INPUT_CIRCUITS"
|
||||
|
||||
echo "Building selected circuits: ${{ inputs.circuit-type }}"
|
||||
echo "Building selected circuits by type: ${{ inputs.circuit-type }}"
|
||||
for circuit in "${CIRCUITS_ARRAY[@]}"; do
|
||||
echo "Building circuit: $circuit"
|
||||
./circuits/scripts/build/build_cpp.sh "$circuit"
|
||||
done
|
||||
|
||||
else
|
||||
# Build all circuits (default behavior)
|
||||
echo "Building all circuits (default behavior)"
|
||||
./circuits/scripts/build/build_cpp.sh register
|
||||
./circuits/scripts/build/build_cpp.sh register_id
|
||||
./circuits/scripts/build/build_cpp.sh register_aadhaar
|
||||
./circuits/scripts/build/build_cpp.sh disclose
|
||||
./circuits/scripts/build/build_cpp.sh dsc
|
||||
fi
|
||||
|
||||
- name: Build cpp circuits - register
|
||||
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
|
||||
run: ./circuits/scripts/build/build_cpp.sh register
|
||||
|
||||
- name: Build cpp circuits - register_id
|
||||
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
|
||||
run: ./circuits/scripts/build/build_cpp.sh register_id
|
||||
|
||||
- name: Build cpp circuits - register_aadhaar
|
||||
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
|
||||
run: ./circuits/scripts/build/build_cpp.sh register_aadhaar
|
||||
|
||||
- name: Build cpp circuits - register_kyc
|
||||
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
|
||||
run: ./circuits/scripts/build/build_cpp.sh register_kyc
|
||||
|
||||
- name: Build cpp circuits - disclose
|
||||
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
|
||||
run: ./circuits/scripts/build/build_cpp.sh disclose
|
||||
|
||||
- name: Build cpp circuits - dsc
|
||||
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
|
||||
run: ./circuits/scripts/build/build_cpp.sh dsc
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
with:
|
||||
|
||||
65
.github/workflows/core-sdk-ci.yml
vendored
65
.github/workflows/core-sdk-ci.yml
vendored
@@ -56,15 +56,10 @@ jobs:
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/core build
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@v4
|
||||
uses: ./.github/actions/cache-core-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/core/dist
|
||||
node_modules
|
||||
sdk/core/node_modules
|
||||
common/node_modules
|
||||
key: core-sdk-build-${{ github.sha }}
|
||||
mode: save
|
||||
cache-version: v1
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -80,19 +75,19 @@ jobs:
|
||||
corepack prepare yarn@4.12.0 --activate
|
||||
- name: Restore build artifacts
|
||||
id: build-cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: ./.github/actions/cache-core-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/core/dist
|
||||
node_modules
|
||||
sdk/core/node_modules
|
||||
common/node_modules
|
||||
key: core-sdk-build-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: v1
|
||||
fail-on-cache-miss: false
|
||||
- name: Install Dependencies
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/core build
|
||||
- name: Run linter
|
||||
run: yarn workspace @selfxyz/core lint
|
||||
|
||||
@@ -110,19 +105,19 @@ jobs:
|
||||
corepack prepare yarn@4.12.0 --activate
|
||||
- name: Restore build artifacts
|
||||
id: build-cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: ./.github/actions/cache-core-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/core/dist
|
||||
node_modules
|
||||
sdk/core/node_modules
|
||||
common/node_modules
|
||||
key: core-sdk-build-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: v1
|
||||
fail-on-cache-miss: false
|
||||
- name: Install Dependencies
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/core build
|
||||
- name: Type checking
|
||||
run: yarn workspace @selfxyz/core types
|
||||
|
||||
@@ -140,18 +135,18 @@ jobs:
|
||||
corepack prepare yarn@4.12.0 --activate
|
||||
- name: Restore build artifacts
|
||||
id: build-cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: ./.github/actions/cache-core-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/core/dist
|
||||
node_modules
|
||||
sdk/core/node_modules
|
||||
common/node_modules
|
||||
key: core-sdk-build-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: v1
|
||||
fail-on-cache-miss: false
|
||||
- name: Install Dependencies
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/core build
|
||||
- name: Run tests
|
||||
run: yarn workspace @selfxyz/core test
|
||||
|
||||
2
.github/workflows/mobile-ci.yml
vendored
2
.github/workflows/mobile-ci.yml
vendored
@@ -5,7 +5,7 @@ env:
|
||||
RUBY_VERSION: 3.2
|
||||
JAVA_VERSION: 17
|
||||
ANDROID_NDK_VERSION: 27.0.12077973
|
||||
XCODE_VERSION: 16.4
|
||||
XCODE_VERSION: 26
|
||||
# Path configuration
|
||||
WORKSPACE: ${{ github.workspace }}
|
||||
APP_PATH: ${{ github.workspace }}/app
|
||||
|
||||
2
.github/workflows/mobile-deploy.yml
vendored
2
.github/workflows/mobile-deploy.yml
vendored
@@ -33,7 +33,7 @@ env:
|
||||
JAVA_VERSION: 17
|
||||
ANDROID_API_LEVEL: 35
|
||||
ANDROID_NDK_VERSION: 27.0.12077973
|
||||
XCODE_VERSION: 16.4
|
||||
XCODE_VERSION: 26
|
||||
|
||||
# Cache versioning - increment these to bust caches when needed
|
||||
GH_CACHE_VERSION: v1 # Global cache version
|
||||
|
||||
328
.github/workflows/mobile-e2e.yml
vendored
328
.github/workflows/mobile-e2e.yml
vendored
@@ -5,7 +5,7 @@ env:
|
||||
JAVA_VERSION: 17
|
||||
ANDROID_API_LEVEL: 33
|
||||
ANDROID_NDK_VERSION: 27.0.12077973
|
||||
XCODE_VERSION: 16.4
|
||||
XCODE_VERSION: 26
|
||||
# Cache versions
|
||||
GH_CACHE_VERSION: v2 # Global cache version - bumped to invalidate caches
|
||||
GH_GEMS_CACHE_VERSION: v1 # Ruby gems cache version
|
||||
@@ -15,6 +15,9 @@ env:
|
||||
# Disable Maestro analytics in CI
|
||||
MAESTRO_CLI_NO_ANALYTICS: true
|
||||
MAESTRO_VERSION: 1.41.0
|
||||
MAESTRO_CACHE_VERSION: v1 # Bump this to clear Maestro cache
|
||||
# Disable Maestro recording/artifacts (keep for debugging - set to "true" to re-enable)
|
||||
ENABLE_MAESTRO_RECORDING: false
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -333,9 +336,35 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.maestro
|
||||
key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}
|
||||
key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}-${{ env.MAESTRO_CACHE_VERSION }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}-
|
||||
- name: Validate Maestro cache
|
||||
if: steps.cache-maestro.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
echo "✅ Maestro restored from cache"
|
||||
echo "Validating cached Maestro installation..."
|
||||
|
||||
if [ ! -f "$HOME/.maestro/bin/maestro" ]; then
|
||||
echo "❌ Maestro binary not found in cache - cache is corrupted"
|
||||
echo "Clearing corrupted cache..."
|
||||
rm -rf ~/.maestro
|
||||
echo "MAESTRO_CACHE_VALID=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "✅ Maestro binary found in cache"
|
||||
# Test if Maestro is executable
|
||||
if "$HOME/.maestro/bin/maestro" --version &>/dev/null; then
|
||||
echo "✅ Maestro cache is valid"
|
||||
echo "MAESTRO_CACHE_VALID=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "❌ Maestro binary is not executable - cache is corrupted"
|
||||
echo "Clearing corrupted cache..."
|
||||
rm -rf ~/.maestro
|
||||
echo "MAESTRO_CACHE_VALID=false" >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
- name: Install Maestro
|
||||
if: steps.cache-maestro.outputs.cache-hit != 'true'
|
||||
if: steps.cache-maestro.outputs.cache-hit != 'true' || env.MAESTRO_CACHE_VALID == 'false'
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 5
|
||||
@@ -344,6 +373,20 @@ jobs:
|
||||
command: curl -Ls "https://get.maestro.mobile.dev" | bash
|
||||
- name: Add Maestro to path
|
||||
run: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
|
||||
- name: Verify Maestro installation
|
||||
run: |
|
||||
echo "🔍 Verifying Maestro installation..."
|
||||
echo "Maestro path: $(which maestro)"
|
||||
echo "Maestro version:"
|
||||
maestro --version || {
|
||||
echo "❌ Maestro installation verification failed"
|
||||
echo "Maestro binary location:"
|
||||
find ~/.maestro -name maestro -type f 2>/dev/null || echo "No maestro binary found"
|
||||
echo ""
|
||||
echo "To fix: Bump MAESTRO_CACHE_VERSION in workflow file"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Maestro installation verified"
|
||||
- name: Set up Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
@@ -417,6 +460,7 @@ jobs:
|
||||
BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh
|
||||
env:
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
E2E_TESTING: 1
|
||||
- name: Setup iOS Simulator
|
||||
run: |
|
||||
echo "Setting up iOS Simulator..."
|
||||
@@ -504,6 +548,8 @@ jobs:
|
||||
echo "WORKSPACE_PATH=$WORKSPACE_PATH" >> "$GITHUB_ENV"
|
||||
echo "Resolved workspace: $WORKSPACE_PATH"
|
||||
- name: Build iOS App
|
||||
env:
|
||||
E2E_TESTING: 1
|
||||
run: |
|
||||
echo "Building iOS app..."
|
||||
echo "Project: ${{ env.IOS_PROJECT_NAME }}, Scheme: ${{ env.IOS_PROJECT_SCHEME }}"
|
||||
@@ -538,10 +584,13 @@ jobs:
|
||||
# Use cached derived data and enable parallel builds for faster compilation
|
||||
# Additional flags disable indexing, restrict architecture, and use whole-module Swift compilation
|
||||
# Use the simulator that was set up earlier in the workflow
|
||||
# E2E_TESTING compilation condition excludes NFCPassportReader which isn't available on simulator
|
||||
FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \
|
||||
xcodebuild -workspace "$WORKSPACE_PATH" -scheme ${{ env.IOS_PROJECT_SCHEME }} -configuration Debug -destination "id=${{ env.IOS_SIMULATOR_ID }}" -derivedDataPath app/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet COMPILER_INDEX_STORE_ENABLE=NO ONLY_ACTIVE_ARCH=YES SWIFT_COMPILATION_MODE=wholemodule || { echo "❌ iOS build failed"; exit 1; }
|
||||
xcodebuild -workspace "$WORKSPACE_PATH" -scheme ${{ env.IOS_PROJECT_SCHEME }} -configuration Debug -destination "id=${{ env.IOS_SIMULATOR_ID }}" -derivedDataPath app/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet COMPILER_INDEX_STORE_ENABLE=NO ONLY_ACTIVE_ARCH=YES SWIFT_COMPILATION_MODE=wholemodule 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) E2E_TESTING' || { echo "❌ iOS build failed"; exit 1; }
|
||||
echo "✅ iOS build succeeded"
|
||||
- name: Install and Test on iOS
|
||||
continue-on-error: true
|
||||
id: maestro-test
|
||||
run: |
|
||||
echo "Installing app on simulator..."
|
||||
APP_PATH=$(find app/ios/build/Build/Products/Debug-iphonesimulator -name "*.app" | head -1)
|
||||
@@ -589,20 +638,213 @@ jobs:
|
||||
# Final readiness check (suppress errors to avoid annotations)
|
||||
xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app >/dev/null 2>&1 || sleep 5
|
||||
|
||||
echo "🎭 Running Maestro tests..."
|
||||
echo "Starting test execution..."
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🔍 PRE-TEST DIAGNOSTICS ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Verify Maestro test file exists
|
||||
if [ ! -f "app/tests/e2e/launch.ios.flow.yaml" ]; then
|
||||
echo "❌ Maestro test file not found: app/tests/e2e/launch.ios.flow.yaml"
|
||||
# Create directory for test artifacts
|
||||
mkdir -p app/test-artifacts
|
||||
|
||||
# Check Maestro version and health
|
||||
echo "📦 Maestro installation details:"
|
||||
echo "Binary location: $(which maestro)"
|
||||
echo "Maestro version:"
|
||||
maestro --version || {
|
||||
echo "❌ Failed to get Maestro version"
|
||||
echo "This is a critical error - Maestro is not working"
|
||||
exit 1
|
||||
}
|
||||
echo ""
|
||||
|
||||
# Check Java (required by Maestro)
|
||||
echo "☕ Java status (required by Maestro):"
|
||||
if command -v java &>/dev/null; then
|
||||
java -version 2>&1 | head -3
|
||||
echo "✅ Java is available"
|
||||
else
|
||||
echo "⚠️ Java not found - this may cause Maestro to fail"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check simulator state
|
||||
echo "📱 Simulator state:"
|
||||
xcrun simctl list devices | grep -A 2 "$SIMULATOR_ID" || echo "⚠️ Simulator not found"
|
||||
echo ""
|
||||
|
||||
# Check if app is installed
|
||||
echo "📲 Checking app installation:"
|
||||
xcrun simctl listapps "$SIMULATOR_ID" | grep -i "$IOS_BUNDLE_ID" && echo "✅ App is installed" || echo "⚠️ App not found in installed apps"
|
||||
echo ""
|
||||
|
||||
# Check if app is running
|
||||
echo "🏃 Checking if app is running:"
|
||||
APP_PID=$(xcrun simctl spawn "$SIMULATOR_ID" launchctl list | grep "$IOS_BUNDLE_ID" | awk '{print $1}' || echo "")
|
||||
if [ -n "$APP_PID" ]; then
|
||||
echo "✅ App process found (PID: $APP_PID)"
|
||||
else
|
||||
echo "ℹ️ App not currently running (will be launched by Maestro)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Verify test file
|
||||
echo "📄 Maestro test file:"
|
||||
if [ -f "app/tests/e2e/launch.ios.flow.yaml" ]; then
|
||||
echo "✅ Found: app/tests/e2e/launch.ios.flow.yaml"
|
||||
echo "Contents:"
|
||||
cat app/tests/e2e/launch.ios.flow.yaml
|
||||
else
|
||||
echo "❌ Test file not found"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Note: Maestro 1.41.0 doesn't have daemon or devices commands
|
||||
# The test command handles daemon management internally
|
||||
echo "🔧 Maestro will manage its daemon automatically during test execution"
|
||||
echo ""
|
||||
|
||||
# Take a screenshot before running tests (if recording enabled)
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
|
||||
echo "📸 Taking pre-test screenshot..."
|
||||
xcrun simctl io "$SIMULATOR_ID" screenshot app/test-artifacts/pre-test-screenshot.png || echo "⚠️ Screenshot failed"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Run Maestro with error handling for cleanup issues
|
||||
# Note: Maestro may show NSPOSIXErrorDomain code=3 errors during cleanup when
|
||||
# terminating the test runner app that's already terminated. This is harmless.
|
||||
MAESTRO_OUTPUT=$(maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml 2>&1)
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🎭 STARTING MAESTRO TEST ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Start native simulator recording BEFORE running tests (if recording enabled)
|
||||
# This ensures we capture everything even if Maestro fails immediately
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
|
||||
echo "📹 Starting native simulator recording..."
|
||||
xcrun simctl io "$SIMULATOR_ID" recordVideo --codec=h264 app/test-artifacts/simulator-recording.mp4 &
|
||||
RECORDING_PID=$!
|
||||
echo "✅ Recording started (PID: $RECORDING_PID)"
|
||||
|
||||
# Give recording a moment to initialize
|
||||
sleep 2
|
||||
else
|
||||
echo "ℹ️ Maestro recording disabled (ENABLE_MAESTRO_RECORDING=false)"
|
||||
fi
|
||||
|
||||
# Run Maestro with verbose output and capture all output (including errors)
|
||||
echo "🎭 Starting Maestro test with verbose logging..."
|
||||
echo "Command: maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml"
|
||||
echo "Environment variables:"
|
||||
echo " MAESTRO_DEVICE_ID: $SIMULATOR_ID"
|
||||
echo " MAESTRO_DRIVER_STARTUP_TIMEOUT: 180000"
|
||||
echo " MAESTRO_CLI_NO_ANALYTICS: $MAESTRO_CLI_NO_ANALYTICS"
|
||||
echo " Working directory: $(pwd)"
|
||||
echo ""
|
||||
|
||||
set +e # Don't exit on error
|
||||
# Run with explicit device ID, increased timeout, and verbose output
|
||||
# Note: exit code 2 typically means Maestro couldn't connect to device or daemon
|
||||
MAESTRO_OUTPUT=$(MAESTRO_DEVICE_ID="$SIMULATOR_ID" MAESTRO_DRIVER_STARTUP_TIMEOUT=180000 maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml 2>&1)
|
||||
MAESTRO_EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
echo "Maestro command completed with exit code: $MAESTRO_EXIT_CODE"
|
||||
echo ""
|
||||
|
||||
# Stop the simulator recording (if recording was started)
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ] && [ -n "${RECORDING_PID:-}" ]; then
|
||||
echo ""
|
||||
echo "🛑 Stopping recording..."
|
||||
kill -SIGINT $RECORDING_PID 2>/dev/null || true
|
||||
wait $RECORDING_PID 2>/dev/null || true
|
||||
sleep 2 # Give time for video to finalize
|
||||
fi
|
||||
|
||||
# Take a screenshot after test (if recording enabled)
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
|
||||
echo "📸 Taking post-test screenshot..."
|
||||
xcrun simctl io "$SIMULATOR_ID" screenshot app/test-artifacts/post-test-screenshot.png || echo "⚠️ Screenshot failed"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Save Maestro output to file for debugging
|
||||
echo "$MAESTRO_OUTPUT" > app/test-artifacts/maestro-output.log
|
||||
echo "📝 Maestro output saved to maestro-output.log"
|
||||
|
||||
# Show Maestro output
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 📋 MAESTRO OUTPUT ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo "$MAESTRO_OUTPUT"
|
||||
echo ""
|
||||
|
||||
# Post-test diagnostics
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🔍 POST-TEST DIAGNOSTICS ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check if app is still running
|
||||
echo "🏃 App status after test:"
|
||||
APP_PID_AFTER=$(xcrun simctl spawn "$SIMULATOR_ID" launchctl list | grep "$IOS_BUNDLE_ID" | awk '{print $1}' || echo "")
|
||||
if [ -n "$APP_PID_AFTER" ]; then
|
||||
echo "✅ App still running (PID: $APP_PID_AFTER)"
|
||||
else
|
||||
echo "⚠️ App not running"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check simulator logs for any crashes
|
||||
echo "🔍 Checking for crash logs..."
|
||||
CRASH_LOGS=$(find ~/Library/Logs/DiagnosticReports -name "Self*.crash" -o -name "OpenPassport*.crash" -mmin -5 2>/dev/null | head -5)
|
||||
if [ -n "$CRASH_LOGS" ]; then
|
||||
echo "⚠️ Recent crash logs found:"
|
||||
echo "$CRASH_LOGS"
|
||||
# Copy crash logs to artifacts
|
||||
for log in $CRASH_LOGS; do
|
||||
cp "$log" app/test-artifacts/ 2>/dev/null || true
|
||||
done
|
||||
else
|
||||
echo "✅ No recent crash logs"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check system log for relevant messages
|
||||
echo "📋 Recent simulator system logs (last 50 lines):"
|
||||
xcrun simctl spawn "$SIMULATOR_ID" log show --predicate 'process == "Self" OR process == "OpenPassport"' --last 5m --style compact 2>/dev/null | tail -50 > app/test-artifacts/simulator-system.log || echo "⚠️ Could not retrieve system logs"
|
||||
if [ -f app/test-artifacts/simulator-system.log ]; then
|
||||
echo "✅ System logs saved to simulator-system.log"
|
||||
echo "Last 20 lines:"
|
||||
tail -20 app/test-artifacts/simulator-system.log
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check if video was created (if recording was enabled)
|
||||
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
|
||||
if [ -f "app/test-artifacts/simulator-recording.mp4" ]; then
|
||||
VIDEO_SIZE=$(ls -lh app/test-artifacts/simulator-recording.mp4 | awk '{print $5}')
|
||||
echo "✅ Video recording saved: simulator-recording.mp4 ($VIDEO_SIZE)"
|
||||
echo ""
|
||||
echo "📹 ===================================================="
|
||||
echo "📹 VIDEO RECORDING AVAILABLE"
|
||||
echo "📹 ===================================================="
|
||||
echo "📹 To view the test recording:"
|
||||
echo "📹 1. Go to: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
echo "📹 2. Scroll to the 'Artifacts' section at the bottom"
|
||||
echo "📹 3. Download 'maestro-artifacts-ios'"
|
||||
echo "📹 4. Extract and open 'simulator-recording.mp4'"
|
||||
echo "📹 ===================================================="
|
||||
echo ""
|
||||
else
|
||||
echo "⚠️ Video recording not created"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ Video recording disabled (ENABLE_MAESTRO_RECORDING=false)"
|
||||
fi
|
||||
|
||||
# Analyze test results
|
||||
echo "🔍 Analyzing test results..."
|
||||
echo "Maestro exit code: $MAESTRO_EXIT_CODE"
|
||||
|
||||
# Check if tests actually passed (ignore cleanup errors)
|
||||
if echo "$MAESTRO_OUTPUT" | grep -q "Flow Passed"; then
|
||||
@@ -616,20 +858,76 @@ jobs:
|
||||
fi
|
||||
elif echo "$MAESTRO_OUTPUT" | grep -q "Flow Failed"; then
|
||||
echo "❌ Maestro tests failed"
|
||||
echo "Check the video recording and maestro-output.log for details"
|
||||
exit 1
|
||||
elif [ $MAESTRO_EXIT_CODE -ne 0 ]; then
|
||||
# Check results file if exit code is non-zero
|
||||
if [ -f "app/maestro-results.xml" ] && ! grep -q "<failure" app/maestro-results.xml; then
|
||||
echo "✅ Tests passed (cleanup error caused non-zero exit)"
|
||||
else
|
||||
echo "❌ Maestro test failed"
|
||||
echo "❌ Maestro test failed with exit code: $MAESTRO_EXIT_CODE"
|
||||
echo "Check the video recording and maestro-output.log for details"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
if: always() && env.ENABLE_MAESTRO_RECORDING == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maestro-results-ios
|
||||
path: app/maestro-results.xml
|
||||
if-no-files-found: warn
|
||||
- name: Upload test artifacts (video and screenshots)
|
||||
if: always() && env.ENABLE_MAESTRO_RECORDING == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maestro-artifacts-ios
|
||||
path: app/test-artifacts/
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
- name: Artifact download instructions
|
||||
if: always() && env.ENABLE_MAESTRO_RECORDING == 'true'
|
||||
run: |
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 📹 TEST ARTIFACTS UPLOADED ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "📦 Artifact name: maestro-artifacts-ios"
|
||||
echo "🎬 Contains:"
|
||||
echo " 📹 simulator-recording.mp4 - Full video of simulator during test"
|
||||
echo " 📝 maestro-output.log - Complete Maestro command output"
|
||||
echo " 📋 simulator-system.log - iOS simulator system logs"
|
||||
echo " 📸 pre-test-screenshot.png - Simulator state before test"
|
||||
echo " 📸 post-test-screenshot.png - Simulator state after test"
|
||||
echo " 📊 maestro-results.xml - Test results (if generated)"
|
||||
echo " 💥 *.crash - Crash logs (if any crashes occurred)"
|
||||
echo "⏰ Retention: 7 days"
|
||||
echo ""
|
||||
echo "🔗 Direct link to this run:"
|
||||
echo " https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
echo ""
|
||||
echo "💡 How to download:"
|
||||
echo " 1. Click the link above (or find this run in the Actions tab)"
|
||||
echo " 2. Scroll down to the 'Artifacts' section"
|
||||
echo " 3. Click 'maestro-artifacts-ios' to download the ZIP"
|
||||
echo " 4. Extract and review:"
|
||||
echo " 🎬 Play simulator-recording.mp4 to see what happened"
|
||||
echo " 📝 Read maestro-output.log for Maestro errors"
|
||||
echo " 📋 Check simulator-system.log for iOS system errors"
|
||||
echo " 📸 Compare pre/post screenshots to see UI state"
|
||||
echo " 💥 Review crash logs if app crashed"
|
||||
echo ""
|
||||
echo "📝 Alternative - Using GitHub CLI:"
|
||||
echo " gh run download ${{ github.run_id }} -n maestro-artifacts-ios"
|
||||
echo ""
|
||||
|
||||
# List what files were actually created
|
||||
echo "📂 Files in test-artifacts directory:"
|
||||
ls -lh app/test-artifacts/ 2>/dev/null || echo " (No artifacts directory found)"
|
||||
echo ""
|
||||
- name: Fail job if tests failed
|
||||
if: steps.maestro-test.outcome == 'failure'
|
||||
run: |
|
||||
echo "❌ Maestro tests failed - failing job after artifact upload"
|
||||
exit 1
|
||||
|
||||
87
.github/workflows/mobile-sdk-ci.yml
vendored
87
.github/workflows/mobile-sdk-ci.yml
vendored
@@ -21,15 +21,10 @@ jobs:
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@v4
|
||||
uses: ./.github/actions/cache-mobile-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
packages/mobile-sdk-alpha/dist
|
||||
node_modules
|
||||
packages/mobile-sdk-alpha/node_modules
|
||||
common/node_modules
|
||||
key: mobile-sdk-alpha-build-${{ github.sha }}
|
||||
mode: save
|
||||
cache-version: v1
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -39,16 +34,17 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore build artifacts
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-build
|
||||
uses: ./.github/actions/cache-mobile-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
packages/mobile-sdk-alpha/dist
|
||||
node_modules
|
||||
packages/mobile-sdk-alpha/node_modules
|
||||
common/node_modules
|
||||
key: mobile-sdk-alpha-build-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: v1
|
||||
fail-on-cache-miss: false
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.restore-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Run linter
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha lint
|
||||
|
||||
@@ -60,16 +56,17 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore build artifacts
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-build
|
||||
uses: ./.github/actions/cache-mobile-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
packages/mobile-sdk-alpha/dist
|
||||
node_modules
|
||||
packages/mobile-sdk-alpha/node_modules
|
||||
common/node_modules
|
||||
key: mobile-sdk-alpha-build-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: v1
|
||||
fail-on-cache-miss: false
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.restore-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Check Prettier formatting
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha prettier --check .
|
||||
|
||||
@@ -81,16 +78,17 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore build artifacts
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-build
|
||||
uses: ./.github/actions/cache-mobile-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
packages/mobile-sdk-alpha/dist
|
||||
node_modules
|
||||
packages/mobile-sdk-alpha/node_modules
|
||||
common/node_modules
|
||||
key: mobile-sdk-alpha-build-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: v1
|
||||
fail-on-cache-miss: false
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.restore-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Type checking
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha types
|
||||
|
||||
@@ -102,15 +100,16 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore build artifacts
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-build
|
||||
uses: ./.github/actions/cache-mobile-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
packages/mobile-sdk-alpha/dist
|
||||
node_modules
|
||||
packages/mobile-sdk-alpha/node_modules
|
||||
common/node_modules
|
||||
key: mobile-sdk-alpha-build-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: v1
|
||||
fail-on-cache-miss: false
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.restore-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Run tests
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha test
|
||||
|
||||
35
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
35
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
@@ -5,7 +5,7 @@ env:
|
||||
JAVA_VERSION: 17
|
||||
ANDROID_API_LEVEL: 33
|
||||
ANDROID_NDK_VERSION: 27.0.12077973
|
||||
XCODE_VERSION: 16.4
|
||||
XCODE_VERSION: 26
|
||||
# Cache versions
|
||||
GH_CACHE_VERSION: v1
|
||||
GH_GEMS_CACHE_VERSION: v1
|
||||
@@ -457,8 +457,39 @@ jobs:
|
||||
fi
|
||||
|
||||
FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \
|
||||
xcodebuild -workspace "$WORKSPACE_PATH" -scheme ${{ env.IOS_PROJECT_SCHEME }} -configuration Debug -destination "id=${{ env.IOS_SIMULATOR_ID }}" -derivedDataPath packages/mobile-sdk-demo/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet COMPILER_INDEX_STORE_ENABLE=NO ONLY_ACTIVE_ARCH=YES SWIFT_COMPILATION_MODE=wholemodule || { echo "❌ iOS build failed"; exit 1; }
|
||||
xcodebuild -workspace "$WORKSPACE_PATH" -scheme ${{ env.IOS_PROJECT_SCHEME }} -configuration Debug -destination "id=${{ env.IOS_SIMULATOR_ID }}" -derivedDataPath packages/mobile-sdk-demo/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet COMPILER_INDEX_STORE_ENABLE=NO ONLY_ACTIVE_ARCH=YES SWIFT_COMPILATION_MODE=wholemodule 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) E2E_TESTING' || { echo "❌ iOS build failed"; exit 1; }
|
||||
echo "✅ iOS build succeeded"
|
||||
- name: Build iOS Release Archive (unsigned)
|
||||
run: |
|
||||
echo "Building iOS Release archive (unsigned) to validate Release configuration..."
|
||||
WORKSPACE_PATH="${{ env.IOS_WORKSPACE_PATH }}"
|
||||
|
||||
FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \
|
||||
xcodebuild archive \
|
||||
-workspace "$WORKSPACE_PATH" \
|
||||
-scheme ${{ env.IOS_PROJECT_SCHEME }} \
|
||||
-configuration Release \
|
||||
-archivePath packages/mobile-sdk-demo/ios/build/SelfDemoApp.xcarchive \
|
||||
-destination "generic/platform=iOS" \
|
||||
-jobs "$(sysctl -n hw.ncpu)" \
|
||||
-parallelizeTargets \
|
||||
-quiet \
|
||||
COMPILER_INDEX_STORE_ENABLE=NO \
|
||||
SWIFT_COMPILATION_MODE=wholemodule \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
AD_HOC_CODE_SIGNING_ALLOWED=NO \
|
||||
|| { echo "❌ iOS Release archive build failed"; exit 1; }
|
||||
echo "✅ iOS Release archive build succeeded (unsigned)"
|
||||
|
||||
# Verify archive was created
|
||||
if [ -d "packages/mobile-sdk-demo/ios/build/SelfDemoApp.xcarchive" ]; then
|
||||
echo "📦 Archive created at packages/mobile-sdk-demo/ios/build/SelfDemoApp.xcarchive"
|
||||
else
|
||||
echo "❌ Archive not found"
|
||||
exit 1
|
||||
fi
|
||||
- name: Install and Test on iOS
|
||||
run: |
|
||||
echo "Installing app on simulator..."
|
||||
|
||||
159
.github/workflows/npm-publish.yml
vendored
159
.github/workflows/npm-publish.yml
vendored
@@ -12,11 +12,26 @@ on:
|
||||
- "sdk/qrcode-angular/package.json"
|
||||
- "contracts/package.json"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
strict_mode:
|
||||
description: "Fail workflow on publish errors (false = continue on error)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
contents: read
|
||||
|
||||
# Error Handling Strategy:
|
||||
# - STRICT_PUBLISH_MODE controls whether publish failures stop the workflow
|
||||
# - Current (false): continue-on-error=true, workflow always succeeds
|
||||
# - Target (true): continue-on-error=false, fail on real errors (expired tokens, network issues)
|
||||
# - Manual override: Use workflow_dispatch with strict_mode input to test
|
||||
# TODO: Set STRICT_PUBLISH_MODE=true once NPM token is rotated and verified
|
||||
env:
|
||||
STRICT_PUBLISH_MODE: false
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -86,8 +101,21 @@ jobs:
|
||||
run: |
|
||||
yarn workspace @selfxyz/core build:deps
|
||||
|
||||
- name: Check NPM Token
|
||||
id: check-token
|
||||
run: |
|
||||
if [ -z "${{ secrets.NPM_TOKEN }}" ]; then
|
||||
echo "⚠️ Warning: NPM_TOKEN is not set. Skipping publish."
|
||||
echo "token_available=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "token_available=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.check-token.outputs.token_available == 'true'
|
||||
working-directory: sdk/core
|
||||
continue-on-error: ${{ github.event.inputs.strict_mode != 'true' && env.STRICT_PUBLISH_MODE != 'true' }}
|
||||
id: publish
|
||||
run: |
|
||||
yarn config set npmPublishAccess public
|
||||
yarn npm publish --access public
|
||||
@@ -95,6 +123,17 @@ jobs:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish result
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check-token.outputs.token_available }}" != "true" ]; then
|
||||
echo "::warning::NPM publish skipped - NPM_TOKEN not configured. Please rotate the token in repository secrets."
|
||||
elif [ "${{ steps.publish.outcome }}" != "success" ]; then
|
||||
echo "::warning::NPM publish failed - This may be due to an expired or invalid NPM_TOKEN. Please check and rotate the token."
|
||||
else
|
||||
echo "✅ Package published successfully"
|
||||
fi
|
||||
|
||||
publish-qrcode:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.qrcode_changed == 'true'
|
||||
@@ -114,8 +153,21 @@ jobs:
|
||||
run: |
|
||||
yarn workspace @selfxyz/qrcode build:deps
|
||||
|
||||
- name: Check NPM Token
|
||||
id: check-token
|
||||
run: |
|
||||
if [ -z "${{ secrets.NPM_TOKEN }}" ]; then
|
||||
echo "⚠️ Warning: NPM_TOKEN is not set. Skipping publish."
|
||||
echo "token_available=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "token_available=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.check-token.outputs.token_available == 'true'
|
||||
working-directory: sdk/qrcode
|
||||
continue-on-error: ${{ github.event.inputs.strict_mode != 'true' && env.STRICT_PUBLISH_MODE != 'true' }}
|
||||
id: publish
|
||||
run: |
|
||||
yarn config set npmPublishAccess public
|
||||
yarn npm publish --access public
|
||||
@@ -123,6 +175,17 @@ jobs:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish result
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check-token.outputs.token_available }}" != "true" ]; then
|
||||
echo "::warning::NPM publish skipped - NPM_TOKEN not configured. Please rotate the token in repository secrets."
|
||||
elif [ "${{ steps.publish.outcome }}" != "success" ]; then
|
||||
echo "::warning::NPM publish failed - This may be due to an expired or invalid NPM_TOKEN. Please check and rotate the token."
|
||||
else
|
||||
echo "✅ Package published successfully"
|
||||
fi
|
||||
|
||||
publish-common:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.common_changed == 'true'
|
||||
@@ -141,14 +204,38 @@ jobs:
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
|
||||
- name: Check NPM Token
|
||||
id: check-token
|
||||
run: |
|
||||
if [ -z "${{ secrets.NPM_TOKEN }}" ]; then
|
||||
echo "⚠️ Warning: NPM_TOKEN is not set. Skipping publish."
|
||||
echo "token_available=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "token_available=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.check-token.outputs.token_available == 'true'
|
||||
working-directory: common
|
||||
continue-on-error: ${{ github.event.inputs.strict_mode != 'true' && env.STRICT_PUBLISH_MODE != 'true' }}
|
||||
id: publish
|
||||
run: |
|
||||
yarn config set npmPublishAccess public
|
||||
yarn npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish result
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check-token.outputs.token_available }}" != "true" ]; then
|
||||
echo "::warning::NPM publish skipped - NPM_TOKEN not configured. Please rotate the token in repository secrets."
|
||||
elif [ "${{ steps.publish.outcome }}" != "success" ]; then
|
||||
echo "::warning::NPM publish failed - This may be due to an expired or invalid NPM_TOKEN. Please check and rotate the token."
|
||||
else
|
||||
echo "✅ Package published successfully"
|
||||
fi
|
||||
publish-contracts:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.contracts_changed == 'true'
|
||||
@@ -165,14 +252,38 @@ jobs:
|
||||
- name: Build package
|
||||
run: |
|
||||
yarn workspace @selfxyz/contracts build
|
||||
- name: Check NPM Token
|
||||
id: check-token
|
||||
run: |
|
||||
if [ -z "${{ secrets.NPM_TOKEN }}" ]; then
|
||||
echo "⚠️ Warning: NPM_TOKEN is not set. Skipping publish."
|
||||
echo "token_available=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "token_available=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.check-token.outputs.token_available == 'true'
|
||||
working-directory: contracts
|
||||
continue-on-error: ${{ github.event.inputs.strict_mode != 'true' && env.STRICT_PUBLISH_MODE != 'true' }}
|
||||
id: publish
|
||||
run: |
|
||||
yarn config set npmPublishAccess public
|
||||
yarn npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish result
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check-token.outputs.token_available }}" != "true" ]; then
|
||||
echo "::warning::NPM publish skipped - NPM_TOKEN not configured. Please rotate the token in repository secrets."
|
||||
elif [ "${{ steps.publish.outcome }}" != "success" ]; then
|
||||
echo "::warning::NPM publish failed - This may be due to an expired or invalid NPM_TOKEN. Please check and rotate the token."
|
||||
else
|
||||
echo "✅ Package published successfully"
|
||||
fi
|
||||
publish-qrcode-angular:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.qrcode_angular_changed == 'true'
|
||||
@@ -192,8 +303,21 @@ jobs:
|
||||
run: |
|
||||
yarn workspace @selfxyz/qrcode-angular build:deps
|
||||
|
||||
- name: Check NPM Token
|
||||
id: check-token
|
||||
run: |
|
||||
if [ -z "${{ secrets.NPM_TOKEN }}" ]; then
|
||||
echo "⚠️ Warning: NPM_TOKEN is not set. Skipping publish."
|
||||
echo "token_available=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "token_available=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.check-token.outputs.token_available == 'true'
|
||||
working-directory: sdk/qrcode-angular
|
||||
continue-on-error: ${{ github.event.inputs.strict_mode != 'true' && env.STRICT_PUBLISH_MODE != 'true' }}
|
||||
id: publish
|
||||
run: |
|
||||
yarn config set npmPublishAccess public
|
||||
yarn npm publish --access public
|
||||
@@ -201,6 +325,17 @@ jobs:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish result
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check-token.outputs.token_available }}" != "true" ]; then
|
||||
echo "::warning::NPM publish skipped - NPM_TOKEN not configured. Please rotate the token in repository secrets."
|
||||
elif [ "${{ steps.publish.outcome }}" != "success" ]; then
|
||||
echo "::warning::NPM publish failed - This may be due to an expired or invalid NPM_TOKEN. Please check and rotate the token."
|
||||
else
|
||||
echo "✅ Package published successfully"
|
||||
fi
|
||||
|
||||
publish-msdk:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.msdk_changed == 'true'
|
||||
@@ -221,11 +356,35 @@ jobs:
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
|
||||
- name: Check NPM Token
|
||||
id: check-token
|
||||
run: |
|
||||
if [ -z "${{ secrets.NPM_TOKEN }}" ]; then
|
||||
echo "⚠️ Warning: NPM_TOKEN is not set. Skipping publish."
|
||||
echo "token_available=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "token_available=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.check-token.outputs.token_available == 'true'
|
||||
working-directory: packages/mobile-sdk-alpha
|
||||
continue-on-error: ${{ github.event.inputs.strict_mode != 'true' && env.STRICT_PUBLISH_MODE != 'true' }}
|
||||
id: publish
|
||||
run: |
|
||||
yarn config set npmPublishAccess restricted
|
||||
yarn npm publish --access restricted --tag alpha
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish result
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check-token.outputs.token_available }}" != "true" ]; then
|
||||
echo "::warning::NPM publish skipped - NPM_TOKEN not configured. Please rotate the token in repository secrets."
|
||||
elif [ "${{ steps.publish.outcome }}" != "success" ]; then
|
||||
echo "::warning::NPM publish failed - This may be due to an expired or invalid NPM_TOKEN. Please check and rotate the token."
|
||||
else
|
||||
echo "✅ Package published successfully"
|
||||
fi
|
||||
|
||||
99
.github/workflows/qrcode-sdk-ci.yml
vendored
99
.github/workflows/qrcode-sdk-ci.yml
vendored
@@ -80,16 +80,14 @@ jobs:
|
||||
|
||||
- name: Cache Yarn dependencies
|
||||
id: yarn-cache
|
||||
uses: actions/cache@v4
|
||||
uses: ./.github/actions/cache-yarn
|
||||
with:
|
||||
path: |
|
||||
.yarn/cache
|
||||
node_modules
|
||||
sdk/qrcode/node_modules
|
||||
common/node_modules
|
||||
key: ${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-
|
||||
cache-version: ${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
@@ -102,13 +100,10 @@ jobs:
|
||||
yarn workspace @selfxyz/qrcode build
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@v4
|
||||
uses: ./.github/actions/cache-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||
mode: save
|
||||
cache-version: ${{ env.GH_SDK_CACHE_VERSION }}
|
||||
|
||||
# Quality checks job
|
||||
quality-checks:
|
||||
@@ -141,29 +136,32 @@ jobs:
|
||||
|
||||
- name: Cache Yarn dependencies
|
||||
id: yarn-cache
|
||||
uses: actions/cache@v4
|
||||
uses: ./.github/actions/cache-yarn
|
||||
with:
|
||||
path: |
|
||||
.yarn/cache
|
||||
node_modules
|
||||
sdk/qrcode/node_modules
|
||||
common/node_modules
|
||||
key: ${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-
|
||||
cache-version: ${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Restore build artifacts
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-build
|
||||
uses: ./.github/actions/cache-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: ${{ env.GH_SDK_CACHE_VERSION }}
|
||||
fail-on-cache-miss: false
|
||||
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.restore-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/sdk-common build
|
||||
yarn workspace @selfxyz/qrcode build
|
||||
|
||||
- name: Run linter
|
||||
run: yarn workspace @selfxyz/qrcode lint:imports:check
|
||||
@@ -210,29 +208,32 @@ jobs:
|
||||
|
||||
- name: Cache Yarn dependencies
|
||||
id: yarn-cache
|
||||
uses: actions/cache@v4
|
||||
uses: ./.github/actions/cache-yarn
|
||||
with:
|
||||
path: |
|
||||
.yarn/cache
|
||||
node_modules
|
||||
sdk/qrcode/node_modules
|
||||
common/node_modules
|
||||
key: ${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-
|
||||
cache-version: ${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Restore build artifacts
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-build
|
||||
uses: ./.github/actions/cache-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: ${{ env.GH_SDK_CACHE_VERSION }}
|
||||
fail-on-cache-miss: false
|
||||
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.restore-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/sdk-common build
|
||||
yarn workspace @selfxyz/qrcode build
|
||||
|
||||
- name: Verify build artifacts
|
||||
run: |
|
||||
@@ -273,29 +274,41 @@ jobs:
|
||||
|
||||
- name: Cache Yarn dependencies
|
||||
id: yarn-cache
|
||||
uses: actions/cache@v4
|
||||
uses: ./.github/actions/cache-yarn
|
||||
with:
|
||||
path: |
|
||||
.yarn/cache
|
||||
node_modules
|
||||
sdk/qrcode/node_modules
|
||||
common/node_modules
|
||||
key: ${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-
|
||||
cache-version: ${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Restore build artifacts
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-build
|
||||
uses: ./.github/actions/cache-sdk-build
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
mode: restore
|
||||
cache-version: ${{ env.GH_SDK_CACHE_VERSION }}
|
||||
fail-on-cache-miss: false
|
||||
|
||||
- name: Build dependencies (fallback on cache miss)
|
||||
if: steps.restore-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn workspace @selfxyz/common build
|
||||
yarn workspace @selfxyz/sdk-common build
|
||||
yarn workspace @selfxyz/qrcode build
|
||||
|
||||
- name: Check for nested require() in tests
|
||||
run: |
|
||||
# Check SDK tests for nested require patterns that cause OOM
|
||||
if grep -rE "require\(['\"]react(-native)?['\"])" sdk/qrcode/src/ sdk/qrcode/tests/ 2>/dev/null; then
|
||||
echo "❌ Found nested require() patterns that cause OOM in CI"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ No nested require() patterns found"
|
||||
|
||||
- name: Run tests
|
||||
run: yarn workspace @selfxyz/qrcode test
|
||||
|
||||
36
.github/workflows/release-calendar.yml
vendored
36
.github/workflows/release-calendar.yml
vendored
@@ -153,12 +153,28 @@ jobs:
|
||||
echo "✓ Successfully pushed branch ${BRANCH_NAME}"
|
||||
fi
|
||||
|
||||
- name: Read app version
|
||||
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr == '' }}
|
||||
id: app_version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python - <<PY >> "$GITHUB_OUTPUT"
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
package_json = pathlib.Path("app/package.json")
|
||||
version = json.loads(package_json.read_text())["version"]
|
||||
print(f"app_version={version}")
|
||||
PY
|
||||
|
||||
- name: Create dev to staging release PR
|
||||
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr == '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_DATE: ${{ steps.check_dev_staging.outputs.date }}
|
||||
BRANCH_NAME: ${{ steps.check_dev_staging.outputs.branch_name }}
|
||||
APP_VERSION: ${{ steps.app_version.outputs.app_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -200,7 +216,7 @@ jobs:
|
||||
"""))
|
||||
PY
|
||||
|
||||
TITLE="Release to Staging - ${PR_DATE}"
|
||||
TITLE="Release to Staging v${APP_VERSION} - ${PR_DATE}"
|
||||
echo "Creating PR with title: ${TITLE} from branch ${BRANCH_NAME}"
|
||||
|
||||
if ! gh pr create \
|
||||
@@ -319,12 +335,28 @@ jobs:
|
||||
gh label create "${LABEL}" --color BFD4F2 --force || true
|
||||
done
|
||||
|
||||
- name: Read app version
|
||||
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.production_status.outputs.staging_not_ahead != 'true' && steps.production_status.outputs.existing_pr == '' }}
|
||||
id: app_version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python - <<PY >> "$GITHUB_OUTPUT"
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
package_json = pathlib.Path("app/package.json")
|
||||
version = json.loads(package_json.read_text())["version"]
|
||||
print(f"app_version={version}")
|
||||
PY
|
||||
|
||||
- name: Create staging to main release PR
|
||||
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.production_status.outputs.staging_not_ahead != 'true' && steps.production_status.outputs.existing_pr == '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_DATE: ${{ steps.production_status.outputs.date }}
|
||||
COMMITS_AHEAD: ${{ steps.production_status.outputs.commits }}
|
||||
APP_VERSION: ${{ steps.app_version.outputs.app_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -367,7 +399,7 @@ jobs:
|
||||
"""))
|
||||
PY
|
||||
|
||||
TITLE="Release to Production - ${PR_DATE}"
|
||||
TITLE="Release to Production v${APP_VERSION} - ${PR_DATE}"
|
||||
echo "Creating PR with title: ${TITLE} from staging with ${COMMITS_AHEAD} commits ahead."
|
||||
|
||||
gh pr create \
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
8bc1e85075f73906767652ab35d5563efce2a931:packages/mobile-sdk-alpha/src/animations/passport_verify.json:aws-access-token:6
|
||||
0e4555eee6589aa9cca68f451227b149277d8c90:app/tests/src/utils/points/api.test.ts:generic-api-key:34
|
||||
circuits/circuits/gcp_jwt_verifier/example_jwt.txt:jwt:1
|
||||
circuits/circuits/gcp_jwt_verifier/example_jwt_fail.txt:jwt:1
|
||||
cadd7ae5b768c261230f84426eac879c1853ce70:app/ios/Podfile.lock:generic-api-key:2586
|
||||
aeb8287078f088ecd8e9430e3f6a9f2c593ef1fc:app/src/utils/points/constants.ts:generic-api-key:7
|
||||
app/src/services/points/constants.ts:generic-api-key:10
|
||||
|
||||
@@ -101,4 +101,3 @@ We are actively looking for contributors. Please check the [open issues](https:/
|
||||
Thanks [Rémi](https://github.com/remicolin), [Florent](https://github.com/0xturboblitz), [Ayman](https://github.com/Nesopie), [Justin](https://github.com/transphorm), [Seshanth](https://github.com/seshanthS), [Nico](https://github.com/motemotech) and all other contributors for building Self.
|
||||
|
||||
Thanks [Aayush](https://twitter.com/yush_g), [Vivek](https://twitter.com/viv_boop), [Andy](https://twitter.com/AndyGuzmanEth) and [Vitalik](https://github.com/vbuterin) for contributing ideas and inspiring us to build this technology, and [PSE](https://pse.dev/) for supporting the initial work through grants!
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1201.0)
|
||||
aws-sdk-core (3.241.3)
|
||||
aws-partitions (1.1213.0)
|
||||
aws-sdk-core (3.242.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -32,11 +32,11 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.120.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-kms (1.121.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.211.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-s3 (1.213.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
@@ -230,7 +230,7 @@ GEM
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.18.0)
|
||||
json (2.18.1)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
@@ -254,7 +254,7 @@ GEM
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
prism (1.7.0)
|
||||
prism (1.9.0)
|
||||
public_suffix (4.0.7)
|
||||
racc (1.8.1)
|
||||
rake (13.3.1)
|
||||
|
||||
@@ -134,8 +134,8 @@ android {
|
||||
applicationId "com.proofofpassportapp"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 121
|
||||
versionName "2.9.11"
|
||||
versionCode 140
|
||||
versionName "2.9.15"
|
||||
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
@@ -231,8 +231,9 @@ dependencies {
|
||||
implementation "com.google.guava:guava:31.1-android"
|
||||
implementation "androidx.profileinstaller:profileinstaller:1.3.1"
|
||||
|
||||
implementation "androidx.activity:activity:1.9.3"
|
||||
implementation "androidx.activity:activity-ktx:1.9.3"
|
||||
implementation "androidx.activity:activity:1.10.1"
|
||||
implementation "androidx.activity:activity-ktx:1.10.1"
|
||||
implementation "com.google.android.material:material:1.12.0"
|
||||
|
||||
implementation "com.google.android.play:app-update:2.1.0"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:usesCleartextTraffic"
|
||||
/>
|
||||
tools:replace="android:usesCleartextTraffic">
|
||||
|
||||
<!-- Override conflicting ML Kit dependencies from passportreader (ocr) and Sumsub SDK (face) -->
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="ocr,face"
|
||||
tools:replace="android:value" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
@@ -25,7 +27,8 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:extractNativeLibs="false"
|
||||
tools:replace="android:icon, android:roundIcon, android:name, android:extractNativeLibs"
|
||||
android:allowBackup="false"
|
||||
tools:replace="android:icon, android:roundIcon, android:name, android:extractNativeLibs, android:allowBackup"
|
||||
android:theme="@style/AppTheme"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="false"
|
||||
@@ -71,6 +74,7 @@
|
||||
|
||||
<service
|
||||
android:name="com.google.firebase.messaging.FirebaseMessagingService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
@@ -105,5 +109,11 @@
|
||||
</intent-filter>
|
||||
<meta-data android:name="photopicker_activity:0:required" android:value="" />
|
||||
</service>
|
||||
|
||||
<!-- Override conflicting ML Kit dependencies from passportreader (ocr) and Sumsub SDK (face) -->
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="ocr,face"
|
||||
tools:replace="android:value" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -5,9 +5,8 @@ package com.proofofpassportapp
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.graphics.Color
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
@@ -44,12 +43,11 @@ class MainActivity : ReactActivity() {
|
||||
// Prevent fragment state restoration to avoid react-native-screens crash
|
||||
// See: https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704978
|
||||
super.onCreate(null)
|
||||
// Ensure edge-to-edge is enabled consistently across Android versions using
|
||||
// the AndroidX helper so deprecated window color APIs are avoided.
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)
|
||||
)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
WindowInsetsControllerCompat(window, window.decorView).apply {
|
||||
systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
// Allow system to manage orientation for large screens
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,11 +256,15 @@ class CameraMLKitFragment(cameraMLKitCallback: CameraMLKitCallback) : CameraFrag
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
OcrUtils.processOcr(
|
||||
results = results,
|
||||
timeRequired = timeRequired,
|
||||
callback = mrzListener
|
||||
)
|
||||
try {
|
||||
OcrUtils.processOcr(
|
||||
results = results,
|
||||
timeRequired = timeRequired,
|
||||
callback = mrzListener
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
mrzListener.onFailure(e, timeRequired)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCanceled(timeRequired: Long) {
|
||||
|
||||
@@ -41,6 +41,7 @@ allprojects {
|
||||
url("$rootDir/../../node_modules/jsc-android/dist")
|
||||
}
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url "https://maven.sumsub.com/repository/maven-public/" }
|
||||
}
|
||||
configurations.configureEach {
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
|
||||
@@ -8,4 +8,5 @@ IS_TEST_BUILD=
|
||||
MIXPANEL_NFC_PROJECT_TOKEN=
|
||||
SEGMENT_KEY=
|
||||
SENTRY_DSN=
|
||||
SUMSUB_TEE_URL=
|
||||
IS_TEST_BUILD=
|
||||
|
||||
@@ -28,9 +28,11 @@ export const IS_TEST_BUILD = process.env.IS_TEST_BUILD === 'true';
|
||||
export const MIXPANEL_NFC_PROJECT_TOKEN = undefined;
|
||||
export const SEGMENT_KEY = process.env.SEGMENT_KEY;
|
||||
export const SENTRY_DSN = process.env.SENTRY_DSN;
|
||||
export const SUMSUB_TEE_URL =
|
||||
process.env.SUMSUB_TEE_URL || 'http://localhost:8080';
|
||||
export const SUMSUB_TEST_TOKEN = process.env.SUMSUB_TEST_TOKEN;
|
||||
|
||||
export const TURNKEY_AUTH_PROXY_CONFIG_ID =
|
||||
process.env.TURNKEY_AUTH_PROXY_CONFIG_ID;
|
||||
|
||||
export const TURNKEY_GOOGLE_CLIENT_ID = process.env.TURNKEY_GOOGLE_CLIENT_ID;
|
||||
export const TURNKEY_ORGANIZATION_ID = process.env.TURNKEY_ORGANIZATION_ID;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.9.11</string>
|
||||
<string>2.9.15</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -70,7 +70,7 @@
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string></string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<string>We use your location to improve document scanning reliability, as performance can vary by region and document type.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need access to your photo library to allow you to choose passport photos or save generated QR codes.</string>
|
||||
<key>UIAppFonts</key>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>production</string>
|
||||
<key>aps-environment</key>
|
||||
<string>production</string>
|
||||
<key>com.apple.developer.associated-appclip-app-identifiers</key>
|
||||
<array>
|
||||
<string>5B29R5LYHQ.com.warroom.proofofpassport.Clip</string>
|
||||
@@ -37,7 +37,5 @@
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -39,7 +39,5 @@
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -11,9 +11,9 @@ import Foundation
|
||||
import React
|
||||
#if !E2E_TESTING
|
||||
import NFCPassportReader
|
||||
import Mixpanel
|
||||
#endif
|
||||
import Security
|
||||
import Mixpanel
|
||||
import Sentry
|
||||
|
||||
#if !E2E_TESTING
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
source "https://cdn.cocoapods.org/"
|
||||
|
||||
# Skip Sumsub configuration for E2E testing
|
||||
unless ENV["E2E_TESTING"] == "1"
|
||||
source "https://github.com/SumSubstance/Specs.git"
|
||||
|
||||
# Enable Fisherman (Device Intelligence) module for fraud detection
|
||||
# Privacy: Device ID collection declared in app/ios/PrivacyInfo.xcprivacy
|
||||
ENV["IDENSIC_WITH_FISHERMAN"] = "true"
|
||||
|
||||
# VideoIdent module disabled for current release
|
||||
# This feature provides liveness checks via live video calls with human agents
|
||||
# Disabled to avoid microphone permission requirements on both platforms
|
||||
# TODO: Re-enable for future release when liveness checks are needed
|
||||
# ENV["IDENSIC_WITH_VIDEOIDENT"] = "true"
|
||||
end
|
||||
|
||||
use_frameworks!
|
||||
require "tmpdir"
|
||||
|
||||
@@ -39,9 +55,8 @@ def using_https_git_auth?
|
||||
end
|
||||
|
||||
target "Self" do
|
||||
# Native module exclusion for E2E testing is handled in react-native.config.cjs
|
||||
config = use_native_modules!
|
||||
|
||||
use_frameworks!
|
||||
# Skip NFCPassportReader for e2e testing to avoid build issues
|
||||
unless ENV["E2E_TESTING"] == "1"
|
||||
# Check if we're running in a selfxyz repo or an external fork
|
||||
@@ -66,10 +81,13 @@ target "Self" do
|
||||
pod "NFCPassportReader", git: nfc_repo_url, commit: "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b"
|
||||
end
|
||||
|
||||
# Explicitly declare Mixpanel to ensure it's available even in E2E builds
|
||||
# (NFCPassportReader also includes Mixpanel, but is skipped during E2E testing)
|
||||
pod "Mixpanel-swift", :modular_headers => true
|
||||
|
||||
pod "QKMRZScanner"
|
||||
pod "lottie-ios"
|
||||
pod "SwiftQRScanner", :git => "https://github.com/vinodiOS/SwiftQRScanner"
|
||||
pod "Mixpanel-swift", "~> 5.0.0"
|
||||
# RNReactNativeHapticFeedback is handled by autolinking
|
||||
|
||||
use_react_native!(
|
||||
|
||||
@@ -11,6 +11,7 @@ PODS:
|
||||
- DoubleConversion (1.1.6)
|
||||
- fast_float (6.1.4)
|
||||
- FBLazyVector (0.76.9)
|
||||
- FingerprintPro (2.12.0)
|
||||
- Firebase (10.24.0):
|
||||
- Firebase/Core (= 10.24.0)
|
||||
- Firebase/Core (10.24.0):
|
||||
@@ -149,6 +150,14 @@ PODS:
|
||||
- hermes-engine (0.76.9):
|
||||
- hermes-engine/Pre-built (= 0.76.9)
|
||||
- hermes-engine/Pre-built (0.76.9)
|
||||
- IdensicMobileSDK (1.40.2):
|
||||
- IdensicMobileSDK/Default (= 1.40.2)
|
||||
- IdensicMobileSDK/Core (1.40.2)
|
||||
- IdensicMobileSDK/Default (1.40.2):
|
||||
- IdensicMobileSDK/Core
|
||||
- IdensicMobileSDK/Fisherman (1.40.2):
|
||||
- FingerprintPro (~> 2.11)
|
||||
- IdensicMobileSDK/Core
|
||||
- lottie-ios (4.5.0)
|
||||
- lottie-react-native (7.2.2):
|
||||
- DoubleConversion
|
||||
@@ -1537,9 +1546,13 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-get-random-values (1.11.0):
|
||||
- React-Core
|
||||
- react-native-mobilesdk-module (1.40.2):
|
||||
- IdensicMobileSDK (= 1.40.2)
|
||||
- IdensicMobileSDK/Fisherman (= 1.40.2)
|
||||
- React-Core
|
||||
- react-native-netinfo (11.4.1):
|
||||
- React-Core
|
||||
- react-native-nfc-manager (3.16.3):
|
||||
- react-native-nfc-manager (3.17.2):
|
||||
- React-Core
|
||||
- react-native-passkey (3.3.1):
|
||||
- DoubleConversion
|
||||
@@ -1965,7 +1978,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNDeviceInfo (14.1.1):
|
||||
- RNDeviceInfo (15.0.1):
|
||||
- React-Core
|
||||
- RNFBApp (19.3.0):
|
||||
- Firebase/CoreOnly (= 10.24.0)
|
||||
@@ -2023,7 +2036,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNLocalize (3.6.0):
|
||||
- RNLocalize (3.6.1):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2110,7 +2123,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNSentry (7.0.1):
|
||||
- RNSentry (7.0.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2133,7 +2146,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- Sentry/HybridSDK (= 8.53.2)
|
||||
- Yoga
|
||||
- RNSVG (15.14.0):
|
||||
- RNSVG (15.12.1):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2153,9 +2166,9 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNSVG/common (= 15.14.0)
|
||||
- RNSVG/common (= 15.12.1)
|
||||
- Yoga
|
||||
- RNSVG/common (15.14.0):
|
||||
- RNSVG/common (15.12.1):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2203,7 +2216,7 @@ DEPENDENCIES:
|
||||
- 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 (~> 5.0.0)
|
||||
- Mixpanel-swift
|
||||
- "NFCPassportReader (from `git@github.com:selfxyz/NFCPassportReader.git`, commit `9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b`)"
|
||||
- QKMRZScanner
|
||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
@@ -2243,6 +2256,7 @@ DEPENDENCIES:
|
||||
- react-native-cloud-storage (from `../node_modules/react-native-cloud-storage`)
|
||||
- "react-native-compat (from `../node_modules/@walletconnect/react-native-compat`)"
|
||||
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
|
||||
- "react-native-mobilesdk-module (from `../node_modules/@sumsub/react-native-mobilesdk-module`)"
|
||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||
- react-native-nfc-manager (from `../node_modules/react-native-nfc-manager`)
|
||||
- react-native-passkey (from `../node_modules/react-native-passkey`)
|
||||
@@ -2296,8 +2310,11 @@ DEPENDENCIES:
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/SumSubstance/Specs.git:
|
||||
- IdensicMobileSDK
|
||||
trunk:
|
||||
- AppAuth
|
||||
- FingerprintPro
|
||||
- Firebase
|
||||
- FirebaseABTesting
|
||||
- FirebaseAnalytics
|
||||
@@ -2416,6 +2433,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/@walletconnect/react-native-compat"
|
||||
react-native-get-random-values:
|
||||
:path: "../node_modules/react-native-get-random-values"
|
||||
react-native-mobilesdk-module:
|
||||
:path: "../node_modules/@sumsub/react-native-mobilesdk-module"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-nfc-manager:
|
||||
@@ -2534,6 +2553,7 @@ SPEC CHECKSUMS:
|
||||
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45
|
||||
FingerprintPro: 035517a1b4e3e4fc073486b53b9956509010f8db
|
||||
Firebase: 91fefd38712feb9186ea8996af6cbdef41473442
|
||||
FirebaseABTesting: d87f56707159bae64e269757a6e963d490f2eebe
|
||||
FirebaseAnalytics: b5efc493eb0f40ec560b04a472e3e1a15d39ca13
|
||||
@@ -2551,6 +2571,7 @@ SPEC CHECKSUMS:
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
|
||||
IdensicMobileSDK: 00b13320e1b1e0574e68475bd0fbc7cd30fce26e
|
||||
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||
lottie-react-native: 7bb65bc88d3f9996ea2f646a96694285405df2f9
|
||||
Mixpanel-swift: e9bef28a9648faff384d5ba6f48ecc2787eb24c0
|
||||
@@ -2595,8 +2616,9 @@ SPEC CHECKSUMS:
|
||||
react-native-cloud-storage: 8d89f2bc574cf11068dfd90933905974087fb9e9
|
||||
react-native-compat: b80530ebcd3d574be5dd99cb27b984a17c119abc
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
react-native-mobilesdk-module: 08c16fea2be97669f8e4c38153106e5fe698126a
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-nfc-manager: 66a00e5ddab9704efebe19d605b1b8afb0bb1bd7
|
||||
react-native-nfc-manager: c8891e460b4943b695d63f7f4effc6345bbefc83
|
||||
react-native-passkey: 8853c3c635164864da68a6dbbcec7148506c3bcf
|
||||
react-native-safe-area-context: a7aad44fe544b55e2369a3086e16a01be60ce398
|
||||
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
|
||||
@@ -2630,18 +2652,18 @@ SPEC CHECKSUMS:
|
||||
ReactCommon: b2eb96a61b826ff327a773a74357b302cf6da678
|
||||
RNCAsyncStorage: 0003b916f1a69fe2d20b7910e0d08da3d32c7bd6
|
||||
RNCClipboard: a4827e134e4774e97fa86f7f986694dd89320f13
|
||||
RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c
|
||||
RNDeviceInfo: 36d7f232bfe7c9b5c494cb7793230424ed32c388
|
||||
RNFBApp: 4097f75673f8b42a7cd1ba17e6ea85a94b45e4d1
|
||||
RNFBMessaging: 92325b0d5619ac90ef023a23cfd16fd3b91d0a88
|
||||
RNFBRemoteConfig: a569bacaa410acfcaba769370e53a787f80fd13b
|
||||
RNGestureHandler: a63b531307e5b2e6ea21d053a1a7ad4cf9695c57
|
||||
RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20
|
||||
RNKeychain: 471ceef8c13f15a5534c3cd2674dbbd9d0680e52
|
||||
RNLocalize: 4f5e4a46d2bccd04ccb96721e438dcb9de17c2e0
|
||||
RNLocalize: 2760999d1e2fc95fb7b7e5247631feb3c08156dc
|
||||
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
|
||||
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
|
||||
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
|
||||
RNSVG: e1cf5a9a5aa12c69f2ec47031defbd87ae7fb697
|
||||
RNSentry: f79dd124cc49088445c16d23955860dd0d1db6f3
|
||||
RNSVG: 0c1fc3e7b147949dc15644845e9124947ac8c9bb
|
||||
segment-analytics-react-native: 0eae155b0e9fa560fa6b17d78941df64537c35b7
|
||||
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
@@ -2650,6 +2672,6 @@ SPEC CHECKSUMS:
|
||||
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
|
||||
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
|
||||
|
||||
PODFILE CHECKSUM: 0aa47f53692543349c43673cda7380fa23049eba
|
||||
PODFILE CHECKSUM: ced4db0072978f965783277bc810af9a7bebe695
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -30,7 +30,20 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeDeviceID</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<true/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeFraudPreventionAndSecurity</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1620;
|
||||
LastUpgradeCheck = 2620;
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
LastSwiftMigration = 1430;
|
||||
@@ -433,8 +433,9 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 5B29R5LYHQ;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CAMERA = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"${PODS_CONFIGURATION_BUILD_DIR}/DoubleConversion\"",
|
||||
@@ -546,7 +547,7 @@
|
||||
"$(PROJECT_DIR)",
|
||||
"$(PROJECT_DIR)/MoproKit/Libs",
|
||||
);
|
||||
MARKETING_VERSION = 2.9.11;
|
||||
MARKETING_VERSION = 2.9.15;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -574,7 +575,8 @@
|
||||
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 5B29R5LYHQ;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CAMERA = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"${PODS_CONFIGURATION_BUILD_DIR}/DoubleConversion\"",
|
||||
@@ -686,7 +688,7 @@
|
||||
"$(PROJECT_DIR)",
|
||||
"$(PROJECT_DIR)/MoproKit/Libs",
|
||||
);
|
||||
MARKETING_VERSION = 2.9.11;
|
||||
MARKETING_VERSION = 2.9.15;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -736,8 +738,10 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CXX = "";
|
||||
DEVELOPMENT_TEAM = 5B29R5LYHQ;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
@@ -799,6 +803,7 @@
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
USE_HERMES = true;
|
||||
};
|
||||
@@ -837,8 +842,10 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
CXX = "";
|
||||
DEVELOPMENT_TEAM = 5B29R5LYHQ;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -892,6 +899,7 @@
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
USE_HERMES = true;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1620"
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
|
||||
|
||||
import Foundation
|
||||
import NFCPassportReader
|
||||
|
||||
#if !E2E_TESTING
|
||||
import Mixpanel
|
||||
import NFCPassportReader
|
||||
|
||||
public class SelfAnalytics: Analytics {
|
||||
private let enableDebugLogs: Bool
|
||||
@@ -67,3 +69,13 @@ public class SelfAnalytics: Analytics {
|
||||
Mixpanel.mainInstance().flush()
|
||||
}
|
||||
}
|
||||
#else
|
||||
// E2E Testing stub - SelfAnalytics is not used when NFCPassportReader is excluded
|
||||
public class SelfAnalytics {
|
||||
public init(token: String, enableDebugLogs: Bool = false, trackAutomaticEvents: Bool = false) {}
|
||||
public func trackEvent(_ name: String, properties: [String: Any]? = nil) {}
|
||||
public func trackDebugEvent(_ name: String, properties: [String: Any]? = nil) {}
|
||||
public func trackError(_ error: Error, context: String) {}
|
||||
public func flush() {}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -16,7 +16,7 @@ module.exports = {
|
||||
'node',
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags|react-native-blur-effect)/)',
|
||||
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags|react-native-blur-effect|@sumsub)/)',
|
||||
],
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
testMatch: [
|
||||
|
||||
@@ -1237,3 +1237,29 @@ jest.mock('react-native/Libraries/AppState/AppState', () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock @sumsub/react-native-mobilesdk-module
|
||||
jest.mock('@sumsub/react-native-mobilesdk-module', () => {
|
||||
const createBuilder = () => ({
|
||||
withHandlers: jest.fn().mockReturnThis(),
|
||||
withDebug: jest.fn().mockReturnThis(),
|
||||
withLocale: jest.fn().mockReturnThis(),
|
||||
withAnalyticsEnabled: jest.fn().mockReturnThis(),
|
||||
build: jest.fn().mockReturnValue({
|
||||
launch: jest.fn().mockResolvedValue({ success: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
const MockSNSMobileSDK = {
|
||||
init: jest
|
||||
.fn()
|
||||
.mockImplementation((accessToken, tokenExpirationHandler) =>
|
||||
createBuilder(),
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
default: MockSNSMobileSDK,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -72,6 +72,9 @@ const config = {
|
||||
new RegExp(
|
||||
'packages/mobile-sdk-alpha/node_modules/react-native-svg(/|$)',
|
||||
),
|
||||
new RegExp(
|
||||
'packages/mobile-sdk-alpha/node_modules/react-native-webview(/|$)',
|
||||
),
|
||||
new RegExp('packages/mobile-sdk-demo/node_modules/react(/|$)'),
|
||||
new RegExp('packages/mobile-sdk-demo/node_modules/react-dom(/|$)'),
|
||||
new RegExp('packages/mobile-sdk-demo/node_modules/react-native(/|$)'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@selfxyz/mobile-app",
|
||||
"version": "2.9.11",
|
||||
"version": "2.9.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -63,8 +63,8 @@
|
||||
"test:ci": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
|
||||
"test:coverage": "yarn jest:run --coverage --passWithNoTests",
|
||||
"test:coverage:ci": "yarn jest:run --coverage --passWithNoTests --ci --coverageReporters=lcov --coverageReporters=text --coverageReporters=json",
|
||||
"test:e2e:android": "./scripts/mobile-ci-build-android.sh && maestro test tests/e2e/launch.android.flow.yaml",
|
||||
"test:e2e:ios": "xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build && maestro test tests/e2e/launch.ios.flow.yaml",
|
||||
"test:e2e:android": "./scripts/test-e2e-local.sh android --ci-match",
|
||||
"test:e2e:ios": "./scripts/test-e2e-local.sh ios --ci-match",
|
||||
"test:fastlane": "bundle exec ruby -Itest fastlane/test/helpers_test.rb",
|
||||
"test:tree-shaking": "node ./scripts/test-tree-shaking.cjs",
|
||||
"test:web-build": "yarn jest:run tests/web-build-render.test.ts --testTimeout=180000",
|
||||
@@ -85,13 +85,13 @@
|
||||
"react-native-webview": "13.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.3",
|
||||
"@ethersproject/shims": "^5.7.0",
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"@ethersproject/shims": "^5.8.0",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
"@openpassport/zk-kit-imt": "^0.0.5",
|
||||
"@openpassport/zk-kit-lean-imt": "^0.0.6",
|
||||
"@openpassport/zk-kit-smt": "^0.0.1",
|
||||
"@peculiar/x509": "^1.13.0",
|
||||
"@peculiar/x509": "^1.14.3",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
@@ -108,8 +108,9 @@
|
||||
"@selfxyz/euclid": "^0.6.1",
|
||||
"@selfxyz/mobile-sdk-alpha": "workspace:^",
|
||||
"@sentry/react": "^9.32.0",
|
||||
"@sentry/react-native": "7.0.1",
|
||||
"@sentry/react-native": "7.0.0",
|
||||
"@stablelib/cbor": "^2.0.1",
|
||||
"@sumsub/react-native-mobilesdk-module": "1.40.2",
|
||||
"@tamagui/animations-css": "1.126.14",
|
||||
"@tamagui/animations-react-native": "1.126.14",
|
||||
"@tamagui/config": "1.126.14",
|
||||
@@ -121,7 +122,7 @@
|
||||
"@turnkey/react-native-wallet-kit": "1.1.5",
|
||||
"@walletconnect/react-native-compat": "^2.23.0",
|
||||
"@xstate/react": "^5.0.3",
|
||||
"asn1js": "^3.0.6",
|
||||
"asn1js": "^3.0.7",
|
||||
"axios": "^1.13.2",
|
||||
"buffer": "^6.0.3",
|
||||
"country-emoji": "^1.5.6",
|
||||
@@ -135,8 +136,8 @@
|
||||
"js-sha512": "^0.9.0",
|
||||
"lottie-react": "^2.4.1",
|
||||
"lottie-react-native": "7.2.2",
|
||||
"node-forge": "^1.3.1",
|
||||
"pkijs": "^3.2.5",
|
||||
"node-forge": "^1.3.3",
|
||||
"pkijs": "^3.3.3",
|
||||
"poseidon-lite": "^0.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -146,7 +147,7 @@
|
||||
"react-native-blur-effect": "^1.1.3",
|
||||
"react-native-check-version": "^1.3.0",
|
||||
"react-native-cloud-storage": "^2.2.2",
|
||||
"react-native-device-info": "^14.0.4",
|
||||
"react-native-device-info": "^15.0.1",
|
||||
"react-native-dotenv": "^3.4.11",
|
||||
"react-native-edge-to-edge": "^1.7.0",
|
||||
"react-native-gesture-handler": "2.19.0",
|
||||
@@ -155,35 +156,35 @@
|
||||
"react-native-inappbrowser-reborn": "^3.7.0",
|
||||
"react-native-keychain": "^10.0.0",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-localize": "^3.5.2",
|
||||
"react-native-logs": "^5.3.0",
|
||||
"react-native-nfc-manager": "3.16.3",
|
||||
"react-native-passkey": "^3.3.1",
|
||||
"react-native-localize": "^3.6.1",
|
||||
"react-native-logs": "^5.5.0",
|
||||
"react-native-nfc-manager": "3.17.2",
|
||||
"react-native-passkey": "^3.3.2",
|
||||
"react-native-passport-reader": "1.0.3",
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
"react-native-safe-area-context": "^5.6.2",
|
||||
"react-native-screens": "4.15.3",
|
||||
"react-native-sqlite-storage": "^6.0.1",
|
||||
"react-native-svg": "15.14.0",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-svg-web": "1.0.9",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "^0.19.0",
|
||||
"react-native-web": "^0.21.2",
|
||||
"react-native-webview": "^13.16.0",
|
||||
"react-qr-barcode-scanner": "^2.1.8",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"tamagui": "1.126.14",
|
||||
"uuid": "^11.1.0",
|
||||
"xstate": "^5.20.2",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/plugin-syntax-flow": "^7.27.1",
|
||||
"@babel/plugin-transform-classes": "^7.27.1",
|
||||
"@babel/core": "^7.28.6",
|
||||
"@babel/plugin-syntax-flow": "^7.28.6",
|
||||
"@babel/plugin-transform-classes": "^7.28.6",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.27.1",
|
||||
"@babel/plugin-transform-private-methods": "^7.27.1",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/plugin-transform-private-methods": "^7.28.6",
|
||||
"@babel/preset-env": "^7.28.6",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@react-native-community/cli": "^16.0.3",
|
||||
"@react-native/babel-preset": "0.76.9",
|
||||
"@react-native/eslint-config": "0.76.9",
|
||||
@@ -209,11 +210,11 @@
|
||||
"@types/react-test-renderer": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"constants-browserify": "^1.0.0",
|
||||
"dompurify": "^3.2.6",
|
||||
"dompurify": "^3.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
@@ -228,14 +229,15 @@
|
||||
"jest": "^30.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"react-native-svg-transformer": "^1.5.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-native-svg-transformer": "^1.5.2",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"ts-morph": "^22.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
const dependencies = {
|
||||
'@selfxyz/mobile-sdk-alpha': { platforms: { android: null, ios: null } },
|
||||
};
|
||||
|
||||
// Disable Sumsub SDK autolinking during E2E testing to avoid build issues
|
||||
if (process.env.E2E_TESTING === '1') {
|
||||
dependencies['@sumsub/react-native-mobilesdk-module'] = {
|
||||
platforms: { android: null, ios: null },
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
project: { ios: {}, android: {} },
|
||||
dependencies: {
|
||||
'@selfxyz/mobile-sdk-alpha': { platforms: { android: null, ios: null } },
|
||||
},
|
||||
dependencies,
|
||||
assets: ['../src/assets/fonts'],
|
||||
};
|
||||
|
||||
@@ -18,20 +18,22 @@ NC='\033[0m' # No Color
|
||||
|
||||
print_usage() {
|
||||
echo "🎭 Local E2E Testing"
|
||||
echo "Usage: $0 [ios|android] [--workflow-match]"
|
||||
echo "Usage: $0 [ios|android] [--ci-match|--workflow-match]"
|
||||
echo ""
|
||||
echo "Modes:"
|
||||
echo " (default) - Debug builds, requires Metro server running"
|
||||
echo " --ci-match - Debug builds with bundled JS (matches GitHub CI exactly)"
|
||||
echo " --workflow-match - Release builds (legacy, for quick local testing)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 ios - Run iOS e2e tests locally"
|
||||
echo " $0 android - Run Android e2e tests locally"
|
||||
echo " $0 android --workflow-match - Run Android tests matching GitHub Actions workflow"
|
||||
echo " $0 ios - Run iOS e2e tests locally (requires Metro)"
|
||||
echo " $0 ios --ci-match - Run iOS tests matching GitHub CI exactly"
|
||||
echo " $0 android - Run Android e2e tests locally (requires Metro)"
|
||||
echo " $0 android --ci-match - Run Android tests matching GitHub CI exactly"
|
||||
echo ""
|
||||
echo "Prerequisites:"
|
||||
echo " iOS: Xcode, iOS Simulator, CocoaPods"
|
||||
echo " Android: Android SDK, running emulator"
|
||||
echo ""
|
||||
echo "Workflow Match Mode:"
|
||||
echo " --workflow-match - Use Release builds and exact workflow steps"
|
||||
echo " (No Metro dependency, matches CI environment)"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
@@ -249,18 +251,39 @@ build_ios_app() {
|
||||
# Set environment variable for e2e testing to enable OpenSSL fixes
|
||||
export E2E_TESTING=1
|
||||
|
||||
# Set build configuration based on workflow match
|
||||
if [ "$WORKFLOW_MATCH" = "true" ]; then
|
||||
if [ "$CI_MATCH" = "true" ]; then
|
||||
log_info "Using Debug configuration with bundled JS (matches CI)"
|
||||
BUILD_CONFIG="Debug"
|
||||
# Match CI xcodebuild flags exactly with FORCE_BUNDLING and RCT_NO_LAUNCH_PACKAGER
|
||||
if ! FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \
|
||||
xcodebuild -workspace ios/OpenPassport.xcworkspace \
|
||||
-scheme OpenPassport \
|
||||
-configuration Debug \
|
||||
-sdk iphonesimulator \
|
||||
-derivedDataPath ios/build \
|
||||
-jobs "$(sysctl -n hw.ncpu)" \
|
||||
-parallelizeTargets \
|
||||
COMPILER_INDEX_STORE_ENABLE=NO \
|
||||
ONLY_ACTIVE_ARCH=YES \
|
||||
SWIFT_COMPILATION_MODE=wholemodule \
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS="E2E_TESTING"; then
|
||||
log_error "iOS build failed"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$WORKFLOW_MATCH" = "true" ]; then
|
||||
log_info "Using Release configuration for workflow match"
|
||||
BUILD_CONFIG="Release"
|
||||
if ! xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration "$BUILD_CONFIG" -sdk iphonesimulator -derivedDataPath ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets SWIFT_ACTIVE_COMPILATION_CONDITIONS="E2E_TESTING"; then
|
||||
log_error "iOS build failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_info "Using Debug configuration for local development"
|
||||
BUILD_CONFIG="Debug"
|
||||
fi
|
||||
|
||||
if ! xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration "$BUILD_CONFIG" -sdk iphonesimulator -derivedDataPath ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets SWIFT_ACTIVE_COMPILATION_CONDITIONS="E2E_TESTING"; then
|
||||
log_error "iOS build failed"
|
||||
exit 1
|
||||
if ! xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration "$BUILD_CONFIG" -sdk iphonesimulator -derivedDataPath ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets SWIFT_ACTIVE_COMPILATION_CONDITIONS="E2E_TESTING"; then
|
||||
log_error "iOS build failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
log_success "iOS build succeeded"
|
||||
}
|
||||
@@ -436,17 +459,32 @@ setup_android_environment() {
|
||||
|
||||
build_android_app() {
|
||||
log_info "🔨 Building Android APK..."
|
||||
# Note: Using Release builds to avoid Metro dependency in CI
|
||||
# Debug builds require Metro server, Release builds have JS bundled
|
||||
# Run the build inside the android directory so gradlew is available
|
||||
echo "Current working directory: $(pwd)"
|
||||
echo "Checking if gradlew exists:"
|
||||
ls -la android/gradlew || echo "gradlew not found in android/"
|
||||
|
||||
cd android
|
||||
if ! ./gradlew assembleRelease --quiet; then
|
||||
log_error "Android build failed"
|
||||
exit 1
|
||||
if [ "$CI_MATCH" = "true" ]; then
|
||||
log_info "Using Debug build with bundled JS (matches CI)"
|
||||
# Force JS bundling in debug build to match CI behavior
|
||||
if ! ./gradlew assembleDebug -PbundleInDebug=true --quiet; then
|
||||
log_error "Android build failed"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$WORKFLOW_MATCH" = "true" ]; then
|
||||
log_info "Using Release build for workflow match"
|
||||
if ! ./gradlew assembleRelease --quiet; then
|
||||
log_error "Android build failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Default local dev uses Debug (requires Metro, like iOS default)
|
||||
log_info "Using Debug build for local development"
|
||||
if ! ./gradlew assembleDebug --quiet; then
|
||||
log_error "Android build failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
log_success "Android build succeeded"
|
||||
cd ..
|
||||
@@ -454,8 +492,14 @@ build_android_app() {
|
||||
|
||||
install_android_app() {
|
||||
log_info "📦 Installing app on emulator..."
|
||||
# Check if APK was built successfully (matching workflow)
|
||||
APK_PATH="android/app/build/outputs/apk/release/app-release.apk"
|
||||
# Check if APK was built successfully
|
||||
if [ "$WORKFLOW_MATCH" = "true" ]; then
|
||||
# WORKFLOW_MATCH uses Release builds
|
||||
APK_PATH="android/app/build/outputs/apk/release/app-release.apk"
|
||||
else
|
||||
# CI_MATCH and default mode both use Debug builds
|
||||
APK_PATH="android/app/build/outputs/apk/debug/app-debug.apk"
|
||||
fi
|
||||
log_info "Looking for APK at: $APK_PATH"
|
||||
if [ ! -f "$APK_PATH" ]; then
|
||||
log_error "APK not found at expected location"
|
||||
@@ -575,7 +619,10 @@ run_ios_tests() {
|
||||
echo "🍎 Starting local iOS e2e testing..."
|
||||
|
||||
shutdown_all_simulators
|
||||
check_metro_running
|
||||
# Skip Metro check for CI_MATCH (bundled JS) and WORKFLOW_MATCH (Release)
|
||||
if [ "$WORKFLOW_MATCH" != "true" ] && [ "$CI_MATCH" != "true" ]; then
|
||||
check_metro_running
|
||||
fi
|
||||
setup_ios_environment
|
||||
setup_ios_simulator
|
||||
build_ios_app
|
||||
@@ -600,8 +647,8 @@ run_android_tests() {
|
||||
# Set up trap to cleanup emulator on script exit
|
||||
trap cleanup_android_emulator EXIT
|
||||
|
||||
# Only check Metro if not in workflow match mode
|
||||
if [ "$WORKFLOW_MATCH" != "true" ]; then
|
||||
# Skip Metro check for CI_MATCH (bundled JS) and WORKFLOW_MATCH (Release)
|
||||
if [ "$WORKFLOW_MATCH" != "true" ] && [ "$CI_MATCH" != "true" ]; then
|
||||
check_metro_running
|
||||
fi
|
||||
|
||||
@@ -628,13 +675,16 @@ main() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for workflow match mode
|
||||
# Check for workflow match mode and CI match mode
|
||||
WORKFLOW_MATCH="false"
|
||||
CI_MATCH="false"
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--workflow-match" ]; then
|
||||
WORKFLOW_MATCH="true"
|
||||
log_info "🔧 Running in workflow match mode (Release builds, no Metro)"
|
||||
break
|
||||
elif [ "$arg" = "--ci-match" ]; then
|
||||
CI_MATCH="true"
|
||||
log_info "🔧 Running in CI match mode (Debug builds with bundled JS, matches GitHub CI)"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
1
app/src/assets/icons/epassport_logo.svg
Normal file
1
app/src/assets/icons/epassport_logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="117" height="72" viewBox="0 0 210 297"><path d="M-111.5625 24.75V136.125H23.539306A82.5 82.5 0 0 1 105 66 82.5 82.5 0 0 1 186.5 136.125H321.5625V24.75ZM105 90.75A57.75 57.75 0 0 0 47.25 148.5 57.75 57.75 0 0 0 105 206.25 57.75 57.75 0 0 0 162.75 148.5 57.75 57.75 0 0 0 105 90.75Zm-216.5625 70.125V272.25h433.125V160.875H186.46068A82.5 82.5 0 0 1 105 231 82.5 82.5 0 0 1 23.5 160.875Z" stroke-width="81.90428162"/></svg>
|
||||
|
After Width: | Height: | Size: 467 B |
3
app/src/assets/icons/shield_error.svg
Normal file
3
app/src/assets/icons/shield_error.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="120" viewBox="0 0 100.078 119.796" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M50.0391 119.796C49.4831 119.796 48.8278 119.677 48.0732 119.438C47.3187 119.24 46.5641 118.942 45.8096 118.545C37.3506 114.018 30.1823 109.927 24.3047 106.273C18.4668 102.66 13.7607 99.1051 10.1865 95.6104C6.65202 92.1156 4.07064 88.3626 2.44238 84.3516C0.814128 80.3008 0 75.5947 0 70.2334V26.3896C0 22.6169 0.714844 19.8171 2.14453 17.9902C3.61393 16.1237 5.95703 14.5352 9.17383 13.2246C10.4049 12.748 12.1523 12.0928 14.416 11.2588C16.7194 10.3851 19.2611 9.45182 22.041 8.45898C24.821 7.42643 27.5811 6.43359 30.3213 5.48047C33.1012 4.52734 35.6032 3.6735 37.8271 2.91895C40.0908 2.12467 41.8382 1.52897 43.0693 1.13184C44.1813 0.814128 45.333 0.55599 46.5244 0.357422C47.7555 0.119141 48.9271 0 50.0391 0C51.151 0 52.3226 0.119141 53.5537 0.357422C54.8245 0.55599 55.9961 0.814128 57.0684 1.13184C58.2995 1.52897 60.027 2.12467 62.251 2.91895C64.5146 3.6735 67.0166 4.5472 69.7568 5.54004C72.5368 6.49316 75.2969 7.46615 78.0371 8.45898C80.8171 9.45182 83.3389 10.3652 85.6025 11.1992C87.9059 12.0332 89.6732 12.7083 90.9043 13.2246C94.1608 14.5749 96.5039 16.1634 97.9336 17.9902C99.3633 19.8171 100.078 22.6169 100.078 26.3896V70.2334C100.078 75.5947 99.2839 80.3206 97.6953 84.4111C96.1465 88.5016 93.6048 92.334 90.0703 95.9082C86.5358 99.4427 81.8298 102.997 75.9521 106.571C70.1143 110.185 62.9062 114.176 54.3281 118.545C53.5339 118.942 52.7594 119.24 52.0049 119.438C51.2503 119.677 50.5951 119.796 50.0391 119.796ZM30.8574 82.3857C32.6842 82.3857 34.2132 81.79 35.4443 80.5986L50.1582 65.8252L64.9316 80.5986C66.0833 81.79 67.5527 82.3857 69.3398 82.3857C71.0872 82.3857 72.5566 81.79 73.748 80.5986C74.9395 79.4072 75.5352 77.9378 75.5352 76.1904C75.5352 74.4827 74.9196 73.0531 73.6885 71.9014L58.8555 57.0684L73.748 42.2354C74.9395 41.0042 75.5352 39.5745 75.5352 37.9463C75.5352 36.1989 74.9395 34.7493 73.748 33.5977C72.5964 32.4062 71.1667 31.8105 69.459 31.8105C67.7116 31.8105 66.2422 32.4062 65.0508 33.5977L50.1582 48.4307L35.3252 33.6572C34.0941 32.4658 32.6048 31.8701 30.8574 31.8701C29.1497 31.8701 27.7002 32.4658 26.5088 33.6572C25.3571 34.8089 24.7812 36.2585 24.7812 38.0059C24.7812 39.6341 25.377 41.0439 26.5684 42.2354L41.4609 57.0684L26.5684 71.9609C25.377 73.1523 24.7812 74.5622 24.7812 76.1904C24.7812 77.9378 25.3571 79.4072 26.5088 80.5986C27.7002 81.79 29.1497 82.3857 30.8574 82.3857Z" fill="#FACC15"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -30,7 +30,7 @@ const ModalBackDrop = styled(View, {
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export interface FeedbackModalScreenParams {
|
||||
export interface AlertModalParams {
|
||||
titleText: string;
|
||||
bodyText: string;
|
||||
buttonText: string;
|
||||
@@ -41,13 +41,13 @@ export interface FeedbackModalScreenParams {
|
||||
preventDismiss?: boolean;
|
||||
}
|
||||
|
||||
interface FeedbackModalScreenProps {
|
||||
interface AlertModalProps {
|
||||
visible: boolean;
|
||||
modalParams: FeedbackModalScreenParams | null;
|
||||
modalParams: AlertModalParams | null;
|
||||
onHideModal?: () => void;
|
||||
}
|
||||
|
||||
const FeedbackModalScreen: React.FC<FeedbackModalScreenProps> = ({
|
||||
const AlertModal: React.FC<AlertModalProps> = ({
|
||||
visible,
|
||||
modalParams,
|
||||
onHideModal,
|
||||
@@ -145,4 +145,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default FeedbackModalScreen;
|
||||
export default AlertModal;
|
||||
@@ -2,24 +2,25 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Alert, Modal, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||
import React from 'react';
|
||||
import { Modal, StyleSheet, Text, View } from 'react-native';
|
||||
import { Button, XStack, YStack } from 'tamagui';
|
||||
|
||||
import { Caption } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import {
|
||||
black,
|
||||
slate400,
|
||||
white,
|
||||
zinc800,
|
||||
zinc900,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { advercase, dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import ModalClose from '@/assets/icons/modal_close.svg';
|
||||
import { openSupportForm } from '@/services/support';
|
||||
|
||||
interface FeedbackModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (
|
||||
onSubmit?: (
|
||||
feedback: string,
|
||||
category: string,
|
||||
name?: string,
|
||||
@@ -27,65 +28,10 @@ interface FeedbackModalProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [category, setCategory] = useState('general');
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const categories = [
|
||||
{ value: 'general', label: 'General Feedback' },
|
||||
{ value: 'bug', label: 'Bug Report' },
|
||||
{ value: 'feature', label: 'Feature Request' },
|
||||
{ value: 'ui', label: 'UI/UX Issue' },
|
||||
];
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!feedback.trim()) {
|
||||
Alert.alert('Error', 'Please enter your feedback');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onSubmit(
|
||||
feedback.trim(),
|
||||
category,
|
||||
name.trim() || undefined,
|
||||
email.trim() || undefined,
|
||||
);
|
||||
setFeedback('');
|
||||
setCategory('general');
|
||||
setName('');
|
||||
setEmail('');
|
||||
onClose();
|
||||
Alert.alert('Success', 'Thank you for your feedback!');
|
||||
} catch (error) {
|
||||
console.error('Error submitting feedback:', error);
|
||||
Alert.alert('Error', 'Failed to submit feedback. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (feedback.trim() || name.trim() || email.trim()) {
|
||||
Alert.alert(
|
||||
'Discard Feedback?',
|
||||
'You have unsaved feedback. Are you sure you want to close?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: 'Discard', style: 'destructive', onPress: onClose },
|
||||
],
|
||||
);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
const FeedbackModal: React.FC<FeedbackModalProps> = ({ visible, onClose }) => {
|
||||
const handleSupportForm = async () => {
|
||||
await openSupportForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -93,93 +39,33 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={handleClose}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.modalContainer}>
|
||||
<YStack gap="$4" padding="$4">
|
||||
<XStack justifyContent="space-between" alignItems="center">
|
||||
<Text style={styles.title}>Send Feedback</Text>
|
||||
<Button
|
||||
size="$2"
|
||||
variant="outlined"
|
||||
onPress={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
<ModalClose onPress={onClose} />
|
||||
</XStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Caption style={styles.label}>Category</Caption>
|
||||
<XStack gap="$2" flexWrap="wrap">
|
||||
{categories.map(cat => (
|
||||
<Button
|
||||
key={cat.value}
|
||||
size="$2"
|
||||
backgroundColor={
|
||||
category === cat.value ? white : 'transparent'
|
||||
}
|
||||
color={category === cat.value ? black : white}
|
||||
borderColor={white}
|
||||
onPress={() => setCategory(cat.value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{cat.label}
|
||||
</Button>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Caption style={styles.label}>
|
||||
Contact Information (Optional)
|
||||
<YStack gap="$3" alignItems="center" paddingVertical="$2">
|
||||
<Caption style={styles.messageText}>
|
||||
Have feedback, suggestions, or found a bug?
|
||||
</Caption>
|
||||
<Caption style={styles.messageText}>
|
||||
Fill out our feedback form and we'll review it as soon as
|
||||
possible.
|
||||
</Caption>
|
||||
<XStack gap="$2">
|
||||
<TextInput
|
||||
style={[styles.textInput, { flex: 1, minHeight: 48 }]}
|
||||
placeholder="Name"
|
||||
placeholderTextColor={slate400}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
editable={!isSubmitting}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.textInput, { flex: 1, minHeight: 48 }]}
|
||||
placeholder="Email"
|
||||
placeholderTextColor={slate400}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
editable={!isSubmitting}
|
||||
/>
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Caption style={styles.label}>Your Feedback</Caption>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder="Tell us what you think, report a bug, or suggest a feature..."
|
||||
placeholderTextColor={slate400}
|
||||
value={feedback}
|
||||
onChangeText={setFeedback}
|
||||
multiline
|
||||
numberOfLines={6}
|
||||
textAlignVertical="top"
|
||||
editable={!isSubmitting}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
<Button
|
||||
size="$4"
|
||||
backgroundColor={white}
|
||||
color={black}
|
||||
onPress={handleSubmit}
|
||||
disabled={isSubmitting || !feedback.trim()}
|
||||
color="$black"
|
||||
onPress={handleSupportForm}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Feedback'}
|
||||
Open Feedback Form
|
||||
</Button>
|
||||
</YStack>
|
||||
</View>
|
||||
@@ -201,7 +87,6 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 16,
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
maxHeight: '80%',
|
||||
borderWidth: 1,
|
||||
borderColor: zinc800,
|
||||
},
|
||||
@@ -211,22 +96,12 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '600',
|
||||
color: white,
|
||||
},
|
||||
label: {
|
||||
messageText: {
|
||||
fontFamily: dinot,
|
||||
color: white,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
textInput: {
|
||||
backgroundColor: black,
|
||||
borderWidth: 1,
|
||||
borderColor: zinc800,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
color: white,
|
||||
fontSize: 16,
|
||||
fontFamily: dinot,
|
||||
minHeight: 120,
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
19
app/src/components/SystemBars.tsx
Normal file
19
app/src/components/SystemBars.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { SystemBars as EdgeToEdgeSystemBars } from 'react-native-edge-to-edge';
|
||||
|
||||
export type { SystemBarStyle } from 'react-native-edge-to-edge';
|
||||
|
||||
type SystemBarsProps = React.ComponentProps<typeof EdgeToEdgeSystemBars>;
|
||||
|
||||
export const SystemBars: React.FC<SystemBarsProps> = props => {
|
||||
if (Platform.OS === 'android' || Platform.OS === 'web') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <EdgeToEdgeSystemBars {...props} />;
|
||||
};
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import type { TextProps } from 'react-native';
|
||||
import type { SystemBarStyle } from 'react-native-edge-to-edge';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import { ChevronLeft, X } from '@tamagui/lucide-icons';
|
||||
|
||||
import type { ViewProps } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
@@ -16,6 +14,9 @@ import {
|
||||
XStack,
|
||||
} from '@selfxyz/mobile-sdk-alpha/components';
|
||||
|
||||
import type { SystemBarStyle } from '@/components/SystemBars';
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
|
||||
interface NavBarProps extends ViewProps {
|
||||
children: React.ReactNode;
|
||||
backgroundColor?: string;
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
|
||||
export const HeadlessNavForEuclid = (props: NativeStackHeaderProps) => {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -33,6 +33,8 @@ export const referralBaseUrl = 'https://referral.self.xyz';
|
||||
export const selfLogoReverseUrl =
|
||||
'https://storage.googleapis.com/self-logo-reverse/Self%20Logomark%20Reverse.png';
|
||||
export const selfUrl = 'https://self.xyz';
|
||||
export const supportFormUrl =
|
||||
'https://hail-jonquil-ef8.notion.site/2b057801cd128041985dfd6e1722eca1';
|
||||
export const supportedBiometricIdsUrl =
|
||||
'https://docs.self.xyz/use-self/self-map-countries-list';
|
||||
export const telegramUrl = 'https://t.me/selfxyz';
|
||||
|
||||
@@ -92,13 +92,22 @@ export const useEarnPointsFlow = ({
|
||||
}, [hasReferrer, navigation, navigateToPointsProof]);
|
||||
|
||||
const showPointsInfoScreen = useCallback(() => {
|
||||
navigation.navigate('PointsInfo', {
|
||||
showNextButton: true,
|
||||
onNextButtonPress: () => {
|
||||
const callbackId = registerModalCallbacks({
|
||||
onButtonPress: () => {
|
||||
showPointsDisclosureModal();
|
||||
},
|
||||
onModalDismiss: () => {
|
||||
if (hasReferrer) {
|
||||
useUserStore.getState().clearDeepLinkReferrer();
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [navigation, showPointsDisclosureModal]);
|
||||
|
||||
navigation.navigate('PointsInfo', {
|
||||
showNextButton: true,
|
||||
callbackId,
|
||||
});
|
||||
}, [hasReferrer, navigation, showPointsDisclosureModal]);
|
||||
|
||||
const handleReferralFlow = useCallback(async () => {
|
||||
if (!referrer) {
|
||||
|
||||
27
app/src/hooks/useErrorInjection.ts
Normal file
27
app/src/hooks/useErrorInjection.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { InjectedErrorType } from '@/stores/errorInjectionStore';
|
||||
import { useErrorInjectionStore } from '@/stores/errorInjectionStore';
|
||||
import { IS_DEV_MODE } from '@/utils/devUtils';
|
||||
|
||||
/**
|
||||
* Hook for checking if a specific error should be injected
|
||||
* Only active in dev mode
|
||||
*/
|
||||
export function useErrorInjection() {
|
||||
const injectedErrors = useErrorInjectionStore(state => state.injectedErrors);
|
||||
|
||||
const shouldInjectError = useCallback(
|
||||
(errorType: InjectedErrorType): boolean => {
|
||||
if (!IS_DEV_MODE) return false;
|
||||
return injectedErrors.includes(errorType);
|
||||
},
|
||||
[injectedErrors],
|
||||
);
|
||||
|
||||
return { shouldInjectError };
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
showFeedbackWidget,
|
||||
} from '@sentry/react-native';
|
||||
|
||||
import type { FeedbackModalScreenParams } from '@/components/FeedbackModalScreen';
|
||||
import type { AlertModalParams } from '@/components/AlertModal';
|
||||
import { captureFeedback } from '@/config/sentry';
|
||||
|
||||
export type FeedbackType = 'button' | 'widget' | 'custom';
|
||||
@@ -18,8 +18,7 @@ export const useFeedbackModal = () => {
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [modalParams, setModalParams] =
|
||||
useState<FeedbackModalScreenParams | null>(null);
|
||||
const [modalParams, setModalParams] = useState<AlertModalParams | null>(null);
|
||||
|
||||
const showFeedbackModal = useCallback((type: FeedbackType = 'button') => {
|
||||
if (timeoutRef.current) {
|
||||
@@ -81,7 +80,7 @@ export const useFeedbackModal = () => {
|
||||
setIsVisible(false);
|
||||
}, []);
|
||||
|
||||
const showModal = useCallback((params: FeedbackModalScreenParams) => {
|
||||
const showModal = useCallback((params: AlertModalParams) => {
|
||||
setModalParams(params);
|
||||
setIsModalVisible(true);
|
||||
}, []);
|
||||
|
||||
127
app/src/hooks/useSumsubLauncher.ts
Normal file
127
app/src/hooks/useSumsubLauncher.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { sanitizeErrorMessage } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import { fetchAccessToken, launchSumsub } from '@/integrations/sumsub';
|
||||
import type { SumsubResult } from '@/integrations/sumsub/types';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
|
||||
export type FallbackErrorSource = 'mrz_scan_failed' | 'nfc_scan_failed';
|
||||
|
||||
export interface UseSumsubLauncherOptions {
|
||||
/**
|
||||
* Country code for the user's document
|
||||
*/
|
||||
countryCode: string;
|
||||
/**
|
||||
* Error source to track where the Sumsub launch was initiated from
|
||||
*/
|
||||
errorSource: FallbackErrorSource;
|
||||
/**
|
||||
* Optional callback to handle successful verification
|
||||
*/
|
||||
onSuccess?: (result: SumsubResult) => void | Promise<void>;
|
||||
/**
|
||||
* Optional callback to handle user cancellation
|
||||
*/
|
||||
onCancel?: () => void | Promise<void>;
|
||||
/**
|
||||
* Optional callback to handle verification failure
|
||||
*/
|
||||
onError?: (error: unknown, result?: SumsubResult) => void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for launching Sumsub verification with consistent error handling.
|
||||
*
|
||||
* Abstracts the common pattern of:
|
||||
* 1. Fetching access token
|
||||
* 2. Launching Sumsub SDK
|
||||
* 3. Handling errors by navigating to fallback screen
|
||||
* 4. Managing loading state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { launchSumsubVerification, isLoading } = useSumsubLauncher({
|
||||
* countryCode: 'US',
|
||||
* errorSource: 'nfc_scan_failed',
|
||||
* });
|
||||
*
|
||||
* <Button onPress={launchSumsubVerification} disabled={isLoading}>
|
||||
* {isLoading ? 'Loading...' : 'Try Alternative Verification'}
|
||||
* </Button>
|
||||
* ```
|
||||
*/
|
||||
export const useSumsubLauncher = (options: UseSumsubLauncherOptions) => {
|
||||
const { countryCode, errorSource, onSuccess, onCancel, onError } = options;
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const launchSumsubVerification = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const accessToken = await fetchAccessToken();
|
||||
const result = await launchSumsub({ accessToken: accessToken.token });
|
||||
|
||||
// Handle user cancellation
|
||||
if (!result.success && result.status === 'Interrupted') {
|
||||
await onCancel?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle verification failure
|
||||
if (!result.success) {
|
||||
const error = result.errorMsg || result.errorType || 'Unknown error';
|
||||
const safeError = sanitizeErrorMessage(error);
|
||||
console.error('Sumsub verification failed:', safeError);
|
||||
|
||||
// Call custom error handler if provided, otherwise navigate to fallback screen
|
||||
if (onError) {
|
||||
await onError(safeError, result);
|
||||
} else {
|
||||
// Navigate to the appropriate fallback screen based on error source
|
||||
if (errorSource === 'mrz_scan_failed') {
|
||||
navigation.navigate('RegistrationFallbackMRZ', { countryCode });
|
||||
} else {
|
||||
navigation.navigate('RegistrationFallbackNFC', { countryCode });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle success
|
||||
await onSuccess?.(result);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
const safeError = sanitizeErrorMessage(errorMessage);
|
||||
console.error('Error launching alternative verification:', safeError);
|
||||
|
||||
// Call custom error handler if provided, otherwise navigate to fallback screen
|
||||
if (onError) {
|
||||
await onError(safeError);
|
||||
} else {
|
||||
// Navigate to the appropriate fallback screen based on error source
|
||||
if (errorSource === 'mrz_scan_failed') {
|
||||
navigation.navigate('RegistrationFallbackMRZ', { countryCode });
|
||||
} else {
|
||||
navigation.navigate('RegistrationFallbackNFC', { countryCode });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [navigation, countryCode, errorSource, onSuccess, onCancel, onError]);
|
||||
|
||||
return {
|
||||
launchSumsubVerification,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
@@ -7,10 +7,12 @@ import type {
|
||||
ACCESSIBLE,
|
||||
GetOptions,
|
||||
SECURITY_LEVEL,
|
||||
SetOptions,
|
||||
} from 'react-native-keychain';
|
||||
import Keychain from 'react-native-keychain';
|
||||
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import type { ExtendedSetOptions } from '@/types/react-native-keychain';
|
||||
|
||||
/**
|
||||
* Security configuration for keychain operations
|
||||
*/
|
||||
@@ -23,6 +25,8 @@ export interface AdaptiveSecurityConfig {
|
||||
export interface GetSecureOptions {
|
||||
requireAuth?: boolean;
|
||||
promptMessage?: string;
|
||||
/** Whether to use StrongBox-backed key generation on Android. Default: true */
|
||||
useStrongBox?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,7 +65,8 @@ export async function checkPasscodeAvailable(): Promise<boolean> {
|
||||
await Keychain.setGenericPassword('test', 'test', {
|
||||
service: testService,
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY,
|
||||
});
|
||||
useStrongBox: false,
|
||||
} as ExtendedSetOptions);
|
||||
// Clean up test entry
|
||||
await Keychain.resetGenericPassword({ service: testService });
|
||||
return true;
|
||||
@@ -78,7 +83,7 @@ export async function createKeychainOptions(
|
||||
options: GetSecureOptions,
|
||||
capabilities?: SecurityCapabilities,
|
||||
): Promise<{
|
||||
setOptions: SetOptions;
|
||||
setOptions: ExtendedSetOptions;
|
||||
getOptions: GetOptions;
|
||||
}> {
|
||||
const config = await getAdaptiveSecurityConfig(
|
||||
@@ -86,10 +91,14 @@ export async function createKeychainOptions(
|
||||
capabilities,
|
||||
);
|
||||
|
||||
const setOptions: SetOptions = {
|
||||
const useStrongBox =
|
||||
options.useStrongBox ?? useSettingStore.getState().useStrongBox;
|
||||
|
||||
const setOptions: ExtendedSetOptions = {
|
||||
accessible: config.accessible,
|
||||
...(config.securityLevel && { securityLevel: config.securityLevel }),
|
||||
...(config.accessControl && { accessControl: config.accessControl }),
|
||||
useStrongBox,
|
||||
};
|
||||
|
||||
const getOptions: GetOptions = {
|
||||
|
||||
14
app/src/integrations/sumsub/index.ts
Normal file
14
app/src/integrations/sumsub/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export type {
|
||||
AccessTokenResponse,
|
||||
SumsubApplicantInfo,
|
||||
SumsubResult,
|
||||
} from '@/integrations/sumsub/types';
|
||||
export {
|
||||
type SumsubConfig,
|
||||
fetchAccessToken,
|
||||
launchSumsub,
|
||||
} from '@/integrations/sumsub/sumsubService';
|
||||
154
app/src/integrations/sumsub/sumsubService.ts
Normal file
154
app/src/integrations/sumsub/sumsubService.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { SUMSUB_TEE_URL } from '@env';
|
||||
import SNSMobileSDK from '@sumsub/react-native-mobilesdk-module';
|
||||
|
||||
import { alpha2ToAlpha3 } from '@selfxyz/common';
|
||||
|
||||
import type {
|
||||
AccessTokenResponse,
|
||||
SumsubResult,
|
||||
} from '@/integrations/sumsub/types';
|
||||
|
||||
// Maps Self document type codes to Sumsub document types
|
||||
type SelfDocumentType = 'p' | 'i';
|
||||
type SumsubDocumentType = 'PASSPORT' | 'ID_CARD';
|
||||
|
||||
const DOCUMENT_TYPE_MAP: Record<SelfDocumentType, SumsubDocumentType> = {
|
||||
p: 'PASSPORT',
|
||||
i: 'ID_CARD',
|
||||
};
|
||||
|
||||
export interface SumsubConfig {
|
||||
accessToken: string;
|
||||
locale?: string;
|
||||
debug?: boolean;
|
||||
/** Self document type code ('p' for passport, 'i' for ID card) */
|
||||
documentType?: SelfDocumentType;
|
||||
/** ISO 3166-1 alpha-2 country code (e.g., 'US', 'GB') */
|
||||
countryCode?: string;
|
||||
onStatusChanged?: (prevStatus: string, newStatus: string) => void;
|
||||
onEvent?: (eventType: string, payload: unknown) => void;
|
||||
}
|
||||
|
||||
const FETCH_TIMEOUT_MS = 30000; // 30 seconds
|
||||
|
||||
export const fetchAccessToken = async (
|
||||
phoneNumber?: string,
|
||||
): Promise<AccessTokenResponse> => {
|
||||
const apiUrl = SUMSUB_TEE_URL;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const requestBody: Record<string, string> = {};
|
||||
|
||||
if (phoneNumber) {
|
||||
requestBody.phone = phoneNumber;
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}/access-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to get Sumsub access token (HTTP ${response.status})`,
|
||||
);
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
// Handle both string and object responses
|
||||
if (typeof body === 'string') {
|
||||
return JSON.parse(body) as AccessTokenResponse;
|
||||
}
|
||||
|
||||
return body as AccessTokenResponse;
|
||||
} catch (err) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (err instanceof Error) {
|
||||
if (err.name === 'AbortError') {
|
||||
throw new Error(
|
||||
`Request to Sumsub TEE timed out after ${FETCH_TIMEOUT_MS / 1000}s`,
|
||||
);
|
||||
}
|
||||
throw new Error(`Failed to get Sumsub access token: ${err.message}`);
|
||||
}
|
||||
|
||||
throw new Error('Failed to get Sumsub access token: Unknown error');
|
||||
}
|
||||
};
|
||||
|
||||
export const launchSumsub = async (
|
||||
config: SumsubConfig,
|
||||
): Promise<SumsubResult> => {
|
||||
let sdk = SNSMobileSDK.init(config.accessToken, async () => {
|
||||
// Token refresh not implemented for test flow
|
||||
throw new Error(
|
||||
'Sumsub token expired - refresh not implemented for test flow',
|
||||
);
|
||||
})
|
||||
.withHandlers({
|
||||
onStatusChanged: event => {
|
||||
console.log(`Sumsub status: ${event.prevStatus} => ${event.newStatus}`);
|
||||
config.onStatusChanged?.(event.prevStatus, event.newStatus);
|
||||
},
|
||||
onLog: _event => {
|
||||
// Log event received but don't log message (may contain PII)
|
||||
console.log('[Sumsub] Log event received');
|
||||
},
|
||||
onEvent: event => {
|
||||
// Only log event type, not full payload (may contain PII)
|
||||
console.log(`Sumsub event: ${event.eventType}`);
|
||||
config.onEvent?.(event.eventType, event.payload);
|
||||
},
|
||||
})
|
||||
.withDebug(config.debug ?? __DEV__)
|
||||
.withLocale(config.locale ?? 'en')
|
||||
// Platform configuration:
|
||||
// - Device Intelligence (Fisherman): Enabled on both iOS and Android
|
||||
// * iOS: Configured via IDENSIC_WITH_FISHERMAN in Podfile
|
||||
// * Android: Configured via idensic-mobile-sdk-fisherman in patch file
|
||||
// * Privacy: iOS declares device ID collection in PrivacyInfo.xcprivacy
|
||||
// * Privacy: Android should declare device fingerprinting in Google Play Data Safety
|
||||
// - VideoIdent (live video calls): Disabled on both platforms for current release
|
||||
// * iOS: Disabled in Podfile (avoids microphone permission requirements)
|
||||
// * Android: Disabled in patch file (avoids FOREGROUND_SERVICE_MICROPHONE permission)
|
||||
// * Note: VideoIdent will be re-enabled on both platforms in future release for liveness checks
|
||||
.withAnalyticsEnabled(true); // Required for Device Intelligence to function
|
||||
|
||||
// Pre-select document type and country if provided
|
||||
// This skips the document selection step in Sumsub
|
||||
if (config.documentType && config.countryCode) {
|
||||
const sumsubDocType = DOCUMENT_TYPE_MAP[config.documentType];
|
||||
// Handle both 2-letter (US) and 3-letter (USA) country codes
|
||||
// alpha2ToAlpha3 returns undefined for 3-letter codes, so use the original if conversion fails
|
||||
const alpha3Country =
|
||||
alpha2ToAlpha3(config.countryCode) ?? config.countryCode;
|
||||
|
||||
if (sumsubDocType && alpha3Country) {
|
||||
console.log(
|
||||
`[Sumsub] Pre-selecting document: ${sumsubDocType} from ${alpha3Country}`,
|
||||
);
|
||||
sdk = sdk.withPreferredDocumentDefinitions({
|
||||
IDENTITY: {
|
||||
idDocType: sumsubDocType,
|
||||
country: alpha3Country,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sdk.build().launch();
|
||||
};
|
||||
40
app/src/integrations/sumsub/types.ts
Normal file
40
app/src/integrations/sumsub/types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export interface AccessTokenResponse {
|
||||
token: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface SumsubApplicantInfo {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
key: string;
|
||||
clientId: string;
|
||||
inspectionId: string;
|
||||
externalUserId: string;
|
||||
info?: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
dob?: string;
|
||||
country?: string;
|
||||
phone?: string;
|
||||
};
|
||||
email?: string;
|
||||
phone?: string;
|
||||
review: {
|
||||
reviewAnswer: string;
|
||||
reviewResult: {
|
||||
reviewAnswer: string;
|
||||
};
|
||||
};
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SumsubResult {
|
||||
success: boolean;
|
||||
status: string;
|
||||
errorType?: string;
|
||||
errorMsg?: string;
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import type {
|
||||
@@ -15,6 +14,8 @@ import type {
|
||||
import { ExpandableBottomLayout as BaseExpandableBottomLayout } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { black } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({
|
||||
children,
|
||||
backgroundColor,
|
||||
|
||||
@@ -35,20 +35,26 @@ export default function SimpleScrolledTitleLayout({
|
||||
footer,
|
||||
}: DetailListProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const dismissBottomPadding = Math.min(16, insets.bottom);
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={white}>
|
||||
<ExpandableBottomLayout.FullSection paddingTop={0} flex={1}>
|
||||
<YStack paddingTop={insets.top + 12}>
|
||||
<YStack paddingTop={insets.top + 24}>
|
||||
<Title>{title}</Title>
|
||||
{header}
|
||||
</YStack>
|
||||
<ScrollView flex={1} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView
|
||||
flex={1}
|
||||
showsVerticalScrollIndicator={true}
|
||||
indicatorStyle="black"
|
||||
scrollIndicatorInsets={{ right: 1 }}
|
||||
>
|
||||
<YStack paddingTop={0} paddingBottom={12} flex={1}>
|
||||
{children}
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
{footer && (
|
||||
<YStack marginTop={8} marginBottom={12}>
|
||||
<YStack marginTop={16} marginBottom={12}>
|
||||
{footer}
|
||||
</YStack>
|
||||
)}
|
||||
@@ -60,8 +66,8 @@ export default function SimpleScrolledTitleLayout({
|
||||
{secondaryButtonText}
|
||||
</SecondaryButton>
|
||||
)}
|
||||
{/* Anchor the Dismiss button to bottom with only safe area padding */}
|
||||
<YStack paddingBottom={insets.bottom + 8}>
|
||||
{/* Anchor the Dismiss button to bottom with sane spacing */}
|
||||
<YStack marginTop="auto" paddingBottom={dismissBottomPadding}>
|
||||
<PrimaryButton onPress={onDismiss}>Dismiss</PrimaryButton>
|
||||
</YStack>
|
||||
</ExpandableBottomLayout.FullSection>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
|
||||
import type { DocumentCategory } from '@selfxyz/common/utils/types';
|
||||
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
import DeferredLinkingInfoScreen from '@/screens/app/DeferredLinkingInfoScreen';
|
||||
import GratificationScreen from '@/screens/app/GratificationScreen';
|
||||
import LoadingScreen from '@/screens/app/LoadingScreen';
|
||||
|
||||
@@ -13,6 +13,7 @@ import DevHapticFeedbackScreen from '@/screens/dev/DevHapticFeedbackScreen';
|
||||
import DevLoadingScreen from '@/screens/dev/DevLoadingScreen';
|
||||
import DevPrivateKeyScreen from '@/screens/dev/DevPrivateKeyScreen';
|
||||
import DevSettingsScreen from '@/screens/dev/DevSettingsScreen';
|
||||
import SumsubTestScreen from '@/screens/dev/SumsubTestScreen';
|
||||
|
||||
const devHeaderOptions: NativeStackNavigationOptions = {
|
||||
headerStyle: {
|
||||
@@ -21,6 +22,7 @@ const devHeaderOptions: NativeStackNavigationOptions = {
|
||||
headerTitleStyle: {
|
||||
color: white,
|
||||
},
|
||||
headerTintColor: white,
|
||||
headerBackTitle: 'close',
|
||||
};
|
||||
|
||||
@@ -80,6 +82,13 @@ const devScreens = {
|
||||
title: 'Dev Loading Screen',
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
SumsubTest: {
|
||||
screen: SumsubTestScreen,
|
||||
options: {
|
||||
...devHeaderOptions,
|
||||
title: 'Sumsub Test',
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
};
|
||||
|
||||
export default devScreens;
|
||||
@@ -19,10 +19,15 @@ import DocumentCameraTroubleScreen from '@/screens/documents/scanning/DocumentCa
|
||||
import DocumentNFCMethodSelectionScreen from '@/screens/documents/scanning/DocumentNFCMethodSelectionScreen';
|
||||
import DocumentNFCScanScreen from '@/screens/documents/scanning/DocumentNFCScanScreen';
|
||||
import DocumentNFCTroubleScreen from '@/screens/documents/scanning/DocumentNFCTroubleScreen';
|
||||
import RegistrationFallbackMRZScreen from '@/screens/documents/scanning/RegistrationFallbackMRZScreen';
|
||||
import RegistrationFallbackNFCScreen from '@/screens/documents/scanning/RegistrationFallbackNFCScreen';
|
||||
import ConfirmBelongingScreen from '@/screens/documents/selection/ConfirmBelongingScreen';
|
||||
import CountryPickerScreen from '@/screens/documents/selection/CountryPickerScreen';
|
||||
import DocumentOnboardingScreen from '@/screens/documents/selection/DocumentOnboardingScreen';
|
||||
import IDPickerScreen from '@/screens/documents/selection/IDPickerScreen';
|
||||
import LogoConfirmationScreen from '@/screens/documents/selection/LogoConfirmationScreen';
|
||||
import KycConnectionErrorScreen from '@/screens/kyc/KycConnectionErrorScreen';
|
||||
import KycFailureScreen from '@/screens/kyc/KycFailureScreen';
|
||||
|
||||
const documentsScreens = {
|
||||
DocumentCamera: {
|
||||
@@ -93,6 +98,16 @@ const documentsScreens = {
|
||||
documentTypes: [],
|
||||
},
|
||||
},
|
||||
LogoConfirmation: {
|
||||
screen: LogoConfirmationScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
} as NativeStackNavigationOptions,
|
||||
initialParams: {
|
||||
documentType: '',
|
||||
countryCode: '',
|
||||
},
|
||||
},
|
||||
ConfirmBelonging: {
|
||||
screen: ConfirmBelongingScreen,
|
||||
options: {
|
||||
@@ -147,14 +162,52 @@ const documentsScreens = {
|
||||
AadhaarUploadError: {
|
||||
screen: AadhaarUploadErrorScreen,
|
||||
options: {
|
||||
title: 'AADHAAR REGISTRATION',
|
||||
header: AadhaarNavBar,
|
||||
headerBackVisible: false,
|
||||
headerShown: false,
|
||||
} as NativeStackNavigationOptions,
|
||||
initialParams: {
|
||||
errorType: 'general',
|
||||
},
|
||||
},
|
||||
RegistrationFallbackMRZ: {
|
||||
screen: RegistrationFallbackMRZScreen,
|
||||
options: {
|
||||
title: 'REGISTRATION',
|
||||
headerShown: false,
|
||||
} as NativeStackNavigationOptions,
|
||||
initialParams: {
|
||||
countryCode: '',
|
||||
},
|
||||
},
|
||||
RegistrationFallbackNFC: {
|
||||
screen: RegistrationFallbackNFCScreen,
|
||||
options: {
|
||||
title: 'REGISTRATION',
|
||||
headerShown: false,
|
||||
} as NativeStackNavigationOptions,
|
||||
initialParams: {
|
||||
countryCode: '',
|
||||
},
|
||||
},
|
||||
KycFailure: {
|
||||
screen: KycFailureScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
animation: 'fade',
|
||||
} as NativeStackNavigationOptions,
|
||||
initialParams: {
|
||||
countryCode: '',
|
||||
canRetry: true,
|
||||
},
|
||||
},
|
||||
KycConnectionError: {
|
||||
screen: KycConnectionErrorScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
} as NativeStackNavigationOptions,
|
||||
initialParams: {
|
||||
countryCode: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default documentsScreens;
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
|
||||
import type { DocumentCategory } from '@selfxyz/common/utils/types';
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import { DefaultNavBar } from '@/components/navbar';
|
||||
@@ -28,11 +27,9 @@ import homeScreens from '@/navigation/home';
|
||||
import onboardingScreens from '@/navigation/onboarding';
|
||||
import sharedScreens from '@/navigation/shared';
|
||||
import starfallScreens from '@/navigation/starfall';
|
||||
import type { ExplicitRouteParams, OmittedRouteKeys } from '@/navigation/types';
|
||||
import verificationScreens from '@/navigation/verification';
|
||||
import type { ModalNavigationParams } from '@/screens/app/ModalScreen';
|
||||
import type { WebViewScreenParams } from '@/screens/shared/WebViewScreen';
|
||||
import { trackScreenView } from '@/services/analytics';
|
||||
import type { ProofHistory } from '@/stores/proofTypes';
|
||||
|
||||
export const navigationScreens = {
|
||||
...appScreens,
|
||||
@@ -58,143 +55,13 @@ const AppNavigation = createNativeStackNavigator({
|
||||
|
||||
type BaseRootStackParamList = StaticParamList<typeof AppNavigation>;
|
||||
|
||||
// Explicitly declare route params that are not inferred from initialParams
|
||||
// Explicitly declare route params that are not inferred from initialParams.
|
||||
// Route param types are defined in @/navigation/types for better organization.
|
||||
export type RootStackParamList = Omit<
|
||||
BaseRootStackParamList,
|
||||
| 'AadhaarUpload'
|
||||
| 'AadhaarUploadError'
|
||||
| 'AadhaarUploadSuccess'
|
||||
| 'AccountRecovery'
|
||||
| 'AccountVerifiedSuccess'
|
||||
| 'CloudBackupSettings'
|
||||
| 'ComingSoon'
|
||||
| 'ConfirmBelonging'
|
||||
| 'CreateMock'
|
||||
| 'Disclaimer'
|
||||
| 'DocumentNFCScan'
|
||||
| 'DocumentOnboarding'
|
||||
| 'DocumentSelectorForProving'
|
||||
| 'ProvingScreenRouter'
|
||||
| 'Gratification'
|
||||
| 'Home'
|
||||
| 'IDPicker'
|
||||
| 'IdDetails'
|
||||
| 'Loading'
|
||||
| 'Modal'
|
||||
| 'MockDataDeepLink'
|
||||
| 'Points'
|
||||
| 'PointsInfo'
|
||||
| 'ProofHistoryDetail'
|
||||
| 'Prove'
|
||||
| 'SaveRecoveryPhrase'
|
||||
| 'WebView'
|
||||
> & {
|
||||
// Shared screens
|
||||
ComingSoon: {
|
||||
countryCode?: string;
|
||||
documentCategory?: string;
|
||||
};
|
||||
WebView: WebViewScreenParams;
|
||||
|
||||
// Document screens
|
||||
IDPicker: {
|
||||
countryCode: string;
|
||||
documentTypes: string[];
|
||||
};
|
||||
ConfirmBelonging:
|
||||
| {
|
||||
documentCategory?: DocumentCategory;
|
||||
signatureAlgorithm?: string;
|
||||
curveOrExponent?: string;
|
||||
}
|
||||
| undefined;
|
||||
DocumentNFCScan:
|
||||
| {
|
||||
passportNumber?: string;
|
||||
dateOfBirth?: string;
|
||||
dateOfExpiry?: string;
|
||||
}
|
||||
| undefined;
|
||||
DocumentCameraTrouble: undefined;
|
||||
DocumentOnboarding: undefined;
|
||||
|
||||
// Aadhaar screens
|
||||
AadhaarUpload: {
|
||||
countryCode: string;
|
||||
};
|
||||
AadhaarUploadSuccess: undefined;
|
||||
AadhaarUploadError: {
|
||||
errorType: string;
|
||||
};
|
||||
|
||||
// Account/Recovery screens
|
||||
AccountRecovery:
|
||||
| {
|
||||
nextScreen?: string;
|
||||
}
|
||||
| undefined;
|
||||
SaveRecoveryPhrase:
|
||||
| {
|
||||
nextScreen?: string;
|
||||
}
|
||||
| undefined;
|
||||
CloudBackupSettings:
|
||||
| {
|
||||
nextScreen?: 'SaveRecoveryPhrase';
|
||||
returnToScreen?: 'Points';
|
||||
}
|
||||
| undefined;
|
||||
ProofSettings: undefined;
|
||||
AccountVerifiedSuccess: undefined;
|
||||
|
||||
// Proof/Verification screens
|
||||
ProofHistoryDetail: {
|
||||
data: ProofHistory;
|
||||
};
|
||||
Prove:
|
||||
| {
|
||||
scrollOffset?: number;
|
||||
}
|
||||
| undefined;
|
||||
ProvingScreenRouter: undefined;
|
||||
DocumentSelectorForProving:
|
||||
| {
|
||||
documentType?: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
// App screens
|
||||
Loading: {
|
||||
documentCategory?: DocumentCategory;
|
||||
signatureAlgorithm?: string;
|
||||
curveOrExponent?: string;
|
||||
};
|
||||
Modal: ModalNavigationParams;
|
||||
Gratification: {
|
||||
points?: number;
|
||||
};
|
||||
StarfallPushCode: undefined;
|
||||
|
||||
// Home screens
|
||||
Home: {
|
||||
testReferralFlow?: boolean;
|
||||
};
|
||||
Points: undefined;
|
||||
PointsInfo:
|
||||
| {
|
||||
showNextButton?: boolean;
|
||||
onNextButtonPress?: () => void;
|
||||
}
|
||||
| undefined;
|
||||
IdDetails: undefined;
|
||||
|
||||
// Onboarding screens
|
||||
Disclaimer: undefined;
|
||||
|
||||
// Dev screens
|
||||
CreateMock: undefined;
|
||||
MockDataDeepLink: undefined;
|
||||
};
|
||||
OmittedRouteKeys
|
||||
> &
|
||||
ExplicitRouteParams;
|
||||
|
||||
export type RootStackScreenProps<T extends keyof RootStackParamList> =
|
||||
NativeStackScreenProps<RootStackParamList, T>;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
|
||||
import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen';
|
||||
import KYCVerifiedScreen from '@/screens/kyc/KYCVerifiedScreen';
|
||||
import AccountVerifiedSuccessScreen from '@/screens/onboarding/AccountVerifiedSuccessScreen';
|
||||
import DisclaimerScreen from '@/screens/onboarding/DisclaimerScreen';
|
||||
import SaveRecoveryPhraseScreen from '@/screens/onboarding/SaveRecoveryPhraseScreen';
|
||||
@@ -30,6 +32,20 @@ const onboardingScreens = {
|
||||
animation: 'slide_from_bottom',
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
KycSuccess: {
|
||||
screen: KycSuccessScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
animation: 'slide_from_bottom',
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
KYCVerified: {
|
||||
screen: KYCVerifiedScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
animation: 'slide_from_bottom',
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
};
|
||||
|
||||
export default onboardingScreens;
|
||||
|
||||
@@ -2,18 +2,213 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { DocumentCategory } from '@selfxyz/common/types';
|
||||
import type { DocumentCategory } from '@selfxyz/common/utils/types';
|
||||
|
||||
import type { ModalNavigationParams } from '@/screens/app/ModalScreen';
|
||||
import type { WebViewScreenParams } from '@/screens/shared/WebViewScreen';
|
||||
import type { ProofHistory } from '@/stores/proofTypes';
|
||||
|
||||
// =============================================================================
|
||||
// Aadhaar Screens
|
||||
// =============================================================================
|
||||
|
||||
export type AadhaarRoutesParamList = {
|
||||
AadhaarUpload: {
|
||||
countryCode: string;
|
||||
};
|
||||
AadhaarUploadSuccess: undefined;
|
||||
AadhaarUploadError: {
|
||||
errorType: string;
|
||||
};
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Account/Recovery Screens
|
||||
// =============================================================================
|
||||
|
||||
export type AccountRoutesParamList = {
|
||||
AccountRecovery:
|
||||
| {
|
||||
nextScreen?: string;
|
||||
}
|
||||
| undefined;
|
||||
SaveRecoveryPhrase:
|
||||
| {
|
||||
nextScreen?: string;
|
||||
}
|
||||
| undefined;
|
||||
CloudBackupSettings:
|
||||
| {
|
||||
nextScreen?: 'SaveRecoveryPhrase';
|
||||
returnToScreen?: 'Points';
|
||||
}
|
||||
| undefined;
|
||||
ProofSettings: undefined;
|
||||
AccountVerifiedSuccess: undefined;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// App Screens
|
||||
// =============================================================================
|
||||
|
||||
export type AppRoutesParamList = {
|
||||
Loading: {
|
||||
documentCategory?: DocumentCategory;
|
||||
signatureAlgorithm?: string;
|
||||
curveOrExponent?: string;
|
||||
};
|
||||
Modal: ModalNavigationParams;
|
||||
Gratification: {
|
||||
points?: number;
|
||||
};
|
||||
StarfallPushCode: undefined;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Dev Screens
|
||||
// =============================================================================
|
||||
|
||||
export type DevRoutesParamList = {
|
||||
CreateMock: undefined;
|
||||
MockDataDeepLink: undefined;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Document Screens
|
||||
// =============================================================================
|
||||
|
||||
export type DocumentRoutesParamList = {
|
||||
IDPicker: {
|
||||
countryCode: string;
|
||||
documentTypes: string[];
|
||||
};
|
||||
LogoConfirmation: {
|
||||
documentType: string;
|
||||
countryCode: string;
|
||||
};
|
||||
ConfirmBelonging:
|
||||
| {
|
||||
documentCategory?: DocumentCategory;
|
||||
signatureAlgorithm?: string;
|
||||
curveOrExponent?: string;
|
||||
}
|
||||
| undefined;
|
||||
DocumentNFCScan:
|
||||
| {
|
||||
passportNumber?: string;
|
||||
dateOfBirth?: string;
|
||||
dateOfExpiry?: string;
|
||||
}
|
||||
| undefined;
|
||||
DocumentCameraTrouble: undefined;
|
||||
DocumentOnboarding: undefined;
|
||||
IdDetails: undefined;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Combined Types
|
||||
// =============================================================================
|
||||
/**
|
||||
* All route param types that need to be explicitly defined (not inferred from initialParams).
|
||||
* This is used to compose RootStackParamList in index.tsx.
|
||||
*/
|
||||
export type ExplicitRouteParams = AadhaarRoutesParamList &
|
||||
AccountRoutesParamList &
|
||||
AppRoutesParamList &
|
||||
DevRoutesParamList &
|
||||
DocumentRoutesParamList &
|
||||
HomeRoutesParamList &
|
||||
OnboardingRoutesParamList &
|
||||
RegistrationRoutesParamList &
|
||||
SharedRoutesParamList &
|
||||
VerificationRoutesParamList;
|
||||
|
||||
// =============================================================================
|
||||
// Home Screens
|
||||
// =============================================================================
|
||||
export type HomeRoutesParamList = {
|
||||
Home: {
|
||||
testReferralFlow?: boolean;
|
||||
};
|
||||
Points: undefined;
|
||||
PointsInfo:
|
||||
| {
|
||||
showNextButton?: boolean;
|
||||
callbackId?: number;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Keys that need to be omitted from BaseRootStackParamList before merging with ExplicitRouteParams.
|
||||
* These are routes whose params are explicitly defined rather than inferred.
|
||||
*/
|
||||
export type OmittedRouteKeys = keyof ExplicitRouteParams;
|
||||
|
||||
// =============================================================================
|
||||
// Onboarding Screens
|
||||
// =============================================================================
|
||||
export type OnboardingRoutesParamList = {
|
||||
Disclaimer: undefined;
|
||||
KycSuccess:
|
||||
| {
|
||||
userId?: string;
|
||||
}
|
||||
| undefined;
|
||||
KYCVerified:
|
||||
| {
|
||||
status?: string;
|
||||
userId?: string;
|
||||
}
|
||||
| undefined;
|
||||
KycFailure: {
|
||||
countryCode?: string;
|
||||
canRetry?: boolean;
|
||||
};
|
||||
KycConnectionError: {
|
||||
countryCode?: string;
|
||||
};
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Registration Fallback Screens
|
||||
// =============================================================================
|
||||
export type RegistrationRoutesParamList = {
|
||||
RegistrationFallbackMRZ: {
|
||||
countryCode: string;
|
||||
};
|
||||
RegistrationFallbackNFC: {
|
||||
countryCode: string;
|
||||
};
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Shared Screens
|
||||
// =============================================================================
|
||||
export type SharedRoutesParamList = {
|
||||
ComingSoon: {
|
||||
countryCode?: string;
|
||||
documentCategory?: DocumentCategory;
|
||||
};
|
||||
WebView: {
|
||||
url: string;
|
||||
title?: string;
|
||||
shareTitle?: string;
|
||||
shareMessage?: string;
|
||||
shareUrl?: string;
|
||||
documentCategory?: string;
|
||||
};
|
||||
WebView: WebViewScreenParams;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Verification/Proof Screens
|
||||
// =============================================================================
|
||||
export type VerificationRoutesParamList = {
|
||||
ProofHistoryDetail: {
|
||||
data: ProofHistory;
|
||||
};
|
||||
Prove:
|
||||
| {
|
||||
scrollOffset?: number;
|
||||
}
|
||||
| undefined;
|
||||
ProvingScreenRouter: undefined;
|
||||
DocumentSelectorForProving:
|
||||
| {
|
||||
documentType?: string;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
import type { AlertModalParams } from '@/components/AlertModal';
|
||||
import AlertModal from '@/components/AlertModal';
|
||||
import FeedbackModal from '@/components/FeedbackModal';
|
||||
import type { FeedbackModalScreenParams } from '@/components/FeedbackModalScreen';
|
||||
import FeedbackModalScreen from '@/components/FeedbackModalScreen';
|
||||
import type { FeedbackType } from '@/hooks/useFeedbackModal';
|
||||
import { useFeedbackModal } from '@/hooks/useFeedbackModal';
|
||||
|
||||
@@ -19,7 +19,7 @@ interface FeedbackContextType {
|
||||
name?: string,
|
||||
email?: string,
|
||||
) => Promise<void>;
|
||||
showModal: (params: FeedbackModalScreenParams) => void;
|
||||
showModal: (params: AlertModalParams) => void;
|
||||
}
|
||||
|
||||
const FeedbackContext = createContext<FeedbackContextType | undefined>(
|
||||
@@ -50,13 +50,9 @@ export const FeedbackProvider: React.FC<FeedbackProviderProps> = ({
|
||||
>
|
||||
{children}
|
||||
|
||||
<FeedbackModal
|
||||
visible={isVisible}
|
||||
onClose={hideFeedbackModal}
|
||||
onSubmit={submitFeedback}
|
||||
/>
|
||||
<FeedbackModal visible={isVisible} onClose={hideFeedbackModal} />
|
||||
|
||||
<FeedbackModalScreen
|
||||
<AlertModal
|
||||
visible={isModalVisible}
|
||||
modalParams={modalParams}
|
||||
onHideModal={hideModal}
|
||||
|
||||
@@ -4,16 +4,98 @@
|
||||
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
|
||||
import messaging from '@react-native-firebase/messaging';
|
||||
|
||||
import { NotificationEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
|
||||
|
||||
import { navigationRef } from '@/navigation';
|
||||
import { trackEvent } from '@/services/analytics';
|
||||
|
||||
// Queue for pending navigation actions that need to wait for navigation to be ready
|
||||
let pendingNavigation: FirebaseMessagingTypes.RemoteMessage | null = null;
|
||||
|
||||
/**
|
||||
* Execute navigation for a notification
|
||||
* @returns true if navigation was executed, false if it needs to be queued
|
||||
*/
|
||||
const executeNotificationNavigation = (
|
||||
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
|
||||
): boolean => {
|
||||
if (!navigationRef.isReady()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const notificationType = remoteMessage.data?.type;
|
||||
const status = remoteMessage.data?.status;
|
||||
|
||||
// Handle KYC result notifications
|
||||
if (notificationType === 'kyc_result') {
|
||||
if (status === 'approved') {
|
||||
navigationRef.navigate('KYCVerified', {
|
||||
status: String(status),
|
||||
userId: remoteMessage.data?.user_id
|
||||
? String(remoteMessage.data.user_id)
|
||||
: undefined,
|
||||
});
|
||||
return true;
|
||||
} else if (status === 'rejected') {
|
||||
navigationRef.navigate('KycFailure', {
|
||||
canRetry: false,
|
||||
});
|
||||
return true;
|
||||
} else if (status === 'retry') {
|
||||
// Take user directly to verification flow to retry
|
||||
navigationRef.navigate('CountryPicker');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Navigation handled (or not applicable)
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle navigation based on notification type and data
|
||||
* Queues navigation if navigationRef is not ready yet
|
||||
*/
|
||||
const handleNotificationNavigation = (
|
||||
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
|
||||
) => {
|
||||
const executed = executeNotificationNavigation(remoteMessage);
|
||||
|
||||
if (!executed) {
|
||||
// Navigation not ready yet - queue for later
|
||||
pendingNavigation = remoteMessage;
|
||||
if (__DEV__) {
|
||||
console.log(
|
||||
'Navigation not ready, queuing notification navigation:',
|
||||
remoteMessage.data?.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Process any pending navigation once navigation is ready
|
||||
*/
|
||||
const processPendingNavigation = () => {
|
||||
if (pendingNavigation && navigationRef.isReady()) {
|
||||
if (__DEV__) {
|
||||
console.log(
|
||||
'Processing pending notification navigation:',
|
||||
pendingNavigation.data?.type,
|
||||
);
|
||||
}
|
||||
executeNotificationNavigation(pendingNavigation);
|
||||
pendingNavigation = null;
|
||||
}
|
||||
};
|
||||
|
||||
export const NotificationTrackingProvider: React.FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
// Handle notification tap when app is in background
|
||||
const unsubscribe = messaging().onNotificationOpenedApp(remoteMessage => {
|
||||
trackEvent(NotificationEvents.BACKGROUND_NOTIFICATION_OPENED, {
|
||||
messageId: remoteMessage.messageId,
|
||||
@@ -22,8 +104,12 @@ export const NotificationTrackingProvider: React.FC<PropsWithChildren> = ({
|
||||
// Track if user interacted with any actions
|
||||
actionId: remoteMessage.data?.actionId,
|
||||
});
|
||||
|
||||
// Handle navigation based on notification type
|
||||
handleNotificationNavigation(remoteMessage);
|
||||
});
|
||||
|
||||
// Handle notification tap when app is completely closed (cold start)
|
||||
messaging()
|
||||
.getInitialNotification()
|
||||
.then(remoteMessage => {
|
||||
@@ -35,11 +121,34 @@ export const NotificationTrackingProvider: React.FC<PropsWithChildren> = ({
|
||||
// Track if user interacted with any actions
|
||||
actionId: remoteMessage.data?.actionId,
|
||||
});
|
||||
|
||||
// Handle navigation based on notification type
|
||||
handleNotificationNavigation(remoteMessage);
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
// Monitor navigation readiness and process pending navigation
|
||||
useEffect(() => {
|
||||
// Check immediately if navigation is already ready
|
||||
if (navigationRef.isReady()) {
|
||||
processPendingNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Poll for navigation readiness if not ready yet
|
||||
const checkInterval = setInterval(() => {
|
||||
if (navigationRef.isReady()) {
|
||||
processPendingNavigation();
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 100); // Check every 100ms
|
||||
|
||||
// Cleanup interval on unmount
|
||||
return () => clearInterval(checkInterval);
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
@@ -13,14 +13,17 @@ import {
|
||||
type LogLevel,
|
||||
type NFCScanContext,
|
||||
reactNativeScannerAdapter,
|
||||
sanitizeErrorMessage,
|
||||
SdkEvents,
|
||||
SelfClientProvider as SDKSelfClientProvider,
|
||||
type TrackEventParams,
|
||||
useMRZStore,
|
||||
webNFCScannerShim,
|
||||
type WsConn,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import { logNFCEvent, logProofEvent } from '@/config/sentry';
|
||||
import { fetchAccessToken, launchSumsub } from '@/integrations/sumsub';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { navigationRef } from '@/navigation';
|
||||
import {
|
||||
@@ -32,7 +35,12 @@ import {
|
||||
setPassportKeychainErrorCallback,
|
||||
} from '@/providers/passportDataProvider';
|
||||
import { trackEvent, trackNfcEvent } from '@/services/analytics';
|
||||
import {
|
||||
type InjectedErrorType,
|
||||
useErrorInjectionStore,
|
||||
} from '@/stores/errorInjectionStore';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { IS_DEV_MODE } from '@/utils/devUtils';
|
||||
import {
|
||||
registerModalCallbacks,
|
||||
unregisterModalCallbacks,
|
||||
@@ -67,7 +75,20 @@ function navigateIfReady<RouteName extends keyof RootStackParamList>(
|
||||
}
|
||||
|
||||
export const SelfClientProvider = ({ children }: PropsWithChildren) => {
|
||||
const config = useMemo(() => ({}), []);
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
devConfig: IS_DEV_MODE
|
||||
? {
|
||||
shouldTrigger: (errorType: string) => {
|
||||
return useErrorInjectionStore
|
||||
.getState()
|
||||
.shouldTrigger(errorType as InjectedErrorType);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const adapters: Adapters = useMemo(
|
||||
() => ({
|
||||
scanner:
|
||||
@@ -166,6 +187,9 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
|
||||
const appListeners = useMemo(() => {
|
||||
const { map, addListener } = createListenersMap();
|
||||
|
||||
// Track current countryCode for error navigation
|
||||
let currentCountryCode = '';
|
||||
|
||||
addListener(SdkEvents.PROVING_PASSPORT_DATA_NOT_FOUND, () => {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.navigate('DocumentDataNotFound');
|
||||
@@ -260,7 +284,9 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
|
||||
});
|
||||
|
||||
addListener(SdkEvents.DOCUMENT_MRZ_READ_FAILURE, () => {
|
||||
navigateIfReady('DocumentCameraTrouble');
|
||||
navigateIfReady('RegistrationFallbackMRZ', {
|
||||
countryCode: currentCountryCode,
|
||||
});
|
||||
});
|
||||
|
||||
addListener(SdkEvents.PROVING_AADHAAR_UPLOAD_SUCCESS, () => {
|
||||
@@ -279,6 +305,9 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
|
||||
countryCode: string;
|
||||
documentTypes: string[];
|
||||
}) => {
|
||||
currentCountryCode = countryCode;
|
||||
// Store country code early so it's available for Sumsub fallback flows
|
||||
useMRZStore.getState().update({ countryCode });
|
||||
navigateIfReady('IDPicker', { countryCode, documentTypes });
|
||||
},
|
||||
);
|
||||
@@ -288,16 +317,106 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
|
||||
if (navigationRef.isReady()) {
|
||||
switch (documentType) {
|
||||
case 'p':
|
||||
navigationRef.navigate('DocumentOnboarding');
|
||||
break;
|
||||
case 'i':
|
||||
navigationRef.navigate('DocumentOnboarding');
|
||||
// Navigate to logo confirmation screen for biometric IDs
|
||||
navigationRef.navigate('LogoConfirmation', {
|
||||
documentType,
|
||||
countryCode,
|
||||
});
|
||||
break;
|
||||
case 'a':
|
||||
if (countryCode) {
|
||||
navigationRef.navigate('AadhaarUpload', { countryCode });
|
||||
}
|
||||
break;
|
||||
case 'kyc':
|
||||
(async () => {
|
||||
try {
|
||||
// Dev-only: Check for injected initialization error
|
||||
if (
|
||||
useErrorInjectionStore
|
||||
.getState()
|
||||
.shouldTrigger('sumsub_initialization')
|
||||
) {
|
||||
console.log('[DEV] Injecting Sumsub initialization error');
|
||||
throw new Error(
|
||||
'Injected Sumsub initialization error for testing',
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = await fetchAccessToken();
|
||||
const result = await launchSumsub({
|
||||
accessToken: accessToken.token,
|
||||
});
|
||||
|
||||
console.log('[Sumsub] Result:', JSON.stringify(result));
|
||||
|
||||
// User cancelled/dismissed without completing verification
|
||||
// Status values: 'Initial' (never started), 'Incomplete' (started but not finished),
|
||||
// 'Interrupted' (explicitly cancelled)
|
||||
const cancelledStatuses = [
|
||||
'Initial',
|
||||
'Incomplete',
|
||||
'Interrupted',
|
||||
];
|
||||
if (cancelledStatuses.includes(result.status)) {
|
||||
console.log(
|
||||
'[Sumsub] User cancelled or closed without completing, status:',
|
||||
result.status,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dev-only: Check for injected verification error
|
||||
const shouldInjectVerificationError = useErrorInjectionStore
|
||||
.getState()
|
||||
.shouldTrigger('sumsub_verification');
|
||||
|
||||
// Actual error from provider
|
||||
if (!result.success || shouldInjectVerificationError) {
|
||||
if (shouldInjectVerificationError) {
|
||||
console.log('[DEV] Injecting Sumsub verification error');
|
||||
} else {
|
||||
const safeError = sanitizeErrorMessage(
|
||||
result.errorMsg || result.errorType || 'unknown_error',
|
||||
);
|
||||
console.error('KYC provider failed:', safeError);
|
||||
}
|
||||
// Guard navigation call after async operations
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.navigate('KycFailure', {
|
||||
countryCode,
|
||||
canRetry: true,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// User completed verification (status: 'Pending', 'Approved', etc.)
|
||||
// Navigate to KYC success screen
|
||||
console.log(
|
||||
'[Sumsub] Verification submitted, status:',
|
||||
result.status,
|
||||
);
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.navigate('KycSuccess', {
|
||||
userId: accessToken.userId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const safeInitError = sanitizeErrorMessage(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
console.error('Error in KYC flow:', safeInitError);
|
||||
// Guard navigation call after async operations
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.navigate('KycConnectionError', {
|
||||
countryCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
break;
|
||||
default:
|
||||
if (countryCode) {
|
||||
navigationRef.navigate('ComingSoon', { countryCode });
|
||||
|
||||
@@ -202,6 +202,10 @@ export function getAlternativeCSCA(
|
||||
useProtocolStore: SelfClient['useProtocolStore'],
|
||||
docCategory: DocumentCategory,
|
||||
): AlternativeCSCA {
|
||||
if (docCategory === 'kyc') {
|
||||
//TODO
|
||||
throw new Error('KYC is not supported yet');
|
||||
}
|
||||
if (docCategory === 'aadhaar') {
|
||||
const publicKeys = useProtocolStore.getState().aadhaar.public_keys;
|
||||
// Convert string[] to Record<string, string> format expected by AlternativeCSCA
|
||||
|
||||
@@ -111,6 +111,10 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
|
||||
return useProtocolStore.getState()[docCategory].commitment_tree;
|
||||
},
|
||||
getAltCSCA(docCategory) {
|
||||
if (docCategory === 'kyc') {
|
||||
//TODO
|
||||
throw new Error('KYC is not supported yet');
|
||||
}
|
||||
if (docCategory === 'aadhaar') {
|
||||
const publicKeys =
|
||||
useProtocolStore.getState().aadhaar.public_keys;
|
||||
|
||||
@@ -99,6 +99,10 @@ const RecoverWithPhraseScreen: React.FC = () => {
|
||||
return useProtocolStore.getState()[docCategory].commitment_tree;
|
||||
},
|
||||
getAltCSCA(docCategory) {
|
||||
if (docCategory === 'kyc') {
|
||||
//TODO
|
||||
throw new Error('KYC is not supported yet');
|
||||
}
|
||||
if (docCategory === 'aadhaar') {
|
||||
const publicKeys =
|
||||
useProtocolStore.getState().aadhaar.public_keys;
|
||||
|
||||
@@ -18,12 +18,7 @@ import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
const ProofSettingsScreen: React.FC = () => {
|
||||
const {
|
||||
skipDocumentSelector,
|
||||
setSkipDocumentSelector,
|
||||
skipDocumentSelectorIfSingle,
|
||||
setSkipDocumentSelectorIfSingle,
|
||||
} = useSettingStore();
|
||||
const { skipDocumentSelector, setSkipDocumentSelector } = useSettingStore();
|
||||
|
||||
return (
|
||||
<YStack flex={1} backgroundColor={white}>
|
||||
@@ -49,35 +44,6 @@ const ProofSettingsScreen: React.FC = () => {
|
||||
testID="skip-document-selector-toggle"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={styles.settingLabel}>
|
||||
Skip when only one document
|
||||
</Text>
|
||||
<Text style={styles.settingDescription}>
|
||||
Automatically select your document when you only have one valid
|
||||
ID available
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={skipDocumentSelectorIfSingle}
|
||||
onValueChange={setSkipDocumentSelectorIfSingle}
|
||||
trackColor={{ false: slate200, true: blue600 }}
|
||||
thumbColor={white}
|
||||
disabled={skipDocumentSelector}
|
||||
testID="skip-document-selector-if-single-toggle"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{skipDocumentSelector && (
|
||||
<Text style={styles.infoText}>
|
||||
Document selection is always skipped. The "Skip when only one
|
||||
document" setting has no effect.
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
@@ -114,17 +80,6 @@ const styles = StyleSheet.create({
|
||||
fontFamily: dinot,
|
||||
color: slate500,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: slate200,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 13,
|
||||
fontFamily: dinot,
|
||||
fontStyle: 'italic',
|
||||
color: slate500,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export { ProofSettingsScreen };
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { PropsWithChildren } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Linking, Platform, Share, View as RNView } from 'react-native';
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import { getCountry, getLocales, getTimeZone } from 'react-native-localize';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import type { SvgProps } from 'react-native-svg';
|
||||
import { Button, ScrollView, View, XStack, YStack } from 'tamagui';
|
||||
@@ -45,10 +44,10 @@ import {
|
||||
} from '@/consts/links';
|
||||
import { impactLight } from '@/integrations/haptics';
|
||||
import { usePassport } from '@/providers/passportDataProvider';
|
||||
import { openSupportForm } from '@/services/support';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { extraYPadding } from '@/utils/styleUtils';
|
||||
|
||||
import { version } from '../../../../package.json';
|
||||
// Avoid importing RootStackParamList to prevent type cycles; use minimal typing
|
||||
type MinimalRootStackParamList = Record<string, object | undefined>;
|
||||
|
||||
@@ -61,9 +60,8 @@ interface SocialButtonProps {
|
||||
href: string;
|
||||
}
|
||||
|
||||
const emailFeedback = 'support@self.xyz';
|
||||
// Avoid importing RootStackParamList; we only need string route names plus a few literals
|
||||
type RouteOption = string | 'share' | 'email_feedback' | 'ManageDocuments';
|
||||
type RouteOption = string | 'share' | 'support_form' | 'ManageDocuments';
|
||||
|
||||
const storeURL = Platform.OS === 'ios' ? appStoreUrl : playStoreUrl;
|
||||
|
||||
@@ -79,7 +77,7 @@ const routes =
|
||||
[Lock, 'Reveal recovery phrase', 'ShowRecoveryPhrase'],
|
||||
[Cloud, 'Cloud backup', 'CloudBackupSettings'],
|
||||
[Settings2 as React.FC<SvgProps>, 'Proof settings', 'ProofSettings'],
|
||||
[Feedback, 'Send feedback', 'email_feedback'],
|
||||
[Feedback, 'Get support', 'support_form'],
|
||||
[ShareIcon, 'Share Self app', 'share'],
|
||||
[
|
||||
FileText as React.FC<SvgProps>,
|
||||
@@ -90,7 +88,7 @@ const routes =
|
||||
: ([
|
||||
[Data, 'View document info', 'DocumentDataInfo'],
|
||||
[Settings2 as React.FC<SvgProps>, 'Proof settings', 'ProofSettings'],
|
||||
[Feedback, 'Send feeback', 'email_feedback'],
|
||||
[Feedback, 'Get support', 'support_form'],
|
||||
[
|
||||
FileText as React.FC<SvgProps>,
|
||||
'Manage ID documents',
|
||||
@@ -222,32 +220,17 @@ const SettingsScreen: React.FC = () => {
|
||||
);
|
||||
break;
|
||||
|
||||
case 'email_feedback':
|
||||
const subject = 'SELF App Feedback';
|
||||
const deviceInfo = [
|
||||
['device', `${Platform.OS}@${Platform.Version}`],
|
||||
['app', `v${version}`],
|
||||
[
|
||||
'locales',
|
||||
getLocales()
|
||||
.map(locale => `${locale.languageCode}-${locale.countryCode}`)
|
||||
.join(','),
|
||||
],
|
||||
['country', getCountry()],
|
||||
['tz', getTimeZone()],
|
||||
['ts', new Date()],
|
||||
['origin', 'settings/feedback'],
|
||||
] as [string, string][];
|
||||
|
||||
const body = `
|
||||
---
|
||||
${deviceInfo.map(([k, v]) => `${k}=${v}`).join('; ')}
|
||||
---`;
|
||||
await Linking.openURL(
|
||||
`mailto:${emailFeedback}?subject=${encodeURIComponent(
|
||||
subject,
|
||||
)}&body=${encodeURIComponent(body)}`,
|
||||
);
|
||||
case 'support_form':
|
||||
try {
|
||||
await openSupportForm();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'SettingsScreen: failed to open support form:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
// Error is already handled and displayed to user in openSupportForm,
|
||||
// but we log here for debugging purposes
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ManageDocuments':
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
StyleSheet,
|
||||
Text as RNText,
|
||||
} from 'react-native';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Text, View, YStack } from 'tamagui';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
@@ -28,6 +27,7 @@ import { dinot, dinotBold } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import GratificationBg from '@/assets/images/gratification_bg.svg';
|
||||
import SelfLogo from '@/assets/logos/self.svg';
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
|
||||
const GratificationScreen: React.FC = () => {
|
||||
|
||||
@@ -107,8 +107,8 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({ route }) => {
|
||||
} else {
|
||||
await init(selfClient, 'dsc', true);
|
||||
}
|
||||
} catch {
|
||||
console.error('Error loading selected document:');
|
||||
} catch (error) {
|
||||
console.error('Error loading selected document:', error);
|
||||
await init(selfClient, 'dsc', true);
|
||||
} finally {
|
||||
setIsInitializing(false);
|
||||
|
||||
@@ -2,812 +2,115 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
||||
import { Alert, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { Check, ChevronDown, ChevronRight } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
red500,
|
||||
slate100,
|
||||
slate200,
|
||||
slate400,
|
||||
slate500,
|
||||
slate600,
|
||||
slate800,
|
||||
slate900,
|
||||
white,
|
||||
yellow500,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks';
|
||||
|
||||
import BugIcon from '@/assets/icons/bug_icon.svg';
|
||||
import WarningIcon from '@/assets/icons/warning.svg';
|
||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { navigationScreens } from '@/navigation';
|
||||
import { unsafe_clearSecrets } from '@/providers/authProvider';
|
||||
import { usePassport } from '@/providers/passportDataProvider';
|
||||
import { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector';
|
||||
import { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
import { useDangerZoneActions } from '@/screens/dev/hooks/useDangerZoneActions';
|
||||
import { useNotificationHandlers } from '@/screens/dev/hooks/useNotificationHandlers';
|
||||
import {
|
||||
isNotificationSystemReady,
|
||||
requestNotificationPermission,
|
||||
subscribeToTopics,
|
||||
unsubscribeFromTopics,
|
||||
} from '@/services/notifications/notificationService';
|
||||
import { usePointEventStore } from '@/stores/pointEventStore';
|
||||
DangerZoneSection,
|
||||
DebugShortcutsSection,
|
||||
DevTogglesSection,
|
||||
PushNotificationsSection,
|
||||
} from '@/screens/dev/sections';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { IS_DEV_MODE } from '@/utils/devUtils';
|
||||
|
||||
interface TopicToggleButtonProps {
|
||||
label: string;
|
||||
isSubscribed: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
const TopicToggleButton: React.FC<TopicToggleButtonProps> = ({
|
||||
label,
|
||||
isSubscribed,
|
||||
onToggle,
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
backgroundColor={isSubscribed ? '$green9' : slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={onToggle}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
paddingHorizontal="$4"
|
||||
pressStyle={{
|
||||
opacity: 0.8,
|
||||
scale: 0.98,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={isSubscribed ? white : slate600}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
color={isSubscribed ? white : slate400}
|
||||
fontSize="$3"
|
||||
fontFamily={dinot}
|
||||
>
|
||||
{isSubscribed ? 'Enabled' : 'Disabled'}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface DevSettingsScreenProps extends PropsWithChildren {
|
||||
color?: string;
|
||||
width?: number;
|
||||
justifyContent?:
|
||||
| 'center'
|
||||
| 'unset'
|
||||
| 'flex-start'
|
||||
| 'flex-end'
|
||||
| 'space-between'
|
||||
| 'space-around'
|
||||
| 'space-evenly';
|
||||
userSelect?: 'all' | 'text' | 'none' | 'contain';
|
||||
textAlign?: 'center' | 'left' | 'right';
|
||||
style?: StyleProp<TextStyle | ViewStyle>;
|
||||
}
|
||||
|
||||
function ParameterSection({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
darkMode,
|
||||
children,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
darkMode?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const renderIcon = () => {
|
||||
const iconElement =
|
||||
typeof icon === 'function'
|
||||
? (icon as () => React.ReactNode)()
|
||||
: isValidElement(icon)
|
||||
? icon
|
||||
: null;
|
||||
|
||||
return iconElement
|
||||
? cloneElement(iconElement as React.ReactElement, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
})
|
||||
: null;
|
||||
};
|
||||
|
||||
return (
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={darkMode ? slate900 : slate100}
|
||||
borderRadius="$4"
|
||||
borderWidth={1}
|
||||
borderColor={darkMode ? slate800 : slate200}
|
||||
padding="$4"
|
||||
flexDirection="column"
|
||||
gap="$3"
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
flexDirection="row"
|
||||
justifyContent="flex-start"
|
||||
gap="$4"
|
||||
>
|
||||
<YStack
|
||||
backgroundColor="gray"
|
||||
borderRadius={5}
|
||||
width={46}
|
||||
height={46}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
padding="$2"
|
||||
>
|
||||
{renderIcon()}
|
||||
</YStack>
|
||||
<YStack flexDirection="column" gap="$1">
|
||||
<Text
|
||||
fontSize="$5"
|
||||
color={darkMode ? white : slate600}
|
||||
fontFamily={dinot}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text fontSize="$3" color={slate400} fontFamily={dinot}>
|
||||
{description}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
{children}
|
||||
</YStack>
|
||||
);
|
||||
}
|
||||
|
||||
const ScreenSelector = ({}) => {
|
||||
const navigation = useNavigation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const screenList = useMemo(
|
||||
() =>
|
||||
(
|
||||
Object.keys(navigationScreens) as (keyof typeof navigationScreens)[]
|
||||
).sort(),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => setOpen(true)}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
Select screen
|
||||
</Text>
|
||||
<ChevronDown color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
snapPoints={[85]}
|
||||
animation="medium"
|
||||
dismissOnSnapToBottom
|
||||
>
|
||||
<Sheet.Overlay />
|
||||
<Sheet.Frame
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius="$9"
|
||||
borderTopRightRadius="$9"
|
||||
>
|
||||
<YStack padding="$4">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
>
|
||||
<Text fontSize="$8" fontFamily={dinot}>
|
||||
Select screen
|
||||
</Text>
|
||||
<Button
|
||||
onPress={() => setOpen(false)}
|
||||
padding="$2"
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<ChevronDown
|
||||
color={slate500}
|
||||
strokeWidth={2.5}
|
||||
style={{ transform: [{ rotate: '180deg' }] }}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 100 }}
|
||||
>
|
||||
{screenList.map(item => (
|
||||
<TouchableOpacity
|
||||
key={item}
|
||||
onPress={() => {
|
||||
setOpen(false);
|
||||
navigation.navigate(item as never);
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
>
|
||||
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
|
||||
{item}
|
||||
</Text>
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LogLevelSelector = ({
|
||||
currentLevel,
|
||||
onSelect,
|
||||
}: {
|
||||
currentLevel: string;
|
||||
onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const logLevels = ['debug', 'info', 'warn', 'error'] as const;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => setOpen(true)}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
{currentLevel.toUpperCase()}
|
||||
</Text>
|
||||
<ChevronDown color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
snapPoints={[50]}
|
||||
animation="medium"
|
||||
dismissOnSnapToBottom
|
||||
>
|
||||
<Sheet.Overlay />
|
||||
<Sheet.Frame
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius="$9"
|
||||
borderTopRightRadius="$9"
|
||||
>
|
||||
<YStack padding="$4">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
>
|
||||
<Text fontSize="$8" fontFamily={dinot}>
|
||||
Select log level
|
||||
</Text>
|
||||
<Button
|
||||
onPress={() => setOpen(false)}
|
||||
padding="$2"
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<ChevronDown
|
||||
color={slate500}
|
||||
strokeWidth={2.5}
|
||||
style={{ transform: [{ rotate: '180deg' }] }}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
{logLevels.map(level => (
|
||||
<TouchableOpacity
|
||||
key={level}
|
||||
onPress={() => {
|
||||
setOpen(false);
|
||||
onSelect(level);
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
|
||||
{level.toUpperCase()}
|
||||
</Text>
|
||||
{currentLevel === level && (
|
||||
<Check color={slate600} size={20} />
|
||||
)}
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
const { clearDocumentCatalogForMigrationTesting } = usePassport();
|
||||
const clearPointEvents = usePointEventStore(state => state.clearEvents);
|
||||
const { resetBackupForPoints } = useSettingStore();
|
||||
const DevSettingsScreen: React.FC = () => {
|
||||
const navigation =
|
||||
useNavigation() as NativeStackScreenProps<RootStackParamList>['navigation'];
|
||||
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
|
||||
const loggingSeverity = useSettingStore(state => state.loggingSeverity);
|
||||
const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity);
|
||||
const [hasNotificationPermission, setHasNotificationPermission] =
|
||||
useState(false);
|
||||
const paddingBottom = useSafeBottomPadding(20);
|
||||
|
||||
// Check notification permissions on mount
|
||||
useEffect(() => {
|
||||
const checkPermissions = async () => {
|
||||
const readiness = await isNotificationSystemReady();
|
||||
setHasNotificationPermission(readiness.ready);
|
||||
};
|
||||
checkPermissions();
|
||||
}, []);
|
||||
// Settings store
|
||||
const loggingSeverity = useSettingStore(state => state.loggingSeverity);
|
||||
const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity);
|
||||
const useStrongBox = useSettingStore(state => state.useStrongBox);
|
||||
const setUseStrongBox = useSettingStore(state => state.setUseStrongBox);
|
||||
const kycEnabled = useSettingStore(state => state.kycEnabled);
|
||||
const setKycEnabled = useSettingStore(state => state.setKycEnabled);
|
||||
|
||||
const handleTopicToggle = async (topics: string[], topicLabel: string) => {
|
||||
// Check permissions first
|
||||
if (!hasNotificationPermission) {
|
||||
Alert.alert(
|
||||
'Permissions Required',
|
||||
'Push notifications are not enabled. Would you like to enable them?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Enable',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const granted = await requestNotificationPermission();
|
||||
if (granted) {
|
||||
// Update permission state
|
||||
setHasNotificationPermission(true);
|
||||
Alert.alert(
|
||||
'Success',
|
||||
'Permissions granted! You can now subscribe to topics.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Failed',
|
||||
'Could not enable notifications. Please enable them in your device Settings.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to request permissions',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentlySubscribed = topics.every(topic =>
|
||||
subscribedTopics.includes(topic),
|
||||
);
|
||||
|
||||
if (isCurrentlySubscribed) {
|
||||
// Show confirmation dialog for unsubscribe
|
||||
Alert.alert(
|
||||
'Disable Notifications',
|
||||
`Are you sure you want to disable push notifications for ${topicLabel}?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Disable',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const result = await unsubscribeFromTopics(topics);
|
||||
if (result.successes.length > 0) {
|
||||
Alert.alert(
|
||||
'Success',
|
||||
`Disabled notifications for ${topicLabel}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
`Failed to disable: ${result.failures.map(f => f.error).join(', ')}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to unsubscribe',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// Subscribe without confirmation
|
||||
try {
|
||||
const result = await subscribeToTopics(topics);
|
||||
if (result.successes.length > 0) {
|
||||
Alert.alert('✅ Success', `Enabled notifications for ${topicLabel}`, [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
`Failed to enable: ${result.failures.map(f => f.error).join(', ')}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error instanceof Error ? error.message : 'Failed to subscribe',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearSecretsPress = () => {
|
||||
Alert.alert(
|
||||
'Delete Keychain Secrets',
|
||||
"Are you sure you want to remove your keychain secrets?\n\nIf this secret is not backed up, your account will be lost and the ID documents attached to it won't be usable.",
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await unsafe_clearSecrets();
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearDocumentCatalogPress = () => {
|
||||
Alert.alert(
|
||||
'Clear Document Catalog',
|
||||
'Are you sure you want to clear the document catalog?\n\nThis will remove all documents from the new storage system but preserve legacy storage for migration testing. You will need to restart the app to test migration.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await clearDocumentCatalogForMigrationTesting();
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearPointEventsPress = () => {
|
||||
Alert.alert(
|
||||
'Clear Point Events',
|
||||
'Are you sure you want to clear all point events from local storage?\n\nThis will reset your point history but not affect your actual points on the blockchain.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await clearPointEvents();
|
||||
Alert.alert('Success', 'Point events cleared successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleResetBackupStatePress = () => {
|
||||
Alert.alert(
|
||||
'Reset Backup State',
|
||||
'Are you sure you want to reset the backup state?\n\nThis will allow you to see and trigger the backup points flow again.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Reset',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
resetBackupForPoints();
|
||||
Alert.alert('Success', 'Backup state reset successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearBackupEventsPress = () => {
|
||||
Alert.alert(
|
||||
'Clear Backup Events',
|
||||
'Are you sure you want to clear all backup point events from local storage?\n\nThis will remove backup events from your point history.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
const events = usePointEventStore.getState().events;
|
||||
const backupEvents = events.filter(
|
||||
event => event.type === 'backup',
|
||||
);
|
||||
for (const event of backupEvents) {
|
||||
await usePointEventStore.getState().removeEvent(event.id);
|
||||
}
|
||||
Alert.alert('Success', 'Backup events cleared successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
// Custom hooks
|
||||
const { hasNotificationPermission, subscribedTopics, handleTopicToggle } =
|
||||
useNotificationHandlers();
|
||||
const {
|
||||
handleClearSecretsPress,
|
||||
handleClearDocumentCatalogPress,
|
||||
handleClearPointEventsPress,
|
||||
handleResetBackupStatePress,
|
||||
handleClearBackupEventsPress,
|
||||
} = useDangerZoneActions();
|
||||
|
||||
return (
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<YStack
|
||||
gap="$3"
|
||||
alignItems="center"
|
||||
backgroundColor="white"
|
||||
flex={1}
|
||||
paddingHorizontal="$4"
|
||||
paddingTop="$4"
|
||||
paddingBottom={paddingBottom}
|
||||
>
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Debug Shortcuts"
|
||||
description="Jump directly to any screen for testing"
|
||||
<ErrorBoundary>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<YStack
|
||||
gap="$3"
|
||||
alignItems="center"
|
||||
backgroundColor="white"
|
||||
flex={1}
|
||||
paddingHorizontal="$4"
|
||||
paddingTop="$4"
|
||||
paddingBottom={paddingBottom}
|
||||
>
|
||||
<YStack gap="$2">
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('DevPrivateKey');
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
View Private Key
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
{IS_DEV_MODE && (
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('Home', { testReferralFlow: true });
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
Test Referral Flow
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
)}
|
||||
<ScreenSelector />
|
||||
</YStack>
|
||||
</ParameterSection>
|
||||
<DebugShortcutsSection navigation={navigation} />
|
||||
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Push Notifications"
|
||||
description="Manage topic subscriptions"
|
||||
>
|
||||
<YStack gap="$2">
|
||||
<TopicToggleButton
|
||||
label="Starfall"
|
||||
isSubscribed={
|
||||
hasNotificationPermission && subscribedTopics.includes('nova')
|
||||
}
|
||||
onToggle={() => handleTopicToggle(['nova'], 'Starfall')}
|
||||
{IS_DEV_MODE && (
|
||||
<DevTogglesSection
|
||||
kycEnabled={kycEnabled}
|
||||
setKycEnabled={setKycEnabled}
|
||||
useStrongBox={useStrongBox}
|
||||
setUseStrongBox={setUseStrongBox}
|
||||
/>
|
||||
<TopicToggleButton
|
||||
label="General"
|
||||
isSubscribed={
|
||||
hasNotificationPermission &&
|
||||
subscribedTopics.includes('general')
|
||||
}
|
||||
onToggle={() => handleTopicToggle(['general'], 'General')}
|
||||
/>
|
||||
<TopicToggleButton
|
||||
label="Both (Starfall + General)"
|
||||
isSubscribed={
|
||||
hasNotificationPermission &&
|
||||
subscribedTopics.includes('nova') &&
|
||||
subscribedTopics.includes('general')
|
||||
}
|
||||
onToggle={() =>
|
||||
handleTopicToggle(['nova', 'general'], 'both topics')
|
||||
}
|
||||
/>
|
||||
</YStack>
|
||||
</ParameterSection>
|
||||
)}
|
||||
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Log Level"
|
||||
description="Configure logging verbosity"
|
||||
>
|
||||
<LogLevelSelector
|
||||
currentLevel={loggingSeverity}
|
||||
onSelect={setLoggingSeverity}
|
||||
<PushNotificationsSection
|
||||
hasNotificationPermission={hasNotificationPermission}
|
||||
subscribedTopics={subscribedTopics}
|
||||
onTopicToggle={handleTopicToggle}
|
||||
/>
|
||||
</ParameterSection>
|
||||
|
||||
<ParameterSection
|
||||
icon={<WarningIcon color={yellow500} />}
|
||||
title="Danger Zone"
|
||||
description="These actions are sensitive"
|
||||
darkMode={true}
|
||||
>
|
||||
{[
|
||||
{
|
||||
label: 'Delete your private key',
|
||||
onPress: handleClearSecretsPress,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear document catalog',
|
||||
onPress: handleClearDocumentCatalogPress,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear point events',
|
||||
onPress: handleClearPointEventsPress,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Reset backup state',
|
||||
onPress: handleResetBackupStatePress,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear backup events',
|
||||
onPress: handleClearBackupEventsPress,
|
||||
dangerTheme: true,
|
||||
},
|
||||
].map(({ label, onPress, dangerTheme }) => (
|
||||
<Button
|
||||
key={label}
|
||||
style={{ backgroundColor: dangerTheme ? red500 : white }}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={onPress}
|
||||
flexDirection="row"
|
||||
justifyContent="flex-start"
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Log Level"
|
||||
description="Configure logging verbosity"
|
||||
>
|
||||
<LogLevelSelector
|
||||
currentLevel={loggingSeverity}
|
||||
onSelect={setLoggingSeverity}
|
||||
/>
|
||||
</ParameterSection>
|
||||
|
||||
{IS_DEV_MODE && (
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Onboarding Error Testing"
|
||||
description="Test onboarding error flows"
|
||||
>
|
||||
<Text
|
||||
color={dangerTheme ? white : slate500}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Button>
|
||||
))}
|
||||
</ParameterSection>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
<ErrorInjectionSelector />
|
||||
</ParameterSection>
|
||||
)}
|
||||
|
||||
<DangerZoneSection
|
||||
onClearSecrets={handleClearSecretsPress}
|
||||
onClearDocumentCatalog={handleClearDocumentCatalogPress}
|
||||
onClearPointEvents={handleClearPointEventsPress}
|
||||
onResetBackupState={handleResetBackupStatePress}
|
||||
onClearBackupEvents={handleClearBackupEventsPress}
|
||||
/>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
686
app/src/screens/dev/SumsubTestScreen.tsx
Normal file
686
app/src/screens/dev/SumsubTestScreen.tsx
Normal file
@@ -0,0 +1,686 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Alert, ScrollView, TextInput } from 'react-native';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { Button, Text, XStack, YStack } from 'tamagui';
|
||||
import { SUMSUB_TEE_URL } from '@env';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { ChevronLeft } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
green500,
|
||||
red500,
|
||||
slate200,
|
||||
slate400,
|
||||
slate500,
|
||||
slate600,
|
||||
slate800,
|
||||
white,
|
||||
yellow500,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks';
|
||||
|
||||
import {
|
||||
fetchAccessToken,
|
||||
launchSumsub,
|
||||
type SumsubApplicantInfo,
|
||||
type SumsubResult,
|
||||
} from '@/integrations/sumsub';
|
||||
|
||||
const SumsubTestScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const [phoneNumber, setPhoneNumber] = useState('+11234567890');
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sdkLaunching, setSdkLaunching] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [result, setResult] = useState<SumsubResult | null>(null);
|
||||
const [applicantInfo, setApplicantInfo] =
|
||||
useState<SumsubApplicantInfo | null>(null);
|
||||
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
const hasSubscribedRef = useRef<boolean>(false);
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
const paddingBottom = useSafeBottomPadding(20);
|
||||
|
||||
const handleFetchToken = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setAccessToken(null);
|
||||
setUserId(null);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const response = await fetchAccessToken(phoneNumber);
|
||||
if (!isMountedRef.current) return;
|
||||
setAccessToken(response.token);
|
||||
setUserId(response.userId);
|
||||
Alert.alert('Success', 'Access token generated successfully', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} catch (err) {
|
||||
if (!isMountedRef.current) return;
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(message);
|
||||
Alert.alert('Error', `Failed to fetch access token: ${message}`, [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [phoneNumber]);
|
||||
|
||||
const subscribeToWebSocket = useCallback(() => {
|
||||
if (!userId || hasSubscribedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Connecting to WebSocket:', SUMSUB_TEE_URL);
|
||||
const socket = io(SUMSUB_TEE_URL, {
|
||||
transports: ['websocket', 'polling'],
|
||||
});
|
||||
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Socket connected, subscribing to user');
|
||||
hasSubscribedRef.current = true;
|
||||
socket.emit('subscribe', userId);
|
||||
});
|
||||
|
||||
socket.on('success', (data: SumsubApplicantInfo) => {
|
||||
console.log('Received applicant info');
|
||||
if (!isMountedRef.current) return;
|
||||
setApplicantInfo(data);
|
||||
Alert.alert(
|
||||
'Verification Complete',
|
||||
'Your verification was successful!',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('verification_failed', (reason: string) => {
|
||||
console.log('Verification failed:', reason);
|
||||
if (!isMountedRef.current) return;
|
||||
setError(`Verification failed: ${reason}`);
|
||||
Alert.alert('Verification Failed', reason, [{ text: 'OK' }]);
|
||||
});
|
||||
|
||||
socket.on('error', (errorMessage: string) => {
|
||||
console.error('Socket error:', errorMessage);
|
||||
if (!isMountedRef.current) return;
|
||||
setError(errorMessage);
|
||||
hasSubscribedRef.current = false;
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Socket disconnected');
|
||||
hasSubscribedRef.current = false;
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
const handleLaunchSumsub = useCallback(async () => {
|
||||
if (!accessToken) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'No access token available. Please generate one first.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setSdkLaunching(true);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const sdkResult = await launchSumsub({
|
||||
accessToken,
|
||||
debug: true,
|
||||
locale: 'en',
|
||||
onEvent: (eventType, _payload) => {
|
||||
console.log('SDK Event:', eventType);
|
||||
// Subscribe to WebSocket when verification is completed
|
||||
if (eventType === 'idCheck.onApplicantVerificationCompleted') {
|
||||
subscribeToWebSocket();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
setResult(sdkResult);
|
||||
|
||||
if (sdkResult.success) {
|
||||
Alert.alert(
|
||||
'SDK Closed',
|
||||
`Sumsub SDK closed with status: ${sdkResult.status}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
`Sumsub failed: ${sdkResult.errorMsg || sdkResult.errorType || 'Unknown error'}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Sumsub launch error:', err);
|
||||
if (!isMountedRef.current) return;
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(message);
|
||||
Alert.alert('Error', `Failed to launch Sumsub SDK: ${message}`, [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setSdkLaunching(false);
|
||||
}
|
||||
}
|
||||
}, [accessToken, subscribeToWebSocket]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setApplicantInfo(null);
|
||||
setAccessToken(null);
|
||||
setUserId(null);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
hasSubscribedRef.current = false;
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
hasSubscribedRef.current = false;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// If we have applicant info, show that
|
||||
if (applicantInfo) {
|
||||
return (
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<YStack
|
||||
gap="$4"
|
||||
alignItems="center"
|
||||
backgroundColor="white"
|
||||
flex={1}
|
||||
paddingHorizontal="$4"
|
||||
paddingTop="$4"
|
||||
paddingBottom={paddingBottom}
|
||||
>
|
||||
{/* Back Button */}
|
||||
<XStack width="100%" justifyContent="flex-start">
|
||||
<Button
|
||||
backgroundColor="transparent"
|
||||
borderRadius="$2"
|
||||
paddingHorizontal="$0"
|
||||
onPress={() => navigation.goBack()}
|
||||
icon={<ChevronLeft size={24} color={slate600} />}
|
||||
>
|
||||
<Text
|
||||
color={slate600}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Back
|
||||
</Text>
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
{/* Success Header */}
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={green500}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text
|
||||
fontSize="$7"
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
✓ Verification Complete
|
||||
</Text>
|
||||
<Text fontSize="$4" color={white} fontFamily={dinot} marginTop="$2">
|
||||
Your verification was successful
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
{/* Applicant Info */}
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={slate200}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
gap="$3"
|
||||
>
|
||||
<Text
|
||||
fontSize="$6"
|
||||
color={slate600}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Applicant Information
|
||||
</Text>
|
||||
|
||||
<YStack gap="$2">
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Name:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.info?.firstName || 'N/A'}{' '}
|
||||
{applicantInfo.info?.lastName || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Date of Birth:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.info?.dob || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Country:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.info?.country || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Phone:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.info?.phone || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Email:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate800}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.email || 'N/A'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Review Result:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={green500}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{applicantInfo.review.reviewAnswer}
|
||||
</Text>
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
{/* Raw JSON */}
|
||||
<YStack
|
||||
marginTop="$2"
|
||||
backgroundColor={white}
|
||||
borderRadius="$2"
|
||||
padding="$3"
|
||||
>
|
||||
<Text
|
||||
fontSize="$3"
|
||||
color={slate400}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
marginBottom="$2"
|
||||
>
|
||||
Raw Data:
|
||||
</Text>
|
||||
<Text fontSize="$2" color={slate500} fontFamily={dinot}>
|
||||
{JSON.stringify(applicantInfo, null, 2)}
|
||||
</Text>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
<Button
|
||||
backgroundColor={slate600}
|
||||
borderRadius="$2"
|
||||
height="$6"
|
||||
width="100%"
|
||||
onPress={handleReset}
|
||||
>
|
||||
<Text
|
||||
color={white}
|
||||
fontSize="$6"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Start New Verification
|
||||
</Text>
|
||||
</Button>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<YStack
|
||||
gap="$4"
|
||||
alignItems="center"
|
||||
backgroundColor="white"
|
||||
flex={1}
|
||||
paddingHorizontal="$4"
|
||||
paddingTop="$4"
|
||||
paddingBottom={paddingBottom}
|
||||
>
|
||||
{/* Back Button */}
|
||||
<XStack width="100%" justifyContent="flex-start">
|
||||
<Button
|
||||
backgroundColor="transparent"
|
||||
borderRadius="$2"
|
||||
paddingHorizontal="$0"
|
||||
onPress={() => navigation.goBack()}
|
||||
icon={<ChevronLeft size={24} color={slate600} />}
|
||||
>
|
||||
<Text
|
||||
color={slate600}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Back
|
||||
</Text>
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
{/* TEE Service Status */}
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={slate200}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
>
|
||||
<Text
|
||||
fontSize="$5"
|
||||
color={slate600}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
TEE Service
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$3"
|
||||
color={slate500}
|
||||
fontFamily={dinot}
|
||||
marginTop="$2"
|
||||
>
|
||||
{SUMSUB_TEE_URL}
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
{/* Phone Number Input */}
|
||||
<YStack width="100%" gap="$2">
|
||||
<Text
|
||||
fontSize="$4"
|
||||
color={slate600}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Phone Number
|
||||
</Text>
|
||||
<TextInput
|
||||
value={phoneNumber}
|
||||
onChangeText={setPhoneNumber}
|
||||
placeholder="+11234567890"
|
||||
keyboardType="phone-pad"
|
||||
style={{
|
||||
backgroundColor: white,
|
||||
borderWidth: 1,
|
||||
borderColor: slate200,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
fontFamily: dinot,
|
||||
color: slate800,
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
{/* Generate Token Button */}
|
||||
<Button
|
||||
backgroundColor={slate600}
|
||||
borderRadius="$2"
|
||||
height="$6"
|
||||
width="100%"
|
||||
onPress={handleFetchToken}
|
||||
disabled={loading || !phoneNumber}
|
||||
opacity={loading || !phoneNumber ? 0.5 : 1}
|
||||
>
|
||||
<Text color={white} fontSize="$6" fontFamily={dinot} fontWeight="600">
|
||||
{loading ? 'Requesting token…' : 'Generate Access Token'}
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
{/* Token Status */}
|
||||
{accessToken && (
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={green500}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
>
|
||||
<Text
|
||||
fontSize="$5"
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
✓ Access Token Generated
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot} marginTop="$2">
|
||||
User ID: {userId}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="$2"
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
marginTop="$2"
|
||||
opacity={0.8}
|
||||
>
|
||||
Token: {accessToken.substring(0, 30)}...
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
{/* Launch SDK Button */}
|
||||
{accessToken && (
|
||||
<Button
|
||||
backgroundColor={green500}
|
||||
borderRadius="$2"
|
||||
height="$6"
|
||||
width="100%"
|
||||
onPress={handleLaunchSumsub}
|
||||
disabled={sdkLaunching}
|
||||
opacity={sdkLaunching ? 0.5 : 1}
|
||||
>
|
||||
<Text
|
||||
color={white}
|
||||
fontSize="$6"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{sdkLaunching ? 'Launching…' : 'Launch Sumsub SDK'}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={red500}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
>
|
||||
<Text
|
||||
fontSize="$5"
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
Error
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot} marginTop="$2">
|
||||
{error}
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
{/* SDK Result Display */}
|
||||
{result && (
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={slate200}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
gap="$2"
|
||||
>
|
||||
<Text
|
||||
fontSize="$6"
|
||||
color={slate600}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
SDK Result
|
||||
</Text>
|
||||
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Success:{' '}
|
||||
<Text
|
||||
fontWeight="600"
|
||||
color={result.success ? green500 : red500}
|
||||
>
|
||||
{result.success ? 'Yes' : 'No'}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Status:{' '}
|
||||
<Text fontWeight="600" color={slate600}>
|
||||
{result.status}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
{result.errorType && (
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Error Type:{' '}
|
||||
<Text fontWeight="600" color={red500}>
|
||||
{result.errorType}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{result.errorMsg && (
|
||||
<Text fontSize="$4" color={slate500} fontFamily={dinot}>
|
||||
Error Message:{' '}
|
||||
<Text fontWeight="600" color={red500}>
|
||||
{result.errorMsg}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
<Text
|
||||
fontSize="$3"
|
||||
color={slate500}
|
||||
fontFamily={dinot}
|
||||
marginTop="$2"
|
||||
>
|
||||
Waiting for verification results from WebSocket...
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
{/* Instructions */}
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={yellow500}
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
gap="$2"
|
||||
>
|
||||
<Text fontSize="$5" color={white} fontFamily={dinot} fontWeight="600">
|
||||
Instructions
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
1. Make sure the TEE service is running at {SUMSUB_TEE_URL}
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
2. Enter a phone number and tap "Generate Access Token"
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
3. Tap "Launch Sumsub SDK" to start verification
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
4. Complete the verification flow
|
||||
</Text>
|
||||
<Text fontSize="$3" color={white} fontFamily={dinot}>
|
||||
5. Results will appear automatically via WebSocket
|
||||
</Text>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SumsubTestScreen;
|
||||
208
app/src/screens/dev/components/ErrorInjectionSelector.tsx
Normal file
208
app/src/screens/dev/components/ErrorInjectionSelector.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { Check, ChevronDown } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
red500,
|
||||
slate200,
|
||||
slate500,
|
||||
slate600,
|
||||
slate800,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import type { InjectedErrorType } from '@/stores/errorInjectionStore';
|
||||
import {
|
||||
ERROR_GROUPS,
|
||||
ERROR_LABELS,
|
||||
useErrorInjectionStore,
|
||||
} from '@/stores/errorInjectionStore';
|
||||
import {
|
||||
registerModalCallbacks,
|
||||
unregisterModalCallbacks,
|
||||
} from '@/utils/modalCallbackRegistry';
|
||||
|
||||
export const ErrorInjectionSelector = () => {
|
||||
const injectedErrors = useErrorInjectionStore(state => state.injectedErrors);
|
||||
const setInjectedErrors = useErrorInjectionStore(
|
||||
state => state.setInjectedErrors,
|
||||
);
|
||||
const clearAllErrors = useErrorInjectionStore(state => state.clearAllErrors);
|
||||
const [open, setOpen] = useState(false);
|
||||
const callbackIdRef = useRef<number>();
|
||||
|
||||
const handleModalDismiss = useCallback(() => {
|
||||
setOpen(false);
|
||||
if (callbackIdRef.current !== undefined) {
|
||||
unregisterModalCallbacks(callbackIdRef.current);
|
||||
callbackIdRef.current = undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openSheet = useCallback(() => {
|
||||
setOpen(true);
|
||||
const id = registerModalCallbacks({
|
||||
onButtonPress: () => {},
|
||||
onModalDismiss: handleModalDismiss,
|
||||
});
|
||||
callbackIdRef.current = id;
|
||||
}, [handleModalDismiss]);
|
||||
|
||||
const closeSheet = useCallback(() => {
|
||||
handleModalDismiss();
|
||||
}, [handleModalDismiss]);
|
||||
|
||||
const handleSheetOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
handleModalDismiss();
|
||||
} else {
|
||||
setOpen(isOpen);
|
||||
}
|
||||
},
|
||||
[handleModalDismiss],
|
||||
);
|
||||
|
||||
// Single error selection - replace instead of toggle
|
||||
const selectError = (errorType: InjectedErrorType) => {
|
||||
// If clicking the same error, clear it; otherwise set the new one
|
||||
if (injectedErrors.length === 1 && injectedErrors[0] === errorType) {
|
||||
clearAllErrors();
|
||||
} else {
|
||||
setInjectedErrors([errorType]);
|
||||
}
|
||||
// Close the sheet after selection
|
||||
closeSheet();
|
||||
};
|
||||
|
||||
const currentError = injectedErrors.length > 0 ? injectedErrors[0] : null;
|
||||
const currentErrorLabel = currentError ? ERROR_LABELS[currentError] : null;
|
||||
|
||||
return (
|
||||
<YStack gap="$2">
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={openSheet}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
{currentErrorLabel || 'Select onboarding error to test'}
|
||||
</Text>
|
||||
<ChevronDown color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
|
||||
{currentError && (
|
||||
<Button
|
||||
backgroundColor={red500}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={clearAllErrors}
|
||||
pressStyle={{
|
||||
opacity: 0.8,
|
||||
scale: 0.98,
|
||||
}}
|
||||
>
|
||||
<Text color={white} fontSize="$5" fontFamily={dinot}>
|
||||
Clear
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={handleSheetOpenChange}
|
||||
snapPoints={[85]}
|
||||
animation="medium"
|
||||
dismissOnSnapToBottom
|
||||
>
|
||||
<Sheet.Overlay />
|
||||
<Sheet.Frame
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius="$9"
|
||||
borderTopRightRadius="$9"
|
||||
>
|
||||
<YStack padding="$4">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
>
|
||||
<Text fontSize="$8" fontFamily={dinot}>
|
||||
Onboarding Error Testing
|
||||
</Text>
|
||||
<Button
|
||||
onPress={closeSheet}
|
||||
padding="$2"
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<ChevronDown
|
||||
color={slate500}
|
||||
strokeWidth={2.5}
|
||||
style={{ transform: [{ rotate: '180deg' }] }}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 100 }}
|
||||
>
|
||||
{Object.entries(ERROR_GROUPS).map(([groupName, errors]) => (
|
||||
<YStack key={groupName} marginBottom="$4">
|
||||
<Text
|
||||
fontSize="$6"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
color={slate800}
|
||||
marginBottom="$2"
|
||||
>
|
||||
{groupName}
|
||||
</Text>
|
||||
{errors.map((errorType: InjectedErrorType) => (
|
||||
<TouchableOpacity
|
||||
key={errorType}
|
||||
onPress={() => selectError(errorType)}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
|
||||
{ERROR_LABELS[errorType]}
|
||||
</Text>
|
||||
{currentError === errorType && (
|
||||
<Check color={slate600} size={20} />
|
||||
)}
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</YStack>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
175
app/src/screens/dev/components/LogLevelSelector.tsx
Normal file
175
app/src/screens/dev/components/LogLevelSelector.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { Check, ChevronDown } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
slate200,
|
||||
slate500,
|
||||
slate600,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import {
|
||||
registerModalCallbacks,
|
||||
unregisterModalCallbacks,
|
||||
} from '@/utils/modalCallbackRegistry';
|
||||
|
||||
interface LogLevelSelectorProps {
|
||||
currentLevel: string;
|
||||
onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void;
|
||||
}
|
||||
|
||||
export const LogLevelSelector: React.FC<LogLevelSelectorProps> = ({
|
||||
currentLevel,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const callbackIdRef = useRef<number>();
|
||||
|
||||
const logLevels = ['debug', 'info', 'warn', 'error'] as const;
|
||||
|
||||
// Cleanup effect to unregister callbacks on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (callbackIdRef.current !== undefined) {
|
||||
unregisterModalCallbacks(callbackIdRef.current);
|
||||
callbackIdRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleModalDismiss = useCallback(() => {
|
||||
setOpen(false);
|
||||
if (callbackIdRef.current !== undefined) {
|
||||
unregisterModalCallbacks(callbackIdRef.current);
|
||||
callbackIdRef.current = undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openSheet = useCallback(() => {
|
||||
setOpen(true);
|
||||
const id = registerModalCallbacks({
|
||||
onButtonPress: () => {},
|
||||
onModalDismiss: handleModalDismiss,
|
||||
});
|
||||
callbackIdRef.current = id;
|
||||
}, [handleModalDismiss]);
|
||||
|
||||
const closeSheet = useCallback(() => {
|
||||
handleModalDismiss();
|
||||
}, [handleModalDismiss]);
|
||||
|
||||
const handleSheetOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
handleModalDismiss();
|
||||
} else {
|
||||
setOpen(isOpen);
|
||||
}
|
||||
},
|
||||
[handleModalDismiss],
|
||||
);
|
||||
|
||||
const handleLevelSelect = useCallback(
|
||||
(level: 'debug' | 'info' | 'warn' | 'error') => {
|
||||
closeSheet();
|
||||
onSelect(level);
|
||||
},
|
||||
[closeSheet, onSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={openSheet}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
{currentLevel.toUpperCase()}
|
||||
</Text>
|
||||
<ChevronDown color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={handleSheetOpenChange}
|
||||
snapPoints={[50]}
|
||||
animation="medium"
|
||||
dismissOnSnapToBottom
|
||||
>
|
||||
<Sheet.Overlay />
|
||||
<Sheet.Frame
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius="$9"
|
||||
borderTopRightRadius="$9"
|
||||
>
|
||||
<YStack padding="$4">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
>
|
||||
<Text fontSize="$8" fontFamily={dinot}>
|
||||
Select log level
|
||||
</Text>
|
||||
<Button
|
||||
onPress={closeSheet}
|
||||
padding="$2"
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<ChevronDown
|
||||
color={slate500}
|
||||
strokeWidth={2.5}
|
||||
style={{ transform: [{ rotate: '180deg' }] }}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
{logLevels.map(level => (
|
||||
<TouchableOpacity
|
||||
key={level}
|
||||
onPress={() => handleLevelSelect(level)}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
|
||||
{level.toUpperCase()}
|
||||
</Text>
|
||||
{currentLevel === level && (
|
||||
<Check color={slate600} size={20} />
|
||||
)}
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
94
app/src/screens/dev/components/ParameterSection.tsx
Normal file
94
app/src/screens/dev/components/ParameterSection.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, { cloneElement, isValidElement } from 'react';
|
||||
import { Text, XStack, YStack } from 'tamagui';
|
||||
|
||||
import {
|
||||
slate100,
|
||||
slate200,
|
||||
slate400,
|
||||
slate600,
|
||||
slate800,
|
||||
slate900,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
interface ParameterSectionProps extends PropsWithChildren {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
darkMode?: boolean;
|
||||
}
|
||||
|
||||
export function ParameterSection({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
darkMode,
|
||||
children,
|
||||
}: ParameterSectionProps) {
|
||||
const renderIcon = () => {
|
||||
const iconElement =
|
||||
typeof icon === 'function'
|
||||
? (icon as () => React.ReactNode)()
|
||||
: isValidElement(icon)
|
||||
? icon
|
||||
: null;
|
||||
|
||||
return iconElement
|
||||
? cloneElement(iconElement as React.ReactElement, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
})
|
||||
: null;
|
||||
};
|
||||
|
||||
return (
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={darkMode ? slate900 : slate100}
|
||||
borderRadius="$4"
|
||||
borderWidth={1}
|
||||
borderColor={darkMode ? slate800 : slate200}
|
||||
padding="$4"
|
||||
flexDirection="column"
|
||||
gap="$3"
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
flexDirection="row"
|
||||
justifyContent="flex-start"
|
||||
gap="$4"
|
||||
>
|
||||
<YStack
|
||||
backgroundColor="gray"
|
||||
borderRadius={5}
|
||||
width={46}
|
||||
height={46}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
padding="$2"
|
||||
>
|
||||
{renderIcon()}
|
||||
</YStack>
|
||||
<YStack flexDirection="column" gap="$1">
|
||||
<Text
|
||||
fontSize="$5"
|
||||
color={darkMode ? white : slate600}
|
||||
fontFamily={dinot}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text fontSize="$3" color={slate400} fontFamily={dinot}>
|
||||
{description}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
{children}
|
||||
</YStack>
|
||||
);
|
||||
}
|
||||
122
app/src/screens/dev/components/ScreenSelector.tsx
Normal file
122
app/src/screens/dev/components/ScreenSelector.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { ChevronDown } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
slate200,
|
||||
slate500,
|
||||
slate600,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import { navigationScreens } from '@/navigation';
|
||||
|
||||
export const ScreenSelector = () => {
|
||||
const navigation = useNavigation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const screenList = useMemo(
|
||||
() =>
|
||||
(
|
||||
Object.keys(navigationScreens) as (keyof typeof navigationScreens)[]
|
||||
).sort(),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => setOpen(true)}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
Select screen
|
||||
</Text>
|
||||
<ChevronDown color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
snapPoints={[85]}
|
||||
animation="medium"
|
||||
dismissOnSnapToBottom
|
||||
>
|
||||
<Sheet.Overlay />
|
||||
<Sheet.Frame
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius="$9"
|
||||
borderTopRightRadius="$9"
|
||||
>
|
||||
<YStack padding="$4">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
>
|
||||
<Text fontSize="$8" fontFamily={dinot}>
|
||||
Select screen
|
||||
</Text>
|
||||
<Button
|
||||
onPress={() => setOpen(false)}
|
||||
padding="$2"
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<ChevronDown
|
||||
color={slate500}
|
||||
strokeWidth={2.5}
|
||||
style={{ transform: [{ rotate: '180deg' }] }}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 100 }}
|
||||
>
|
||||
{screenList.map(item => (
|
||||
<TouchableOpacity
|
||||
key={item}
|
||||
onPress={() => {
|
||||
setOpen(false);
|
||||
navigation.navigate(item as never);
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
>
|
||||
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
|
||||
{item}
|
||||
</Text>
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
58
app/src/screens/dev/components/TopicToggleButton.tsx
Normal file
58
app/src/screens/dev/components/TopicToggleButton.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Text } from 'tamagui';
|
||||
|
||||
import {
|
||||
slate200,
|
||||
slate400,
|
||||
slate600,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
export interface TopicToggleButtonProps {
|
||||
label: string;
|
||||
isSubscribed: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export const TopicToggleButton: React.FC<TopicToggleButtonProps> = ({
|
||||
label,
|
||||
isSubscribed,
|
||||
onToggle,
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
backgroundColor={isSubscribed ? '$green9' : slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={onToggle}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
paddingHorizontal="$4"
|
||||
pressStyle={{
|
||||
opacity: 0.8,
|
||||
scale: 0.98,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={isSubscribed ? white : slate600}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
color={isSubscribed ? white : slate400}
|
||||
fontSize="$3"
|
||||
fontFamily={dinot}
|
||||
>
|
||||
{isSubscribed ? 'Enabled' : 'Disabled'}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
10
app/src/screens/dev/components/index.ts
Normal file
10
app/src/screens/dev/components/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export type { TopicToggleButtonProps } from '@/screens/dev/components/TopicToggleButton';
|
||||
export { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector';
|
||||
export { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector';
|
||||
export { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
export { ScreenSelector } from '@/screens/dev/components/ScreenSelector';
|
||||
export { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton';
|
||||
197
app/src/screens/dev/hooks/useDangerZoneActions.ts
Normal file
197
app/src/screens/dev/hooks/useDangerZoneActions.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
import { unsafe_clearSecrets } from '@/providers/authProvider';
|
||||
import { usePassport } from '@/providers/passportDataProvider';
|
||||
import { usePointEventStore } from '@/stores/pointEventStore';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
export const useDangerZoneActions = () => {
|
||||
const { clearDocumentCatalogForMigrationTesting } = usePassport();
|
||||
const clearPointEvents = usePointEventStore(state => state.clearEvents);
|
||||
const { resetBackupForPoints } = useSettingStore();
|
||||
|
||||
const handleClearSecretsPress = () => {
|
||||
Alert.alert(
|
||||
'Delete Keychain Secrets',
|
||||
"Are you sure you want to remove your keychain secrets?\n\nIf this secret is not backed up, your account will be lost and the ID documents attached to it won't be usable.",
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await unsafe_clearSecrets();
|
||||
Alert.alert('Success', 'Keychain secrets cleared successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to clear keychain secrets:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to clear keychain secrets. Please try again.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearDocumentCatalogPress = () => {
|
||||
Alert.alert(
|
||||
'Clear Document Catalog',
|
||||
'Are you sure you want to clear the document catalog?\n\nThis will remove all documents from the new storage system but preserve legacy storage for migration testing. You will need to restart the app to test migration.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await clearDocumentCatalogForMigrationTesting();
|
||||
Alert.alert(
|
||||
'Success',
|
||||
'Document catalog cleared successfully. Please restart the app to test migration.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to clear document catalog:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to clear document catalog. Please try again.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearPointEventsPress = () => {
|
||||
Alert.alert(
|
||||
'Clear Point Events',
|
||||
'Are you sure you want to clear all point events from local storage?\n\nThis will reset your point history but not affect your actual points on the blockchain.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await clearPointEvents();
|
||||
Alert.alert('Success', 'Point events cleared successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to clear point events:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to clear point events. Please try again.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleResetBackupStatePress = () => {
|
||||
Alert.alert(
|
||||
'Reset Backup State',
|
||||
'Are you sure you want to reset the backup state?\n\nThis will allow you to see and trigger the backup points flow again.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Reset',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
resetBackupForPoints();
|
||||
Alert.alert('Success', 'Backup state reset successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearBackupEventsPress = () => {
|
||||
Alert.alert(
|
||||
'Clear Backup Events',
|
||||
'Are you sure you want to clear all backup point events from local storage?\n\nThis will remove backup events from your point history.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const events = usePointEventStore.getState().events;
|
||||
const backupEvents = events.filter(
|
||||
event => event.type === 'backup',
|
||||
);
|
||||
await Promise.all(
|
||||
backupEvents.map(event =>
|
||||
usePointEventStore.getState().removeEvent(event.id),
|
||||
),
|
||||
);
|
||||
Alert.alert('Success', 'Backup events cleared successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to clear backup events:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to clear backup events. Please try again.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
handleClearSecretsPress,
|
||||
handleClearDocumentCatalogPress,
|
||||
handleClearPointEventsPress,
|
||||
handleResetBackupStatePress,
|
||||
handleClearBackupEventsPress,
|
||||
};
|
||||
};
|
||||
156
app/src/screens/dev/hooks/useNotificationHandlers.ts
Normal file
156
app/src/screens/dev/hooks/useNotificationHandlers.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Alert, AppState } from 'react-native';
|
||||
|
||||
import {
|
||||
isNotificationSystemReady,
|
||||
requestNotificationPermission,
|
||||
subscribeToTopics,
|
||||
unsubscribeFromTopics,
|
||||
} from '@/services/notifications/notificationService';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
export const useNotificationHandlers = () => {
|
||||
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
|
||||
const [hasNotificationPermission, setHasNotificationPermission] =
|
||||
useState(false);
|
||||
|
||||
const checkPermissions = useCallback(async () => {
|
||||
const readiness = await isNotificationSystemReady();
|
||||
setHasNotificationPermission(readiness.ready);
|
||||
}, []);
|
||||
|
||||
// Check notification permissions on mount and when app regains focus
|
||||
useEffect(() => {
|
||||
checkPermissions();
|
||||
|
||||
const subscription = AppState.addEventListener('change', nextAppState => {
|
||||
if (nextAppState === 'active') {
|
||||
checkPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.remove();
|
||||
}, [checkPermissions]);
|
||||
|
||||
const handleTopicToggle = async (topics: string[], topicLabel: string) => {
|
||||
// Check permissions first
|
||||
if (!hasNotificationPermission) {
|
||||
Alert.alert(
|
||||
'Permissions Required',
|
||||
'Push notifications are not enabled. Would you like to enable them?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Enable',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const granted = await requestNotificationPermission();
|
||||
if (granted) {
|
||||
// Update permission state
|
||||
setHasNotificationPermission(true);
|
||||
Alert.alert(
|
||||
'Success',
|
||||
'Permissions granted! You can now subscribe to topics.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Failed',
|
||||
'Could not enable notifications. Please enable them in your device Settings.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to request permissions',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentlySubscribed = topics.every(topic =>
|
||||
subscribedTopics.includes(topic),
|
||||
);
|
||||
|
||||
if (isCurrentlySubscribed) {
|
||||
// Show confirmation dialog for unsubscribe
|
||||
Alert.alert(
|
||||
'Disable Notifications',
|
||||
`Are you sure you want to disable push notifications for ${topicLabel}?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Disable',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const result = await unsubscribeFromTopics(topics);
|
||||
if (result.successes.length > 0) {
|
||||
Alert.alert(
|
||||
'Success',
|
||||
`Disabled notifications for ${topicLabel}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
`Failed to disable: ${result.failures.map(f => f.error).join(', ')}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to unsubscribe',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// Subscribe without confirmation
|
||||
try {
|
||||
const result = await subscribeToTopics(topics);
|
||||
if (result.successes.length > 0) {
|
||||
Alert.alert('✅ Success', `Enabled notifications for ${topicLabel}`, [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
`Failed to enable: ${result.failures.map(f => f.error).join(', ')}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error instanceof Error ? error.message : 'Failed to subscribe',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
hasNotificationPermission,
|
||||
subscribedTopics,
|
||||
handleTopicToggle,
|
||||
};
|
||||
};
|
||||
90
app/src/screens/dev/sections/DangerZoneSection.tsx
Normal file
90
app/src/screens/dev/sections/DangerZoneSection.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Text } from 'tamagui';
|
||||
|
||||
import {
|
||||
red500,
|
||||
slate500,
|
||||
white,
|
||||
yellow500,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import WarningIcon from '@/assets/icons/warning.svg';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
|
||||
interface DangerZoneSectionProps {
|
||||
onClearSecrets: () => void;
|
||||
onClearDocumentCatalog: () => void;
|
||||
onClearPointEvents: () => void;
|
||||
onResetBackupState: () => void;
|
||||
onClearBackupEvents: () => void;
|
||||
}
|
||||
|
||||
export const DangerZoneSection: React.FC<DangerZoneSectionProps> = ({
|
||||
onClearSecrets,
|
||||
onClearDocumentCatalog,
|
||||
onClearPointEvents,
|
||||
onResetBackupState,
|
||||
onClearBackupEvents,
|
||||
}) => {
|
||||
const dangerActions = [
|
||||
{
|
||||
label: 'Delete your private key',
|
||||
onPress: onClearSecrets,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear document catalog',
|
||||
onPress: onClearDocumentCatalog,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear point events',
|
||||
onPress: onClearPointEvents,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Reset backup state',
|
||||
onPress: onResetBackupState,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear backup events',
|
||||
onPress: onClearBackupEvents,
|
||||
dangerTheme: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ParameterSection
|
||||
icon={<WarningIcon color={yellow500} />}
|
||||
title="Danger Zone"
|
||||
description="These actions are sensitive"
|
||||
darkMode={true}
|
||||
>
|
||||
{dangerActions.map(({ label, onPress, dangerTheme }) => (
|
||||
<Button
|
||||
key={label}
|
||||
style={{ backgroundColor: dangerTheme ? red500 : white }}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={onPress}
|
||||
flexDirection="row"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
<Text
|
||||
color={dangerTheme ? white : slate500}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Button>
|
||||
))}
|
||||
</ParameterSection>
|
||||
);
|
||||
};
|
||||
108
app/src/screens/dev/sections/DebugShortcutsSection.tsx
Normal file
108
app/src/screens/dev/sections/DebugShortcutsSection.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Text, XStack, YStack } from 'tamagui';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { ChevronRight } from '@tamagui/lucide-icons';
|
||||
|
||||
import { slate200, slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import BugIcon from '@/assets/icons/bug_icon.svg';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
import { ScreenSelector } from '@/screens/dev/components/ScreenSelector';
|
||||
import { IS_DEV_MODE } from '@/utils/devUtils';
|
||||
|
||||
interface DebugShortcutsSectionProps {
|
||||
navigation: NativeStackNavigationProp<RootStackParamList>;
|
||||
}
|
||||
|
||||
export const DebugShortcutsSection: React.FC<DebugShortcutsSectionProps> = ({
|
||||
navigation,
|
||||
}) => {
|
||||
return (
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Debug Shortcuts"
|
||||
description="Jump directly to any screen for testing"
|
||||
>
|
||||
<YStack gap="$2">
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('DevPrivateKey');
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
View Private Key
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('SumsubTest');
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
Sumsub Test Flow
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
{IS_DEV_MODE && (
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('Home', { testReferralFlow: true });
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
Test Referral Flow
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
)}
|
||||
<ScreenSelector />
|
||||
</YStack>
|
||||
</ParameterSection>
|
||||
);
|
||||
};
|
||||
61
app/src/screens/dev/sections/DevTogglesSection.tsx
Normal file
61
app/src/screens/dev/sections/DevTogglesSection.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Alert, Platform } from 'react-native';
|
||||
|
||||
import BugIcon from '@/assets/icons/bug_icon.svg';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton';
|
||||
|
||||
interface DevTogglesSectionProps {
|
||||
kycEnabled: boolean;
|
||||
setKycEnabled: (enabled: boolean) => void;
|
||||
useStrongBox: boolean;
|
||||
setUseStrongBox: (useStrongBox: boolean) => void;
|
||||
}
|
||||
|
||||
export const DevTogglesSection: React.FC<DevTogglesSectionProps> = ({
|
||||
kycEnabled,
|
||||
setKycEnabled,
|
||||
useStrongBox,
|
||||
setUseStrongBox,
|
||||
}) => {
|
||||
const handleToggleStrongBox = () => {
|
||||
Alert.alert(
|
||||
useStrongBox ? 'Disable StrongBox' : 'Enable StrongBox',
|
||||
useStrongBox
|
||||
? 'New keys will be generated without StrongBox hardware backing. Existing keys will continue to work.'
|
||||
: 'New keys will attempt to use StrongBox hardware backing for enhanced security.',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: useStrongBox ? 'Disable' : 'Enable',
|
||||
onPress: () => setUseStrongBox(!useStrongBox),
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Options"
|
||||
description="Development and security options"
|
||||
>
|
||||
<TopicToggleButton
|
||||
label="KYC Flow"
|
||||
isSubscribed={kycEnabled}
|
||||
onToggle={() => setKycEnabled(!kycEnabled)}
|
||||
/>
|
||||
{Platform.OS === 'android' && (
|
||||
<TopicToggleButton
|
||||
label="Use StrongBox"
|
||||
isSubscribed={useStrongBox}
|
||||
onToggle={handleToggleStrongBox}
|
||||
/>
|
||||
)}
|
||||
</ParameterSection>
|
||||
);
|
||||
};
|
||||
54
app/src/screens/dev/sections/PushNotificationsSection.tsx
Normal file
54
app/src/screens/dev/sections/PushNotificationsSection.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { YStack } from 'tamagui';
|
||||
|
||||
import BugIcon from '@/assets/icons/bug_icon.svg';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton';
|
||||
|
||||
interface PushNotificationsSectionProps {
|
||||
hasNotificationPermission: boolean;
|
||||
subscribedTopics: string[];
|
||||
onTopicToggle: (topics: string[], topicLabel: string) => void;
|
||||
}
|
||||
|
||||
export const PushNotificationsSection: React.FC<
|
||||
PushNotificationsSectionProps
|
||||
> = ({ hasNotificationPermission, subscribedTopics, onTopicToggle }) => {
|
||||
return (
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Push Notifications"
|
||||
description="Manage topic subscriptions"
|
||||
>
|
||||
<YStack gap="$2">
|
||||
<TopicToggleButton
|
||||
label="Starfall"
|
||||
isSubscribed={
|
||||
hasNotificationPermission && subscribedTopics.includes('nova')
|
||||
}
|
||||
onToggle={() => onTopicToggle(['nova'], 'Starfall')}
|
||||
/>
|
||||
<TopicToggleButton
|
||||
label="General"
|
||||
isSubscribed={
|
||||
hasNotificationPermission && subscribedTopics.includes('general')
|
||||
}
|
||||
onToggle={() => onTopicToggle(['general'], 'General')}
|
||||
/>
|
||||
<TopicToggleButton
|
||||
label="Both (Starfall + General)"
|
||||
isSubscribed={
|
||||
hasNotificationPermission &&
|
||||
subscribedTopics.includes('nova') &&
|
||||
subscribedTopics.includes('general')
|
||||
}
|
||||
onToggle={() => onTopicToggle(['nova', 'general'], 'both topics')}
|
||||
/>
|
||||
</YStack>
|
||||
</ParameterSection>
|
||||
);
|
||||
};
|
||||
8
app/src/screens/dev/sections/index.ts
Normal file
8
app/src/screens/dev/sections/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export { DangerZoneSection } from '@/screens/dev/sections/DangerZoneSection';
|
||||
export { DebugShortcutsSection } from '@/screens/dev/sections/DebugShortcutsSection';
|
||||
export { DevTogglesSection } from '@/screens/dev/sections/DevTogglesSection';
|
||||
export { PushNotificationsSection } from '@/screens/dev/sections/PushNotificationsSection';
|
||||
@@ -2,25 +2,35 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { XStack, YStack } from 'tamagui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Button, XStack, YStack } from 'tamagui';
|
||||
import type { RouteProp } from '@react-navigation/native';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { Image } from '@tamagui/lucide-icons';
|
||||
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { BodyText, PrimaryButton } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { BodyText } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
|
||||
import {
|
||||
black,
|
||||
cyan300,
|
||||
slate100,
|
||||
slate200,
|
||||
slate300,
|
||||
slate500,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks';
|
||||
import { getErrorMessages } from '@selfxyz/mobile-sdk-alpha/onboarding/import-aadhaar';
|
||||
|
||||
import WarningIcon from '@/assets/images/warning.svg';
|
||||
import { NavBar } from '@/components/navbar/BaseNavBar';
|
||||
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
|
||||
import { buttonTap } from '@/integrations/haptics';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { extraYPadding } from '@/utils/styleUtils';
|
||||
|
||||
type AadhaarUploadErrorRouteParams = {
|
||||
@@ -32,80 +42,218 @@ type AadhaarUploadErrorRoute = RouteProp<
|
||||
string
|
||||
>;
|
||||
|
||||
const getErrorMessages = (
|
||||
errorType: 'general' | 'expired',
|
||||
): { title: string; description: string } => {
|
||||
switch (errorType) {
|
||||
case 'expired':
|
||||
return {
|
||||
title: 'Your Aadhaar document has expired',
|
||||
description: 'Please upload a valid Aadhaar document',
|
||||
};
|
||||
case 'general':
|
||||
default:
|
||||
return {
|
||||
title: 'There was a problem reading the code',
|
||||
description: 'Make sure the QR code is valid and try again',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const AadhaarUploadErrorScreen: React.FC = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const paddingBottom = useSafeBottomPadding(extraYPadding + 35);
|
||||
const navigation = useNavigation();
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const route = useRoute<AadhaarUploadErrorRoute>();
|
||||
const { trackEvent } = useSelfClient();
|
||||
const errorType = route.params?.errorType || 'general';
|
||||
const kycEnabled = useSettingStore(state => state.kycEnabled);
|
||||
|
||||
const errorType = route.params?.errorType || 'general';
|
||||
const { title, description } = getErrorMessages(errorType);
|
||||
|
||||
const { launchSumsubVerification, isLoading: isRetrying } = useSumsubLauncher(
|
||||
{
|
||||
countryCode: 'IND',
|
||||
errorSource: 'mrz_scan_failed', // Use a compatible error source
|
||||
onCancel: () => {
|
||||
navigation.goBack();
|
||||
},
|
||||
onError: () => {
|
||||
// Stay on this screen - user can try again
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Success - provider handles its own success UI
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
buttonTap();
|
||||
navigation.goBack();
|
||||
}, [navigation]);
|
||||
|
||||
const handleTryAgain = useCallback(() => {
|
||||
trackEvent(AadhaarEvents.RETRY_BUTTON_PRESSED, { errorType });
|
||||
navigation.goBack();
|
||||
}, [errorType, navigation, trackEvent]);
|
||||
|
||||
const handleTryAlternative = useCallback(async () => {
|
||||
trackEvent(AadhaarEvents.HELP_BUTTON_PRESSED, { errorType });
|
||||
await launchSumsubVerification();
|
||||
}, [errorType, launchSumsubVerification, trackEvent]);
|
||||
|
||||
return (
|
||||
<YStack flex={1} backgroundColor={slate100}>
|
||||
<YStack flex={1} paddingHorizontal={20} paddingTop={20}>
|
||||
<YStack
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
{/* Header */}
|
||||
<YStack backgroundColor={slate100}>
|
||||
<NavBar.Container
|
||||
backgroundColor={slate100}
|
||||
barStyle="dark"
|
||||
paddingHorizontal="$4"
|
||||
paddingTop={insets.top + extraYPadding}
|
||||
paddingBottom={10}
|
||||
alignItems="center"
|
||||
paddingVertical={20}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<WarningIcon width={120} height={120} />
|
||||
<NavBar.LeftAction
|
||||
component="close"
|
||||
color={black}
|
||||
onPress={handleClose}
|
||||
/>
|
||||
<NavBar.Title style={{ fontFamily: dinot, fontSize: 17 }}>
|
||||
AADHAAR REGISTRATION
|
||||
</NavBar.Title>
|
||||
{/* Invisible spacer to balance header */}
|
||||
<YStack width={30} height={30} />
|
||||
</NavBar.Container>
|
||||
|
||||
{/* Progress Bar - Step 2 for Aadhaar upload */}
|
||||
<YStack paddingHorizontal={40} paddingBottom={14} paddingTop={4}>
|
||||
<XStack gap={3} height={6}>
|
||||
{[1, 2, 3, 4].map(step => (
|
||||
<YStack
|
||||
key={step}
|
||||
flex={1}
|
||||
backgroundColor={step === 2 ? cyan300 : slate300}
|
||||
borderRadius={10}
|
||||
/>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<YStack
|
||||
flex={1}
|
||||
backgroundColor={slate100}
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
>
|
||||
{/* Warning Icon */}
|
||||
<YStack flex={1} paddingHorizontal={20} paddingBottom={20}>
|
||||
<YStack flex={1} justifyContent="center" alignItems="center">
|
||||
<WarningIcon width={150} height={150} />
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Error Message and Retry Button */}
|
||||
<YStack
|
||||
paddingHorizontal={20}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
gap={20}
|
||||
borderTopWidth={1}
|
||||
borderTopColor={slate200}
|
||||
>
|
||||
<YStack alignItems="center" gap={4}>
|
||||
<BodyText
|
||||
style={{ fontSize: 18, textAlign: 'center', color: black }}
|
||||
>
|
||||
{title}
|
||||
</BodyText>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
color: slate500,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</BodyText>
|
||||
</YStack>
|
||||
|
||||
{/* Retry Button - Primary style with icon */}
|
||||
<Button
|
||||
backgroundColor={black}
|
||||
borderRadius={100}
|
||||
height={52}
|
||||
pressStyle={{ opacity: 0.8 }}
|
||||
onPress={handleTryAgain}
|
||||
disabled={isRetrying}
|
||||
>
|
||||
<XStack alignItems="center" gap={8}>
|
||||
<Image size={20} color={white} />
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: '500',
|
||||
fontFamily: dinot,
|
||||
color: white,
|
||||
}}
|
||||
>
|
||||
Try upload again
|
||||
</BodyText>
|
||||
</XStack>
|
||||
</Button>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Bottom Section */}
|
||||
<YStack
|
||||
paddingHorizontal={20}
|
||||
paddingTop={20}
|
||||
alignItems="center"
|
||||
paddingVertical={25}
|
||||
borderBlockWidth={1}
|
||||
borderBlockColor={slate200}
|
||||
>
|
||||
<BodyText style={{ fontSize: 19, textAlign: 'center', color: black }}>
|
||||
{title}
|
||||
</BodyText>
|
||||
<BodyText
|
||||
style={{
|
||||
marginTop: 6,
|
||||
fontSize: 17,
|
||||
textAlign: 'center',
|
||||
color: slate500,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</BodyText>
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
paddingHorizontal={25}
|
||||
backgroundColor={white}
|
||||
paddingBottom={paddingBottom}
|
||||
paddingTop={25}
|
||||
gap={10}
|
||||
>
|
||||
<XStack gap="$3" alignItems="stretch">
|
||||
<YStack flex={1}>
|
||||
<PrimaryButton
|
||||
onPress={() => {
|
||||
trackEvent(AadhaarEvents.RETRY_BUTTON_PRESSED, { errorType });
|
||||
// Navigate back to upload screen to try again
|
||||
navigation.goBack();
|
||||
{kycEnabled && (
|
||||
<>
|
||||
{/* Secondary Button - White fill, black text, rounded */}
|
||||
<Button
|
||||
backgroundColor={white}
|
||||
borderWidth={1}
|
||||
borderColor={slate200}
|
||||
borderRadius={100}
|
||||
height={52}
|
||||
pressStyle={{ opacity: 0.8 }}
|
||||
onPress={handleTryAlternative}
|
||||
disabled={isRetrying}
|
||||
>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: '500',
|
||||
fontFamily: dinot,
|
||||
color: black,
|
||||
}}
|
||||
>
|
||||
{isRetrying ? 'Loading...' : 'Try a different method'}
|
||||
</BodyText>
|
||||
</Button>
|
||||
|
||||
{/* Footer Text - Not italic */}
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
color: slate500,
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</PrimaryButton>
|
||||
</YStack>
|
||||
{/* <YStack flex={1}>
|
||||
<SecondaryButton
|
||||
onPress={() => {
|
||||
trackEvent(AadhaarEvents.HELP_BUTTON_PRESSED, { errorType });
|
||||
// TODO: Implement help functionality
|
||||
}}
|
||||
>
|
||||
Need Help?
|
||||
</SecondaryButton>
|
||||
</YStack> */}
|
||||
</XStack>
|
||||
Registering with alternative methods may take longer to verify
|
||||
your document.
|
||||
</BodyText>
|
||||
</>
|
||||
)}
|
||||
</YStack>
|
||||
</YStack>
|
||||
);
|
||||
|
||||
@@ -109,7 +109,8 @@ const PassportDataSelector = () => {
|
||||
try {
|
||||
await setSelectedDocument(documentId);
|
||||
navigation.navigate('ConfirmBelonging', {});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('Failed to navigate to registration:', error);
|
||||
Alert.alert(
|
||||
'Registration Error',
|
||||
'Failed to prepare document for registration. Please try again.',
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { View, XStack, YStack } from 'tamagui';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { useIsFocused, useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import {
|
||||
DelayedLottieView,
|
||||
@@ -33,21 +34,45 @@ import {
|
||||
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';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { getDocumentScanPrompt } from '@/utils/documentAttributes';
|
||||
|
||||
const DocumentCameraScreen: React.FC = () => {
|
||||
const isFocused = useIsFocused();
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const selfClient = useSelfClient();
|
||||
const selectedDocumentType = selfClient.useMRZStore(
|
||||
state => state.documentType,
|
||||
);
|
||||
const countryCode = selfClient.useMRZStore(state => state.countryCode);
|
||||
const { shouldInjectError } = useErrorInjection();
|
||||
|
||||
// Add a ref to track when the camera screen is mounted
|
||||
const scanStartTimeRef = useRef(Date.now());
|
||||
const { onPassportRead } = useReadMRZ(scanStartTimeRef);
|
||||
|
||||
// Dev-only: Auto-trigger MRZ error after short delay if error injection is enabled
|
||||
useEffect(() => {
|
||||
if (
|
||||
shouldInjectError('mrz_invalid_format') ||
|
||||
shouldInjectError('mrz_unknown_error')
|
||||
) {
|
||||
const timer = setTimeout(() => {
|
||||
console.log(
|
||||
'[DEV] Injecting MRZ error - navigating to fallback screen',
|
||||
);
|
||||
navigation.navigate('RegistrationFallbackMRZ', {
|
||||
countryCode: countryCode || '',
|
||||
});
|
||||
}, 1500); // 1.5 second delay to show camera briefly
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [shouldInjectError, navigation, countryCode]);
|
||||
|
||||
const scanPrompt = getDocumentScanPrompt(selectedDocumentType);
|
||||
|
||||
const navigateToHome = useHapticNavigation('Home', {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { YStack } from 'tamagui';
|
||||
|
||||
import { Caption } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { Caption, SecondaryButton } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { slate500, slate700 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import Activity from '@/assets/icons/activity.svg';
|
||||
import PassportCameraBulb from '@/assets/icons/passport_camera_bulb.svg';
|
||||
@@ -15,8 +17,10 @@ import Star from '@/assets/icons/star.svg';
|
||||
import type { TipProps } from '@/components/Tips';
|
||||
import Tips from '@/components/Tips';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
|
||||
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
|
||||
import { flush as flushAnalytics } from '@/services/analytics';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
const tips: TipProps[] = [
|
||||
{
|
||||
@@ -48,6 +52,14 @@ const tips: TipProps[] = [
|
||||
|
||||
const DocumentCameraTroubleScreen: React.FC = () => {
|
||||
const go = useHapticNavigation('DocumentCamera', { action: 'cancel' });
|
||||
const selfClient = useSelfClient();
|
||||
const { useMRZStore } = selfClient;
|
||||
const { countryCode } = useMRZStore();
|
||||
const kycEnabled = useSettingStore(state => state.kycEnabled);
|
||||
const { launchSumsubVerification, isLoading } = useSumsubLauncher({
|
||||
countryCode,
|
||||
errorSource: 'mrz_scan_failed',
|
||||
});
|
||||
|
||||
// error screen, flush analytics
|
||||
useEffect(() => {
|
||||
@@ -64,10 +76,32 @@ const DocumentCameraTroubleScreen: React.FC = () => {
|
||||
</Caption>
|
||||
}
|
||||
footer={
|
||||
<Caption size="large" style={{ color: slate500 }}>
|
||||
Following these steps should help your phone's camera capture the ID
|
||||
page quickly and clearly!
|
||||
</Caption>
|
||||
<YStack gap="$3">
|
||||
<Caption size="large" style={{ color: slate500 }}>
|
||||
Following these steps should help your phone's camera capture the ID
|
||||
page quickly and clearly!
|
||||
</Caption>
|
||||
|
||||
{kycEnabled && (
|
||||
<>
|
||||
<Caption
|
||||
size="large"
|
||||
style={{ color: slate500, marginTop: 12, marginBottom: 8 }}
|
||||
>
|
||||
Or try an alternative verification method:
|
||||
</Caption>
|
||||
|
||||
<SecondaryButton
|
||||
onPress={launchSumsubVerification}
|
||||
disabled={isLoading}
|
||||
textColor={slate700}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
|
||||
</SecondaryButton>
|
||||
</>
|
||||
)}
|
||||
</YStack>
|
||||
}
|
||||
>
|
||||
<Tips items={tips} />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Platform, ScrollView } from 'react-native';
|
||||
import { Input, YStack } from 'tamagui';
|
||||
import { Input, Switch, XStack, YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
@@ -21,6 +21,7 @@ import { white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
type NFCParams = {
|
||||
skipPACE?: boolean;
|
||||
@@ -111,6 +112,10 @@ const DocumentNFCMethodSelectionScreen: React.FC = () => {
|
||||
const { useMRZStore } = selfClient;
|
||||
const { update, passportNumber, dateOfBirth, dateOfExpiry } = useMRZStore();
|
||||
|
||||
const loggingSeverity = useSettingStore(state => state.loggingSeverity);
|
||||
const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity);
|
||||
const isDebugMode = loggingSeverity === 'debug';
|
||||
|
||||
const handleSelect = (key: string) => {
|
||||
setSelectedMethod(key);
|
||||
setError('');
|
||||
@@ -153,6 +158,30 @@ 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>Debug Logging</Description>
|
||||
<Switch
|
||||
size="$4"
|
||||
checked={isDebugMode}
|
||||
onCheckedChange={checked => {
|
||||
setLoggingSeverity(checked ? 'debug' : 'warn');
|
||||
}}
|
||||
backgroundColor={isDebugMode ? '$green7Light' : '$gray4'}
|
||||
style={{ minWidth: 48, minHeight: 36 }}
|
||||
>
|
||||
<Switch.Thumb animation="quick" backgroundColor="$white" />
|
||||
</Switch>
|
||||
</XStack>
|
||||
|
||||
{NFC_METHODS.filter(method =>
|
||||
method.platform.includes(Platform.OS),
|
||||
).map(method => (
|
||||
|
||||
@@ -54,6 +54,7 @@ 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';
|
||||
import { useFeedbackAutoHide } from '@/hooks/useFeedbackAutoHide';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import {
|
||||
@@ -73,7 +74,11 @@ import {
|
||||
setNfcScanningActive,
|
||||
trackNfcEvent,
|
||||
} from '@/services/analytics';
|
||||
import { sendFeedbackEmail } from '@/services/email';
|
||||
import {
|
||||
openSupportForm,
|
||||
SUPPORT_FORM_BUTTON_TEXT,
|
||||
SUPPORT_FORM_MESSAGE,
|
||||
} from '@/services/support';
|
||||
|
||||
const emitter =
|
||||
Platform.OS === 'android'
|
||||
@@ -102,8 +107,9 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const route = useRoute<DocumentNFCScanRoute>();
|
||||
const { showModal } = useFeedback();
|
||||
useFeedback();
|
||||
useFeedbackAutoHide();
|
||||
const { shouldInjectError } = useErrorInjection();
|
||||
const {
|
||||
passportNumber,
|
||||
dateOfBirth,
|
||||
@@ -170,10 +176,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
});
|
||||
|
||||
const onReportIssue = useCallback(() => {
|
||||
sendFeedbackEmail({
|
||||
message: 'User reported an issue from NFC scan screen',
|
||||
origin: 'passport/nfc',
|
||||
});
|
||||
openSupportForm();
|
||||
}, []);
|
||||
|
||||
const openErrorModal = useCallback(
|
||||
@@ -188,22 +191,11 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
},
|
||||
{ message: sanitizeErrorMessage(message) },
|
||||
);
|
||||
showModal({
|
||||
titleText: 'NFC Scan Error',
|
||||
bodyText: message,
|
||||
buttonText: 'Report Issue',
|
||||
secondaryButtonText: 'Help',
|
||||
preventDismiss: false,
|
||||
onButtonPress: () =>
|
||||
sendFeedbackEmail({
|
||||
message: sanitizeErrorMessage(message),
|
||||
origin: 'passport/nfc',
|
||||
}),
|
||||
onSecondaryButtonPress: goToNFCTrouble,
|
||||
onModalDismiss: () => {},
|
||||
navigation.navigate('RegistrationFallbackNFC', {
|
||||
countryCode,
|
||||
});
|
||||
},
|
||||
[baseContext, showModal, goToNFCTrouble],
|
||||
[baseContext, navigation, countryCode],
|
||||
);
|
||||
|
||||
const checkNfcSupport = useCallback(async () => {
|
||||
@@ -327,6 +319,18 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
}, 30000);
|
||||
|
||||
try {
|
||||
// Dev-only: Check for injected timeout error
|
||||
if (shouldInjectError('nfc_timeout')) {
|
||||
console.log('[DEV] Injecting NFC timeout error');
|
||||
throw new Error('Injected timeout error for testing');
|
||||
}
|
||||
|
||||
// Dev-only: Check for injected module unavailable error
|
||||
if (shouldInjectError('nfc_module_unavailable')) {
|
||||
console.log('[DEV] Injecting NFC module unavailable error');
|
||||
throw new Error('NFC scanning is currently unavailable');
|
||||
}
|
||||
|
||||
const {
|
||||
canNumber,
|
||||
useCan,
|
||||
@@ -379,6 +383,12 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
);
|
||||
let passportData: PassportData | null = null;
|
||||
try {
|
||||
// Dev-only: Check for injected parse failure error
|
||||
if (shouldInjectError('nfc_parse_failure')) {
|
||||
console.log('[DEV] Injecting NFC parse failure error');
|
||||
throw new Error('Failed to parse NFC response');
|
||||
}
|
||||
|
||||
passportData = parseScanResponse(scanResponse);
|
||||
} catch (e: unknown) {
|
||||
console.error('Parsing NFC Response Unsuccessful');
|
||||
@@ -426,7 +436,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
});
|
||||
openErrorModal(message);
|
||||
// We deliberately avoid opening any external feedback widgets here;
|
||||
// users can send feedback via the email action in the modal.
|
||||
// users can request support via the support form action in the modal.
|
||||
} finally {
|
||||
if (scanTimeoutRef.current) {
|
||||
clearTimeout(scanTimeoutRef.current);
|
||||
@@ -455,6 +465,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
navigation,
|
||||
openErrorModal,
|
||||
trackEvent,
|
||||
shouldInjectError,
|
||||
]);
|
||||
|
||||
const navigateToHome = useHapticNavigation('Home', {
|
||||
@@ -612,6 +623,9 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
</BodyText>
|
||||
</>
|
||||
)}
|
||||
<BodyText style={[styles.disclaimer, { marginTop: 12 }]}>
|
||||
{SUPPORT_FORM_MESSAGE}
|
||||
</BodyText>
|
||||
</TextsContainer>
|
||||
<ButtonsContainer>
|
||||
<PrimaryButton
|
||||
@@ -634,7 +648,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton onPress={onReportIssue}>
|
||||
Report Issue
|
||||
{SUPPORT_FORM_BUTTON_TEXT}
|
||||
</SecondaryButton>
|
||||
</ButtonsContainer>
|
||||
</>
|
||||
|
||||
@@ -2,21 +2,26 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import { YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { Caption, SecondaryButton } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { slate500, slate700 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import type { TipProps } from '@/components/Tips';
|
||||
import Tips from '@/components/Tips';
|
||||
import { useFeedbackAutoHide } from '@/hooks/useFeedbackAutoHide';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
|
||||
import { selectionChange } from '@/integrations/haptics';
|
||||
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
|
||||
import { flushAllAnalytics } from '@/services/analytics';
|
||||
import { sendFeedbackEmail } from '@/services/email';
|
||||
import { openSupportForm, SUPPORT_FORM_BUTTON_TEXT } from '@/services/support';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
const tips: TipProps[] = [
|
||||
{
|
||||
@@ -46,10 +51,22 @@ const tips: TipProps[] = [
|
||||
];
|
||||
|
||||
const DocumentNFCTroubleScreen: React.FC = () => {
|
||||
const go = useHapticNavigation('DocumentNFCScan', { action: 'cancel' });
|
||||
const navigation = useNavigation();
|
||||
const handleDismiss = useCallback(() => {
|
||||
selectionChange();
|
||||
navigation.goBack();
|
||||
}, [navigation]);
|
||||
const goToNFCMethodSelection = useHapticNavigation(
|
||||
'DocumentNFCMethodSelection',
|
||||
);
|
||||
const selfClient = useSelfClient();
|
||||
const { useMRZStore } = selfClient;
|
||||
const { countryCode } = useMRZStore();
|
||||
const kycEnabled = useSettingStore(state => state.kycEnabled);
|
||||
const { launchSumsubVerification, isLoading } = useSumsubLauncher({
|
||||
countryCode,
|
||||
errorSource: 'nfc_scan_failed',
|
||||
});
|
||||
useFeedbackAutoHide();
|
||||
|
||||
// error screen, flush analytics
|
||||
@@ -67,23 +84,29 @@ const DocumentNFCTroubleScreen: React.FC = () => {
|
||||
return (
|
||||
<SimpleScrolledTitleLayout
|
||||
title="Having trouble verifying your ID?"
|
||||
onDismiss={go}
|
||||
onDismiss={handleDismiss}
|
||||
secondaryButtonText="Open NFC Options"
|
||||
onSecondaryButtonPress={goToNFCMethodSelection}
|
||||
footer={
|
||||
// Add top padding before buttons and normalize spacing
|
||||
<YStack marginTop={16} marginBottom={0} gap={10}>
|
||||
<YStack gap="$3">
|
||||
<SecondaryButton
|
||||
onPress={() =>
|
||||
sendFeedbackEmail({
|
||||
message: 'User reported an issue from NFC trouble screen',
|
||||
origin: 'passport/nfc-trouble',
|
||||
})
|
||||
}
|
||||
onPress={openSupportForm}
|
||||
textColor={slate700}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
Report Issue
|
||||
{SUPPORT_FORM_BUTTON_TEXT}
|
||||
</SecondaryButton>
|
||||
|
||||
{kycEnabled && (
|
||||
<SecondaryButton
|
||||
onPress={launchSumsubVerification}
|
||||
disabled={isLoading}
|
||||
textColor={slate700}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
|
||||
</SecondaryButton>
|
||||
)}
|
||||
</YStack>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Button, XStack, YStack } from 'tamagui';
|
||||
import type { RouteProp } from '@react-navigation/native';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { BodyText } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import {
|
||||
black,
|
||||
cyan300,
|
||||
slate100,
|
||||
slate200,
|
||||
slate300,
|
||||
slate500,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks';
|
||||
|
||||
import WarningIcon from '@/assets/images/warning.svg';
|
||||
import { NavBar } from '@/components/navbar/BaseNavBar';
|
||||
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
|
||||
import { buttonTap } from '@/integrations/haptics';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { extraYPadding } from '@/utils/styleUtils';
|
||||
|
||||
type RegistrationFallbackMRZRouteParams = {
|
||||
countryCode: string;
|
||||
};
|
||||
|
||||
type RegistrationFallbackMRZRoute = RouteProp<
|
||||
Record<string, RegistrationFallbackMRZRouteParams>,
|
||||
string
|
||||
>;
|
||||
|
||||
const getHeaderTitle = (documentType: string): string => {
|
||||
switch (documentType) {
|
||||
case 'p':
|
||||
return 'PASSPORT REGISTRATION';
|
||||
case 'i':
|
||||
return 'ID CARD REGISTRATION';
|
||||
default:
|
||||
return 'DOCUMENT REGISTRATION';
|
||||
}
|
||||
};
|
||||
|
||||
const RegistrationFallbackMRZScreen: React.FC = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const paddingBottom = useSafeBottomPadding(extraYPadding + 35);
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const route = useRoute<RegistrationFallbackMRZRoute>();
|
||||
const selfClient = useSelfClient();
|
||||
const { trackEvent, useMRZStore } = selfClient;
|
||||
const storeCountryCode = useMRZStore(state => state.countryCode);
|
||||
const documentType = useMRZStore(state => state.documentType);
|
||||
const kycEnabled = useSettingStore(state => state.kycEnabled);
|
||||
|
||||
// Use country code from route params, or fall back to MRZ store
|
||||
const countryCode = route.params?.countryCode || storeCountryCode || '';
|
||||
|
||||
const headerTitle = getHeaderTitle(documentType);
|
||||
|
||||
const { launchSumsubVerification, isLoading: isRetrying } = useSumsubLauncher(
|
||||
{
|
||||
countryCode,
|
||||
errorSource: 'mrz_scan_failed',
|
||||
onCancel: () => {
|
||||
navigation.goBack();
|
||||
},
|
||||
onError: (_error, _result) => {
|
||||
// Stay on this screen - user can try again
|
||||
// Error is already logged in the hook
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Success - provider handles its own success UI
|
||||
// The screen will be navigated away by the provider's flow
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
buttonTap();
|
||||
navigation.goBack();
|
||||
}, [navigation]);
|
||||
|
||||
const handleTryAlternative = useCallback(async () => {
|
||||
trackEvent('REGISTRATION_FALLBACK_TRY_ALTERNATIVE', {
|
||||
errorSource: 'mrz_scan_failed',
|
||||
});
|
||||
await launchSumsubVerification();
|
||||
}, [launchSumsubVerification, trackEvent]);
|
||||
|
||||
const handleRetryOriginal = useCallback(() => {
|
||||
trackEvent('REGISTRATION_FALLBACK_RETRY_ORIGINAL', {
|
||||
errorSource: 'mrz_scan_failed',
|
||||
});
|
||||
navigation.navigate('DocumentCamera');
|
||||
}, [navigation, trackEvent]);
|
||||
|
||||
return (
|
||||
<YStack flex={1} backgroundColor={slate100}>
|
||||
{/* Header */}
|
||||
<YStack backgroundColor={slate100}>
|
||||
<NavBar.Container
|
||||
backgroundColor={slate100}
|
||||
barStyle="dark"
|
||||
paddingHorizontal="$4"
|
||||
paddingTop={insets.top + extraYPadding}
|
||||
paddingBottom={10}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<NavBar.LeftAction
|
||||
component="close"
|
||||
color={black}
|
||||
onPress={handleClose}
|
||||
/>
|
||||
<NavBar.Title style={{ fontFamily: dinot, fontSize: 17 }}>
|
||||
{headerTitle}
|
||||
</NavBar.Title>
|
||||
{/* Invisible spacer to balance header */}
|
||||
<YStack width={30} height={30} />
|
||||
</NavBar.Container>
|
||||
|
||||
{/* Progress Bar - Step 2 for MRZ */}
|
||||
<YStack paddingHorizontal={40} paddingBottom={14} paddingTop={4}>
|
||||
<XStack gap={3} height={6}>
|
||||
{[1, 2, 3, 4].map(step => (
|
||||
<YStack
|
||||
key={step}
|
||||
flex={1}
|
||||
backgroundColor={step === 2 ? cyan300 : slate300}
|
||||
borderRadius={10}
|
||||
/>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<YStack
|
||||
flex={1}
|
||||
backgroundColor={slate100}
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
>
|
||||
{/* Warning Icon */}
|
||||
<YStack flex={1} paddingHorizontal={20} paddingBottom={20}>
|
||||
<YStack flex={1} justifyContent="center" alignItems="center">
|
||||
<WarningIcon width={150} height={150} />
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Error Message and Retry Button */}
|
||||
<YStack
|
||||
paddingHorizontal={20}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
gap={20}
|
||||
borderTopWidth={1}
|
||||
borderTopColor={slate200}
|
||||
>
|
||||
<YStack alignItems="center" gap={4}>
|
||||
<BodyText
|
||||
style={{ fontSize: 18, textAlign: 'center', color: black }}
|
||||
>
|
||||
We couldn't read your document's MRZ
|
||||
</BodyText>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
color: slate500,
|
||||
}}
|
||||
>
|
||||
Make sure the machine-readable zone at the bottom is clearly
|
||||
visible and try again
|
||||
</BodyText>
|
||||
</YStack>
|
||||
|
||||
{/* Retry Button - Primary style with very rounded corners */}
|
||||
<Button
|
||||
backgroundColor={black}
|
||||
borderRadius={100}
|
||||
height={52}
|
||||
pressStyle={{ opacity: 0.8 }}
|
||||
onPress={handleRetryOriginal}
|
||||
disabled={isRetrying}
|
||||
>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: '500',
|
||||
fontFamily: dinot,
|
||||
color: white,
|
||||
}}
|
||||
>
|
||||
Try scanning again
|
||||
</BodyText>
|
||||
</Button>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Bottom Section */}
|
||||
<YStack
|
||||
paddingHorizontal={20}
|
||||
paddingTop={20}
|
||||
paddingBottom={paddingBottom}
|
||||
gap={10}
|
||||
>
|
||||
{kycEnabled && (
|
||||
<>
|
||||
{/* Secondary Button - White fill, black text, rounded */}
|
||||
<Button
|
||||
backgroundColor={white}
|
||||
borderWidth={1}
|
||||
borderColor={slate200}
|
||||
borderRadius={100}
|
||||
height={52}
|
||||
pressStyle={{ opacity: 0.8 }}
|
||||
onPress={handleTryAlternative}
|
||||
disabled={isRetrying}
|
||||
>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: '500',
|
||||
fontFamily: dinot,
|
||||
color: black,
|
||||
}}
|
||||
>
|
||||
{isRetrying ? 'Loading...' : 'Try a different method'}
|
||||
</BodyText>
|
||||
</Button>
|
||||
|
||||
{/* Footer Text - Not italic */}
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
color: slate500,
|
||||
}}
|
||||
>
|
||||
Registering with alternative methods may take longer to verify
|
||||
your document.
|
||||
</BodyText>
|
||||
</>
|
||||
)}
|
||||
</YStack>
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegistrationFallbackMRZScreen;
|
||||
@@ -0,0 +1,288 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Button, XStack, YStack } from 'tamagui';
|
||||
import type { RouteProp } from '@react-navigation/native';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { BodyText } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import {
|
||||
black,
|
||||
blue600,
|
||||
cyan300,
|
||||
slate100,
|
||||
slate200,
|
||||
slate300,
|
||||
slate500,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks';
|
||||
|
||||
import WarningIcon from '@/assets/images/warning.svg';
|
||||
import { NavBar } from '@/components/navbar/BaseNavBar';
|
||||
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
|
||||
import { buttonTap } from '@/integrations/haptics';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { extraYPadding } from '@/utils/styleUtils';
|
||||
|
||||
type RegistrationFallbackNFCRouteParams = {
|
||||
countryCode: string;
|
||||
};
|
||||
|
||||
type RegistrationFallbackNFCRoute = RouteProp<
|
||||
Record<string, RegistrationFallbackNFCRouteParams>,
|
||||
string
|
||||
>;
|
||||
|
||||
const getHeaderTitle = (documentType: string): string => {
|
||||
switch (documentType) {
|
||||
case 'p':
|
||||
return 'PASSPORT REGISTRATION';
|
||||
case 'i':
|
||||
return 'ID CARD REGISTRATION';
|
||||
default:
|
||||
return 'DOCUMENT REGISTRATION';
|
||||
}
|
||||
};
|
||||
|
||||
const RegistrationFallbackNFCScreen: React.FC = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const paddingBottom = useSafeBottomPadding(extraYPadding + 35);
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const route = useRoute<RegistrationFallbackNFCRoute>();
|
||||
const selfClient = useSelfClient();
|
||||
const { trackEvent, useMRZStore } = selfClient;
|
||||
const storeCountryCode = useMRZStore(state => state.countryCode);
|
||||
const documentType = useMRZStore(state => state.documentType);
|
||||
const kycEnabled = useSettingStore(state => state.kycEnabled);
|
||||
|
||||
// Use country code from route params, or fall back to MRZ store
|
||||
const countryCode = route.params?.countryCode || storeCountryCode || '';
|
||||
|
||||
const headerTitle = getHeaderTitle(documentType);
|
||||
|
||||
const { launchSumsubVerification, isLoading: isRetrying } = useSumsubLauncher(
|
||||
{
|
||||
countryCode,
|
||||
errorSource: 'nfc_scan_failed',
|
||||
onCancel: () => {
|
||||
navigation.goBack();
|
||||
},
|
||||
onError: (_error, _result) => {
|
||||
// Stay on this screen - user can try again
|
||||
// Error is already logged in the hook
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Success - provider handles its own success UI
|
||||
// The screen will be navigated away by the provider's flow
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
buttonTap();
|
||||
navigation.goBack();
|
||||
}, [navigation]);
|
||||
|
||||
const handleHelp = useCallback(() => {
|
||||
buttonTap();
|
||||
navigation.navigate('DocumentNFCTrouble');
|
||||
}, [navigation]);
|
||||
|
||||
const handleTryAlternative = useCallback(async () => {
|
||||
trackEvent('REGISTRATION_FALLBACK_TRY_ALTERNATIVE', {
|
||||
errorSource: 'nfc_scan_failed',
|
||||
});
|
||||
await launchSumsubVerification();
|
||||
}, [launchSumsubVerification, trackEvent]);
|
||||
|
||||
const handleRetryOriginal = useCallback(() => {
|
||||
trackEvent('REGISTRATION_FALLBACK_RETRY_ORIGINAL', {
|
||||
errorSource: 'nfc_scan_failed',
|
||||
});
|
||||
navigation.navigate('DocumentNFCScan', {});
|
||||
}, [navigation, trackEvent]);
|
||||
|
||||
return (
|
||||
<YStack flex={1} backgroundColor={slate100}>
|
||||
{/* Header */}
|
||||
<YStack backgroundColor={slate100}>
|
||||
<NavBar.Container
|
||||
backgroundColor={slate100}
|
||||
barStyle="dark"
|
||||
paddingHorizontal="$4"
|
||||
paddingTop={insets.top + extraYPadding}
|
||||
paddingBottom={10}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<NavBar.LeftAction
|
||||
component="close"
|
||||
color={black}
|
||||
onPress={handleClose}
|
||||
/>
|
||||
<NavBar.Title style={{ fontFamily: dinot, fontSize: 17 }}>
|
||||
{headerTitle}
|
||||
</NavBar.Title>
|
||||
<Button unstyled onPress={handleHelp} aria-label="Help" hitSlop={8}>
|
||||
<YStack
|
||||
width={26}
|
||||
height={26}
|
||||
borderRadius={13}
|
||||
backgroundColor={blue600}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<BodyText
|
||||
style={{
|
||||
color: white,
|
||||
fontSize: 16,
|
||||
fontWeight: '900',
|
||||
lineHeight: 18,
|
||||
textAlign: 'center',
|
||||
includeFontPadding: false,
|
||||
}}
|
||||
>
|
||||
?
|
||||
</BodyText>
|
||||
</YStack>
|
||||
</Button>
|
||||
</NavBar.Container>
|
||||
|
||||
{/* Progress Bar - Step 3 for NFC */}
|
||||
<YStack paddingHorizontal={40} paddingBottom={14} paddingTop={4}>
|
||||
<XStack gap={3} height={6}>
|
||||
{[1, 2, 3, 4].map(step => (
|
||||
<YStack
|
||||
key={step}
|
||||
flex={1}
|
||||
backgroundColor={step === 3 ? cyan300 : slate300}
|
||||
borderRadius={10}
|
||||
/>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<YStack
|
||||
flex={1}
|
||||
backgroundColor={slate100}
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
>
|
||||
{/* Warning Icon */}
|
||||
<YStack flex={1} paddingHorizontal={20} paddingBottom={20}>
|
||||
<YStack flex={1} justifyContent="center" alignItems="center">
|
||||
<WarningIcon width={150} height={150} />
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Error Message and Retry Button */}
|
||||
<YStack
|
||||
paddingHorizontal={20}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
gap={20}
|
||||
borderTopWidth={1}
|
||||
borderTopColor={slate200}
|
||||
>
|
||||
<YStack alignItems="center" gap={4}>
|
||||
<BodyText
|
||||
style={{ fontSize: 18, textAlign: 'center', color: black }}
|
||||
>
|
||||
There was a problem reading the chip
|
||||
</BodyText>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
color: slate500,
|
||||
}}
|
||||
>
|
||||
Make sure NFC is enabled and try again
|
||||
</BodyText>
|
||||
</YStack>
|
||||
|
||||
{/* Retry Button - Primary style with very rounded corners */}
|
||||
<Button
|
||||
backgroundColor={black}
|
||||
borderRadius={100}
|
||||
height={52}
|
||||
pressStyle={{ opacity: 0.8 }}
|
||||
onPress={handleRetryOriginal}
|
||||
disabled={isRetrying}
|
||||
>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: '500',
|
||||
fontFamily: dinot,
|
||||
color: white,
|
||||
}}
|
||||
>
|
||||
Try reading again
|
||||
</BodyText>
|
||||
</Button>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Bottom Section */}
|
||||
<YStack
|
||||
paddingHorizontal={20}
|
||||
paddingTop={20}
|
||||
paddingBottom={paddingBottom}
|
||||
gap={10}
|
||||
>
|
||||
{kycEnabled && (
|
||||
<>
|
||||
{/* Secondary Button - White fill, black text, rounded */}
|
||||
<Button
|
||||
backgroundColor={white}
|
||||
borderWidth={1}
|
||||
borderColor={slate200}
|
||||
borderRadius={100}
|
||||
height={52}
|
||||
pressStyle={{ opacity: 0.8 }}
|
||||
onPress={handleTryAlternative}
|
||||
disabled={isRetrying}
|
||||
>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: '500',
|
||||
fontFamily: dinot,
|
||||
color: black,
|
||||
}}
|
||||
>
|
||||
{isRetrying ? 'Loading...' : 'Try a different method'}
|
||||
</BodyText>
|
||||
</Button>
|
||||
|
||||
{/* Footer Text - Not italic */}
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
color: slate500,
|
||||
}}
|
||||
>
|
||||
Registering with alternative methods may take longer to verify
|
||||
your document.
|
||||
</BodyText>
|
||||
</>
|
||||
)}
|
||||
</YStack>
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegistrationFallbackNFCScreen;
|
||||
@@ -5,7 +5,6 @@
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
@@ -58,7 +57,6 @@ const DocumentOnboardingScreen: React.FC = () => {
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<SystemBars style="light" />
|
||||
<ExpandableBottomLayout.TopSection roundTop backgroundColor={black}>
|
||||
<LottieView
|
||||
ref={animationRef}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user