Merge pull request #1323 from selfxyz/staging

Release to Production - 2025-10-26
This commit is contained in:
Justin Hernandez
2025-10-26 15:19:09 -07:00
committed by GitHub
698 changed files with 230721 additions and 5421 deletions

View File

@@ -36,9 +36,57 @@ paths-ignore:
- "**/generated/**/*.crt"
- "**/generated/**/*.pem"
# iOS frameworks and build artifacts
- "**/*.xcframework"
- "**/*.xcframework/**"
- "**/*.swiftinterface"
- "**/NFCPassportReader.xcframework/**"
- "**/OpenSSL.xcframework/**"
- "**/SelfSDK.xcframework/**"
- "**/packages/mobile-sdk-alpha/ios/Frameworks/**"
- "**/packages/mobile-sdk-alpha/ios/SelfSDK/**"
# Ignore specific secret types for mock files
secrets-ignore:
- "Generic Private Key" # For mock certificate keys
- "Generic Certificate" # For mock certificates
- "RSA Private Key" # For mock RSA keys
- "EC Private Key" # For mock EC keys
secret:
ignored_matches:
- match: 2036b4e50ad3042969b290e354d9864465107a14de6f5a36d49f81ea8290def8
name: prebuilt-ios-arm64-apple-ios.private.swiftinterface
ignored_paths:
- '**/*.swiftinterface'
- '**/*.xcframework/**'
- '**/packages/mobile-sdk-alpha/ios/Frameworks/**'
- '**/OpenSSL.xcframework/**'
- '**/demo-app/**/mock/**'
- common/src/mock_certificates/aadhaar/mockAadhaarCert.ts
- '**/NFCPassportReader.xcframework/**'
- common/src/utils/passports/genMockIdDoc.ts
- '**/tests/**/*.crt'
- '**/mock_certificates/**/*.crt'
- '**/mock_certificates/**/*.key'
- '**/demo-app/**/test-data/**'
- '**/generated/**/*.key'
- '**/SelfSDK.xcframework/**'
- '**/mock/**/*.crt'
- '**/generated/**/*.crt'
- '**/test/**/*.key'
- '**/mock/**/*.key'
- '**/test/**/*.crt'
- '**/test/**/*.pem'
- '**/constants/mockCertificates.ts'
- '**/mock/**/*.pem'
- '**/mock_certificates/**/*.pem'
- '**/mock-data/**'
- '**/packages/mobile-sdk-alpha/ios/SelfSDK/**'
- '**/tests/**/*.key'
- '**/generated/**/*.pem'
- '**/tests/**/*.pem'
- '**/test-data/**'
- common/src/mock_certificates/**
- '**/*.xcframework'
version: 2

View File

@@ -76,7 +76,8 @@ jobs:
with:
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }}
- name: Build dependencies (cache miss)
if: steps.built-deps.outputs.cache-hit != 'true'
# Temporarily disabled due to `yarn types` failures when cache is used.
# if: steps.built-deps.outputs.cache-hit != 'true'
run: |
echo "Cache miss for built dependencies. Building now..."
yarn workspace @selfxyz/mobile-app run build:deps
@@ -297,7 +298,7 @@ jobs:
with:
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }}
- name: Build dependencies (cache miss)
if: steps.built-deps.outputs.cache-hit != 'true'
# if: steps.built-deps.outputs.cache-hit != 'true'
run: |
echo "Cache miss for built dependencies. Building now..."
yarn workspace @selfxyz/mobile-app run build:deps

View File

@@ -56,9 +56,7 @@ env:
IOS_PROV_PROFILE_DIRECTORY: "~/Library/MobileDevice/Provisioning\ Profiles/"
permissions:
contents: write
pull-requests: write
id-token: write
contents: read
on:
workflow_dispatch:
@@ -150,6 +148,8 @@ jobs:
# NOTE: Checks out the triggering branch (staging for PR merges, or the branch where manually triggered)
bump-version:
runs-on: ubuntu-latest
permissions:
contents: read
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
!contains(github.event.pull_request.labels.*.name, 'deploy:skip')
@@ -241,9 +241,26 @@ jobs:
echo "✅ Version bump calculated successfully"
echo "⚠️ Note: Changes are local only. Will be committed in PR after successful builds."
- name: Verify bump outputs were set
run: |
VERSION="${{ steps.bump.outputs.version }}"
IOS_BUILD="${{ steps.bump.outputs.ios_build }}"
ANDROID_BUILD="${{ steps.bump.outputs.android_build }}"
if [ -z "$VERSION" ] || [ -z "$IOS_BUILD" ] || [ -z "$ANDROID_BUILD" ]; then
echo "❌ Version bump failed to set required outputs"
echo "version='$VERSION', ios_build='$IOS_BUILD', android_build='$ANDROID_BUILD'"
exit 1
fi
echo "✅ All version outputs verified"
build-ios:
needs: [bump-version]
runs-on: macos-latest-large
permissions:
contents: read
actions: write
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
(
@@ -285,7 +302,7 @@ jobs:
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- name: Verify branch and commit (iOS)
if: inputs.platform != 'android'
if: needs.bump-version.outputs.platform != 'android'
run: |
echo "🔍 Verifying we're building from the correct branch and commit..."
echo "Current branch: $(git branch --show-current || git symbolic-ref --short HEAD 2>/dev/null || echo 'detached')"
@@ -309,7 +326,7 @@ jobs:
fi
- name: Apply version bump for build
if: inputs.platform != 'android'
if: needs.bump-version.outputs.platform != 'android'
run: |
cd ${{ env.APP_PATH }}
@@ -804,6 +821,10 @@ jobs:
build-android:
needs: [bump-version]
runs-on: ubuntu-latest
permissions:
contents: read
actions: write
id-token: write
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
(
@@ -910,7 +931,7 @@ jobs:
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- name: Verify branch and commit (Android)
if: inputs.platform != 'ios'
if: needs.bump-version.outputs.platform != 'ios'
run: |
echo "🔍 Verifying we're building from the correct branch and commit..."
echo "Current branch: $(git branch --show-current || git symbolic-ref --short HEAD 2>/dev/null || echo 'detached')"
@@ -930,7 +951,7 @@ jobs:
fi
- name: Apply version bump for build
if: inputs.platform != 'ios'
if: needs.bump-version.outputs.platform != 'ios'
run: |
cd ${{ env.APP_PATH }}
@@ -1234,6 +1255,9 @@ jobs:
# but create the version bump PR to dev so it can be reviewed before merging to staging
create-version-bump-pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
needs: [bump-version, build-ios, build-android]
if: |
always() &&
@@ -1284,6 +1308,20 @@ jobs:
echo "✅ Versions applied successfully"
- name: Verify version changes
run: |
cd ${{ env.APP_PATH }}
# Check that version files actually changed
if ! git diff --quiet package.json version.json; then
echo "✅ Version changes detected"
git diff package.json version.json
else
echo "⚠️ No version changes detected in package.json or version.json"
echo "This may indicate a problem with version application"
exit 1
fi
- name: Determine platforms that succeeded and PR title
id: platforms
run: |
@@ -1383,6 +1421,9 @@ jobs:
# Create git tags after successful deployment
create-release-tags:
runs-on: ubuntu-latest
permissions:
contents: write
needs: [bump-version, build-ios, build-android, create-version-bump-pr]
if: |
always() &&
@@ -1390,7 +1431,6 @@ jobs:
needs.create-version-bump-pr.result == 'success' &&
(needs.build-ios.result == 'success' || needs.build-android.result == 'success') &&
(inputs.deployment_track == 'production')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:

View File

@@ -15,6 +15,8 @@ env:
# Disable Maestro analytics in CI
MAESTRO_CLI_NO_ANALYTICS: true
MAESTRO_VERSION: 1.41.0
# E2E Testing flag for conditional compilation
E2E_TESTING: 1
on:
pull_request:

View File

@@ -8,6 +8,7 @@ on:
- "sdk/core/package.json"
- "sdk/qrcode/package.json"
- "common/package.json"
- 'packages/mobile-sdk-alpha/package.json'
- "sdk/qrcode-angular/package.json"
- "contracts/package.json"
workflow_dispatch:
@@ -21,6 +22,7 @@ jobs:
common_changed: ${{ steps.check-version.outputs.common_changed }}
contracts_changed: ${{ steps.check-version.outputs.contracts_changed }}
qrcode_angular_changed: ${{ steps.check-version.outputs.qrcode_angular_changed }}
msdk_changed: ${{ steps.check-version.outputs.msdk_changed }}
steps:
- uses: actions/checkout@v4
with:
@@ -34,6 +36,7 @@ jobs:
git diff HEAD^ HEAD --name-only | grep -q "common/package.json" && echo "common_changed=true" >> $GITHUB_OUTPUT || echo "common_changed=false" >> $GITHUB_OUTPUT
git diff HEAD^ HEAD --name-only | grep -q "contracts/package.json" && echo "contracts_changed=true" >> $GITHUB_OUTPUT || echo "contracts_changed=false" >> $GITHUB_OUTPUT
git diff HEAD^ HEAD --name-only | grep -q "sdk/qrcode-angular/package.json" && echo "qrcode_angular_changed=true" >> $GITHUB_OUTPUT || echo "qrcode_angular_changed=false" >> $GITHUB_OUTPUT
git diff HEAD^ HEAD --name-only | grep -q "packages/mobile-sdk-alpha/package.json" && echo "msdk_changed=true" >> $GITHUB_OUTPUT || echo "msdk_changed=false" >> $GITHUB_OUTPUT
# check if it was dispatched manually as well
if git diff HEAD^ HEAD -- sdk/core/package.json | grep -q '"version":' || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
@@ -56,6 +59,10 @@ jobs:
echo "qrcode_angular_changed=true" >> $GITHUB_OUTPUT
fi
if git diff HEAD^ HEAD -- sdk/mobile-sdk-alpha/package.json | grep -q '"version":' || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "msdk_changed=true" >> $GITHUB_OUTPUT
fi
publish-core:
needs: detect-changes
if: needs.detect-changes.outputs.core_changed == 'true'
@@ -196,3 +203,33 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-msdk:
needs: detect-changes
if: needs.detect-changes.outputs.msdk_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
registry-url: "https://registry.npmjs.org"
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Build package dependencies
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/mobile-sdk-alpha build
- name: Publish to npm
working-directory: packages/mobile-sdk-alpha
run: |
yarn config set npmScopes.selfxyz.npmAuthToken ${{ secrets.NPM_TOKEN }}
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 }}

View File

@@ -31,11 +31,6 @@ on:
- staging
- production
default: staging
push:
branches:
- dev
paths:
- ".github/workflows/release-calendar.yml"
schedule:
# Friday 17:00 UTC (see timezone conversions above) to prepare the weekend staging PR.
- cron: "0 17 * * 5"
@@ -58,18 +53,6 @@ jobs:
run: |
set -euo pipefail
# Allow push events (when workflow file is modified) to run
if [ "${{ github.event_name }}" == "push" ]; then
if [ "${{ github.ref_name }}" == "dev" ]; then
echo "Triggered by push event on dev. Running staging job."
echo "continue=true" >> "$GITHUB_OUTPUT"
else
echo "Triggered by push event on non-dev. Skipping staging job."
echo "continue=false" >> "$GITHUB_OUTPUT"
fi
exit 0
fi
# Allow workflow_dispatch based on input
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
JOB_TO_RUN="${{ inputs.job_to_run }}"
@@ -160,7 +143,7 @@ jobs:
- name: Create dev to staging release PR
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr == '' }}
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
PR_DATE: ${{ steps.check_dev_staging.outputs.date }}
BRANCH_NAME: ${{ steps.check_dev_staging.outputs.branch_name }}
shell: bash
@@ -226,13 +209,6 @@ jobs:
run: |
set -euo pipefail
# Skip production job on push events (we only test on dev)
if [ "${{ github.event_name }}" == "push" ]; then
echo "Push event: skipping production job."
echo "continue=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Allow workflow_dispatch based on input
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
JOB_TO_RUN="${{ inputs.job_to_run }}"
@@ -328,7 +304,7 @@ jobs:
- 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: ${{ github.token }}
GH_TOKEN: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
PR_DATE: ${{ steps.production_status.outputs.date }}
COMMITS_AHEAD: ${{ steps.production_status.outputs.commits }}
shell: bash

View File

@@ -14,6 +14,7 @@ on:
jobs:
web-build:
runs-on: ubuntu-latest
if: false
steps:
- uses: actions/checkout@v4
- name: Install Dependencies

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
contracts/contracts/RegisterASCII.txt
sdk/.env
.DS_Store
# Exception: Include mobile-sdk-alpha AAR files
!packages/mobile-sdk-alpha/dist/android/*.aar
dist
**/node_modules
**/node_modules/

View File

@@ -28,6 +28,7 @@ paths = [
'''Database.refactorlog''',
'''vendor''',
'''.*tamagui-components\.config\.cjs$''',
'''packages/mobile-sdk-alpha/src/animations/.*\.json$''',
]
[[rules]]

View File

@@ -2,3 +2,4 @@
1b461a626e0a4a93d4e1c727e7aed8c955aa728c:common/src/utils/passports/validate.test.ts:generic-api-key:55
1b461a626e0a4a93d4e1c727e7aed8c955aa728c:common/src/utils/passports/validate.test.ts:generic-api-key:73
1b461a626e0a4a93d4e1c727e7aed8c955aa728c:common/src/utils/passports/validate.test.ts:generic-api-key:74
8bc1e85075f73906767652ab35d5563efce2a931:packages/mobile-sdk-alpha/src/animations/passport_verify.json:aws-access-token:6

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "packages/mobile-sdk-alpha/mobile-sdk-native"]
path = packages/mobile-sdk-alpha/mobile-sdk-native
url = git@github.com:selfxyz/mobile-sdk-native.git
branch = main

View File

@@ -34,24 +34,6 @@ module.exports = {
],
settings: {
react: { version: 'detect' },
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json',
extensions: [
'.ts',
'.tsx',
'.native.ts',
'.native.tsx',
'.web.ts',
'.web.tsx',
'.ios.ts',
'.ios.tsx',
'.android.ts',
'.android.tsx',
],
},
},
'import/ignore': ['react-native'],
},
rules: {
@@ -169,7 +151,7 @@ module.exports = {
'@typescript-eslint/no-namespace': 'off',
'no-case-declarations': 'off',
'react/no-children-prop': 'off',
'import/no-unresolved': 'error',
'import/no-unresolved': 'off', // TypeScript handles this
'@typescript-eslint/ban-ts-comment': 'off',
'no-empty': 'off',
@@ -188,6 +170,14 @@ module.exports = {
'@typescript-eslint/indent': 'off',
},
overrides: [
{
// Enable TypeScript project service for source files for faster parsing
files: ['src/**/*.{ts,tsx}'],
parserOptions: {
project: true,
EXPERIMENTAL_useProjectService: true,
},
},
{
files: ['docs/examples/**/*.ts'],
rules: {
@@ -218,6 +208,7 @@ module.exports = {
},
parserOptions: {
project: './tsconfig.test.json',
EXPERIMENTAL_useProjectService: true,
},
rules: {
// Allow console logging and relaxed typing in tests

6
app/.gitignore vendored
View File

@@ -74,6 +74,12 @@ yarn-error.log
# testing
/coverage
# linting cache
.eslintcache
# prettier cache
.cache/
.env
**/Pods/

View File

@@ -25,7 +25,7 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1172.0)
aws-partitions (1.1173.0)
aws-sdk-core (3.233.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@@ -34,10 +34,10 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.113.0)
aws-sdk-kms (1.114.0)
aws-sdk-core (~> 3, >= 3.231.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.199.1)
aws-sdk-s3 (1.200.0)
aws-sdk-core (~> 3, >= 3.231.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)

View File

@@ -10,3 +10,5 @@ include ':react-native-passport-reader'
project(':react-native-passport-reader').projectDir = new File(rootProject.projectDir, './react-native-passport-reader/android')
include ':passportreader'
project(':passportreader').projectDir = new File(rootProject.projectDir, './android-passport-nfc-reader/app')
include ':mobile-sdk-alpha'
project(':mobile-sdk-alpha').projectDir = new File(rootProject.projectDir, '../../packages/mobile-sdk-alpha/android')

View File

@@ -49,12 +49,12 @@ ios_xcode_profile_path = "../ios/#{PROJECT_NAME}.xcodeproj"
default_platform(:ios)
platform :ios do
desc "Sync ios version"
desc "Sync ios version (DEPRECATED)"
lane :sync_version do
increment_version_number(
xcodeproj: "ios/#{PROJECT_NAME}.xcodeproj",
version_number: package_version,
)
UI.error("⛔ This lane is deprecated!")
UI.error("Version management is now centralized in CI.")
UI.error("Use: node scripts/version-manager.cjs apply <version> <ios> <android>")
UI.user_error!("sync_version lane is deprecated - use version-manager.cjs instead")
end
desc "Push a new build to TestFlight Internal Testing"
@@ -247,12 +247,12 @@ platform :ios do
end
platform :android do
desc "Sync android version"
desc "Sync android version (DEPRECATED)"
lane :sync_version do
android_set_version_name(
version_name: package_version,
gradle_file: android_gradle_file_path.gsub("../", ""),
)
UI.error("⛔ This lane is deprecated!")
UI.error("Version management is now centralized in CI.")
UI.error("Use: node scripts/version-manager.cjs apply <version> <ios> <android>")
UI.user_error!("sync_version lane is deprecated - use version-manager.cjs instead")
end
desc "Push a new build to Google Play Internal Testing"

View File

@@ -63,10 +63,26 @@ module Fastlane
android_matches = android_build == expected_android_build
unless version_matches && ios_matches && android_matches
UI.error("Version mismatch detected!")
UI.error("Version mismatch detected!")
UI.error("Expected: v#{expected_version} (iOS: #{expected_ios_build}, Android: #{expected_android_build})")
UI.error("Actual: v#{pkg_version} (iOS: #{ios_build}, Android: #{android_build})")
UI.user_error!("Version mismatch! CI version-manager script should have set these correctly.")
UI.error("")
# Add specific diagnostics
UI.error("Mismatched fields:")
UI.error(" • package.json version") unless version_matches
UI.error(" • version.json iOS build") unless ios_matches
UI.error(" • version.json Android build") unless android_matches
UI.error("")
UI.error("💡 Common causes:")
UI.error(" 1. version-manager.cjs 'apply' command didn't run in workflow")
UI.error(" 2. Files were modified after version bump was applied")
UI.error(" 3. CI_VERSION, CI_IOS_BUILD, or CI_ANDROID_BUILD env vars are incorrect")
UI.error("")
UI.error("🔍 Debug: Check workflow logs for 'Apply version bump' step")
UI.user_error!("Version verification failed")
end
UI.success("✅ Version verification passed:")

View File

@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.7.0</string>
<string>2.7.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -1590,6 +1590,27 @@ PODS:
- Yoga
- react-native-sqlite-storage (6.0.1):
- React-Core
- react-native-webview (13.16.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.10.14.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- React-nativeconfig (0.76.9)
- React-NativeModulesApple (0.76.9):
- glog
@@ -2183,6 +2204,7 @@ DEPENDENCIES:
- react-native-nfc-manager (from `../node_modules/react-native-nfc-manager`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
@@ -2355,6 +2377,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-safe-area-context"
react-native-sqlite-storage:
:path: "../node_modules/react-native-sqlite-storage"
react-native-webview:
:path: "../node_modules/react-native-webview"
React-nativeconfig:
:path: "../node_modules/react-native/ReactCommon"
React-NativeModulesApple:
@@ -2523,6 +2547,7 @@ SPEC CHECKSUMS:
react-native-nfc-manager: 66a00e5ddab9704efebe19d605b1b8afb0bb1bd7
react-native-safe-area-context: 90a89cb349c7f8168a707e6452288c2f665b9fd1
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
react-native-webview: 3f45e19f0ffc3701168768a6c37695e0f252410e
React-nativeconfig: 415626a63057638759bcc75e0a96e2e07771a479
React-NativeModulesApple: d33b55553c6957ff94835574636838d78121a1c6
React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358

View File

@@ -542,7 +542,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
MARKETING_VERSION = 2.7.0;
MARKETING_VERSION = 2.7.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -682,7 +682,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
MARKETING_VERSION = 2.7.0;
MARKETING_VERSION = 2.7.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

View File

@@ -4,12 +4,19 @@
module.exports = {
preset: 'react-native',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'cjs', 'json', '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)/)',
],
setupFiles: ['<rootDir>/jest.setup.js'],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
testMatch: [
'<rootDir>/**/__tests__/**/*.{js,jsx,ts,tsx,cjs}',
'<rootDir>/**/?(*.)+(spec|test).{js,jsx,ts,tsx,cjs}',
],
testPathIgnorePatterns: [
'/node_modules/',
'/scripts/tests/', // Node.js native test runner tests
],
moduleNameMapper: {
'^@env$': '<rootDir>/tests/__setup__/@env.js',
'\\.svg$': '<rootDir>/tests/__setup__/svgMock.js',
@@ -27,6 +34,8 @@ module.exports = {
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/flows/onboarding/$1.cjs',
'^@selfxyz/mobile-sdk-alpha/disclosing/(.*)$':
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/flows/disclosing/$1.cjs',
'^@selfxyz/mobile-sdk-alpha/(.*)\\.json$':
'<rootDir>/../packages/mobile-sdk-alpha/dist/$1.json',
'^@selfxyz/mobile-sdk-alpha/(.*)$':
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/$1.cjs',
// Fix snarkjs resolution for @anon-aadhaar/core

View File

@@ -4,6 +4,33 @@
/* global jest */
/** @jest-environment jsdom */
// Mock React Native PixelRatio globally before anything else loads
const mockPixelRatio = {
get: jest.fn(() => 2),
getFontScale: jest.fn(() => 1),
getPixelSizeForLayoutSize: jest.fn(layoutSize => layoutSize * 2),
roundToNearestPixel: jest.fn(layoutSize => Math.round(layoutSize * 2) / 2),
startDetecting: jest.fn(),
};
global.PixelRatio = mockPixelRatio;
// Also make it available for require() calls
const Module = require('module');
const originalRequire = Module.prototype.require;
Module.prototype.require = function (id) {
if (id === 'react-native') {
const RN = originalRequire.apply(this, arguments);
if (!RN.PixelRatio || !RN.PixelRatio.getFontScale) {
RN.PixelRatio = mockPixelRatio;
}
return RN;
}
return originalRequire.apply(this, arguments);
};
require('react-native-gesture-handler/jestSetup');
// Mock NativeAnimatedHelper - using virtual mock during RN 0.76.9 prep phase
@@ -64,7 +91,50 @@ jest.mock('react-native/Libraries/TurboModule/TurboModuleRegistry', () => ({
get: jest.fn(() => null),
}));
// Mock the mobile-sdk-alpha's React Native instance separately
// Mock main React Native PixelRatio module
jest.mock('react-native/Libraries/Utilities/PixelRatio', () => ({
get: jest.fn(() => 2),
getFontScale: jest.fn(() => 1),
getPixelSizeForLayoutSize: jest.fn(layoutSize => layoutSize * 2),
roundToNearestPixel: jest.fn(layoutSize => Math.round(layoutSize * 2) / 2),
startDetecting: jest.fn(),
}));
// Mock mobile-sdk-alpha to use the main React Native instance instead of its own
jest.mock(
'../packages/mobile-sdk-alpha/node_modules/react-native',
() => {
// Create the PixelRatio mock first
const PixelRatio = {
get: jest.fn(() => 2),
getFontScale: jest.fn(() => 1),
getPixelSizeForLayoutSize: jest.fn(layoutSize => layoutSize * 2),
roundToNearestPixel: jest.fn(
layoutSize => Math.round(layoutSize * 2) / 2,
),
startDetecting: jest.fn(),
};
const RN = jest.requireActual('react-native');
// Override the PixelRatio immediately
RN.PixelRatio = PixelRatio;
// Make sure both the default and named exports work
const mockedRN = {
...RN,
PixelRatio,
default: {
...RN,
PixelRatio,
},
};
return mockedRN;
},
{ virtual: true },
);
// Mock the mobile-sdk-alpha's TurboModuleRegistry to prevent native module errors
jest.mock(
'../packages/mobile-sdk-alpha/node_modules/react-native/Libraries/TurboModule/TurboModuleRegistry',
() => ({
@@ -112,7 +182,7 @@ jest.mock(
{ virtual: true },
);
// Mock mobile-sdk-alpha's PixelRatio module
// Mock mobile-sdk-alpha's PixelRatio module directly since it's still needed by StyleSheet
jest.mock(
'../packages/mobile-sdk-alpha/node_modules/react-native/Libraries/Utilities/PixelRatio',
() => ({
@@ -125,7 +195,7 @@ jest.mock(
{ virtual: true },
);
// Mock mobile-sdk-alpha's StyleSheet module directly
// Mock mobile-sdk-alpha's StyleSheet module directly since it's still needed
jest.mock(
'../packages/mobile-sdk-alpha/node_modules/react-native/Libraries/StyleSheet/StyleSheet',
() => ({
@@ -144,6 +214,21 @@ jest.mock(
{ virtual: true },
);
// Mock main React Native StyleSheet module
jest.mock('react-native/Libraries/StyleSheet/StyleSheet', () => ({
create: jest.fn(styles => styles),
flatten: jest.fn(style => style),
hairlineWidth: 1,
absoluteFillObject: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
roundToNearestPixel: jest.fn(layoutSize => Math.round(layoutSize * 2) / 2),
}));
// Mock NativeDeviceInfo specs for both main app and mobile-sdk-alpha
jest.mock('react-native/src/private/specs/modules/NativeDeviceInfo', () => ({
getConstants: jest.fn(() => ({
@@ -167,6 +252,19 @@ jest.mock('react-native-gesture-handler', () => {
};
});
// Mock react-native-safe-area-context
jest.mock('react-native-safe-area-context', () => {
const React = require('react');
const { View } = require('react-native');
return {
__esModule: true,
SafeAreaProvider: ({ children }) =>
React.createElement(View, null, children),
SafeAreaView: ({ children }) => React.createElement(View, null, children),
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
};
});
// Mock NativeEventEmitter to prevent null argument errors
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => {
return class MockNativeEventEmitter {
@@ -731,6 +829,10 @@ jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native');
return {
...actualNav,
useFocusEffect: jest.fn(callback => {
// Immediately invoke the effect for testing without requiring a container
return callback();
}),
useNavigation: jest.fn(() => ({
navigate: jest.fn(),
goBack: jest.fn(),
@@ -751,3 +853,104 @@ jest.mock('@react-navigation/native-stack', () => ({
})),
createNavigatorFactory: jest.fn(),
}));
// Mock core navigation to avoid requiring a NavigationContainer for hooks
jest.mock('@react-navigation/core', () => {
const actualCore = jest.requireActual('@react-navigation/core');
return {
...actualCore,
useNavigation: jest.fn(() => ({
navigate: jest.fn(),
goBack: jest.fn(),
canGoBack: jest.fn(() => true),
dispatch: jest.fn(),
})),
};
});
// Mock react-native-webview globally to avoid ESM parsing and native behaviors
jest.mock('react-native-webview', () => {
const React = require('react');
const { View } = require('react-native');
const MockWebView = React.forwardRef((props, ref) => {
return React.createElement(View, { ref, testID: 'webview', ...props });
});
MockWebView.displayName = 'MockWebView';
return {
__esModule: true,
default: MockWebView,
WebView: MockWebView,
};
});
// Mock ExpandableBottomLayout to simple containers to avoid SDK internals in tests
jest.mock('@/layouts/ExpandableBottomLayout', () => {
const React = require('react');
const { View } = require('react-native');
const Layout = ({ children }) => React.createElement(View, null, children);
const TopSection = ({ children }) =>
React.createElement(View, null, children);
const BottomSection = ({ children }) =>
React.createElement(View, null, children);
const FullSection = ({ children }) =>
React.createElement(View, null, children);
return {
__esModule: true,
ExpandableBottomLayout: { Layout, TopSection, BottomSection, FullSection },
};
});
// Mock mobile-sdk-alpha components used by NavBar (Button, XStack)
jest.mock('@selfxyz/mobile-sdk-alpha/components', () => {
const React = require('react');
const { View, Text, TouchableOpacity } = require('react-native');
const Button = ({ children, onPress, icon, ...props }) =>
React.createElement(
TouchableOpacity,
{ onPress, ...props, testID: 'msdk-button' },
icon
? React.createElement(View, { testID: 'msdk-button-icon' }, icon)
: null,
children,
);
const XStack = ({ children, ...props }) =>
React.createElement(View, { ...props, testID: 'msdk-xstack' }, children);
return {
__esModule: true,
Button,
XStack,
// Provide minimal Text to satisfy potential usages
Text,
};
});
// Mock Tamagui lucide icons to simple components to avoid theme context
jest.mock('@tamagui/lucide-icons', () => {
const React = require('react');
const { View } = require('react-native');
const makeIcon = name => {
const Icon = ({ size, color, opacity }) =>
React.createElement(View, {
testID: `icon-${name}`,
size,
color,
opacity,
});
Icon.displayName = `MockIcon(${name})`;
return Icon;
};
return {
__esModule: true,
ExternalLink: makeIcon('external-link'),
X: makeIcon('x'),
};
});
// Mock WebViewFooter to avoid SDK rendering complexity
jest.mock('@/components/WebViewFooter', () => {
const React = require('react');
const { View } = require('react-native');
const WebViewFooter = () =>
React.createElement(View, { testID: 'webview-footer' });
return { __esModule: true, WebViewFooter };
});

View File

@@ -66,11 +66,18 @@ const config = {
new RegExp('packages/mobile-sdk-alpha/node_modules/react(/|$)'),
new RegExp('packages/mobile-sdk-alpha/node_modules/react-dom(/|$)'),
new RegExp('packages/mobile-sdk-alpha/node_modules/react-native(/|$)'),
new RegExp(
'packages/mobile-sdk-alpha/node_modules/lottie-react-native(/|$)',
),
new RegExp('packages/mobile-sdk-alpha/node_modules/scheduler(/|$)'),
new RegExp(
'packages/mobile-sdk-alpha/node_modules/react-native-svg(/|$)',
),
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(/|$)'),
new RegExp('packages/mobile-sdk-demo/node_modules/scheduler(/|$)'),
new RegExp('packages/mobile-sdk-demo/node_modules/react-native-svg(/|$)'),
],
// Enable automatic workspace package resolution
enableGlobalPackages: true,
@@ -95,6 +102,10 @@ const config = {
assert: require.resolve('assert'),
events: require.resolve('events'),
process: require.resolve('process'),
'react-native-svg': path.resolve(
projectRoot,
'node_modules/react-native-svg',
),
// App-specific alias
'@': path.join(__dirname, 'src'),
},

View File

@@ -1,6 +1,6 @@
{
"name": "@selfxyz/mobile-app",
"version": "2.7.0",
"version": "2.7.1",
"private": true,
"type": "module",
"scripts": {
@@ -24,8 +24,8 @@
"clean:xcode": "rm -rf ~/Library/Developer/Xcode/DerivedData",
"clean:xcode-env-local": "rm -f ios/.xcode.env.local",
"find:type-imports": "node scripts/find-type-import-issues.mjs",
"fmt": "yarn prettier --check .",
"fmt:fix": "yarn prettier --write .",
"fmt": "yarn prettier --check --cache .",
"fmt:fix": "yarn prettier --write --cache .",
"format": "yarn nice",
"ia": "yarn install-app",
"imports:fix": "node ./scripts/alias-imports.cjs",
@@ -34,8 +34,8 @@
"install-app:setup": "yarn install && yarn build:deps && yarn setup:android-deps && cd ios && bundle install && scripts/pod-install-with-cache-fix.sh && cd ..",
"ios": "yarn build:deps && node scripts/run-ios-simulator.cjs",
"ios:fastlane-debug": "yarn reinstall && bundle exec fastlane --verbose ios internal_test",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"lint": "eslint . --cache --cache-location .eslintcache",
"lint:fix": "eslint --fix . --cache --cache-location .eslintcache",
"mobile-deploy": "node scripts/mobile-deploy-confirm.cjs both",
"mobile-deploy:android": "node scripts/mobile-deploy-confirm.cjs android",
"mobile-deploy:ios": "node scripts/mobile-deploy-confirm.cjs ios",
@@ -145,6 +145,7 @@
"react-native-svg": "15.12.1",
"react-native-svg-web": "^1.0.9",
"react-native-web": "^0.19.0",
"react-native-webview": "^13.16.0",
"react-qr-barcode-scanner": "^2.1.8",
"socket.io-client": "^4.8.1",
"tamagui": "1.126.14",

View File

@@ -5,7 +5,7 @@
module.exports = {
project: { ios: {}, android: {} },
dependencies: {
'@selfxyz/mobile-sdk-alpha': { platforms: { android: null } },
'@selfxyz/mobile-sdk-alpha': { platforms: { android: null, ios: null } },
},
assets: ['../src/assets/fonts'],
};

View File

@@ -123,15 +123,15 @@ else
log "📁 android-passport-nfc-reader already exists - preserving existing directory"
fi
# Build and package the SDK with timeout
log "Building SDK..."
# Build and package the SDK with timeout (including dependencies)
log "Building SDK and dependencies..."
if is_ci; then
timeout 300 yarn workspace @selfxyz/mobile-sdk-alpha build || {
timeout 300 yarn workspaces foreach --from @selfxyz/mobile-sdk-alpha --topological --recursive run build || {
log "SDK build timed out after 5 minutes"
exit 1
}
else
yarn workspace @selfxyz/mobile-sdk-alpha build
yarn workspaces foreach --from @selfxyz/mobile-sdk-alpha --topological --recursive run build
fi
log "Creating SDK tarball..."
@@ -189,20 +189,20 @@ else
env -u SELFXYZ_INTERNAL_REPO_PAT yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH"
fi
# Verify installation (check both local and hoisted locations)
SDK_ANDROID_PATH=""
if [[ -d "node_modules/@selfxyz/mobile-sdk-alpha/android/src/main/res" ]]; then
SDK_ANDROID_PATH="node_modules/@selfxyz/mobile-sdk-alpha/android/src/main/res"
elif [[ -d "../node_modules/@selfxyz/mobile-sdk-alpha/android/src/main/res" ]]; then
SDK_ANDROID_PATH="../node_modules/@selfxyz/mobile-sdk-alpha/android/src/main/res"
# Verify installation (check for AAR file in both local and hoisted locations)
SDK_AAR_PATH=""
if [[ -f "node_modules/@selfxyz/mobile-sdk-alpha/dist/android/mobile-sdk-alpha-release.aar" ]]; then
SDK_AAR_PATH="node_modules/@selfxyz/mobile-sdk-alpha/dist/android/mobile-sdk-alpha-release.aar"
elif [[ -f "../node_modules/@selfxyz/mobile-sdk-alpha/dist/android/mobile-sdk-alpha-release.aar" ]]; then
SDK_AAR_PATH="../node_modules/@selfxyz/mobile-sdk-alpha/dist/android/mobile-sdk-alpha-release.aar"
else
log "ERROR: SDK Android resources not found after installation"
log "Checked: node_modules/@selfxyz/mobile-sdk-alpha/android/src/main/res"
log "Checked: ../node_modules/@selfxyz/mobile-sdk-alpha/android/src/main/res"
log "ERROR: SDK AAR file not found after installation"
log "Checked: node_modules/@selfxyz/mobile-sdk-alpha/dist/android/mobile-sdk-alpha-release.aar"
log "Checked: ../node_modules/@selfxyz/mobile-sdk-alpha/dist/android/mobile-sdk-alpha-release.aar"
exit 1
fi
log "SDK Android resources found at: $SDK_ANDROID_PATH"
log "SDK AAR file found at: $SDK_AAR_PATH"
# Build Android APK (don't install to device)
log "Building Android APK..."

View File

@@ -204,10 +204,33 @@ function bumpVersion(bumpType, platform = 'both') {
* Apply version changes to files
*/
function applyVersions(version, iosBuild, androidBuild) {
// Validate version format (semver X.Y.Z)
if (
!version ||
typeof version !== 'string' ||
!/^\d+\.\d+\.\d+$/.test(version)
) {
throw new Error(`Invalid version format: ${version}. Expected X.Y.Z`);
}
// Validate and coerce build numbers
const iosNum = Number(iosBuild);
const androidNum = Number(androidBuild);
if (!Number.isInteger(iosNum) || iosNum < 1) {
throw new Error(`Invalid iOS build: ${iosBuild}. Must be positive integer`);
}
if (!Number.isInteger(androidNum) || androidNum < 1) {
throw new Error(
`Invalid Android build: ${androidBuild}. Must be positive integer`,
);
}
console.log(`📝 Applying versions to files...`);
console.log(` Version: ${version}`);
console.log(` iOS Build: ${iosBuild}`);
console.log(` Android Build: ${androidBuild}`);
console.log(` iOS Build: ${iosNum}`);
console.log(` Android Build: ${androidNum}`);
// Update package.json
const pkg = readPackageJson();
@@ -217,8 +240,8 @@ function applyVersions(version, iosBuild, androidBuild) {
// Update version.json
const versionData = readVersionJson();
versionData.ios.build = iosBuild;
versionData.android.build = androidBuild;
versionData.ios.build = iosNum;
versionData.android.build = androidNum;
writeVersionJson(versionData);
console.log(`✅ Updated version.json`);
}

View File

@@ -0,0 +1,316 @@
// 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.
/**
* @jest-environment node
*
* 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.
*/
/**
* Unit tests for version-manager.cjs
*
* This file is only meant to be run with Jest.
*/
const path = require('path');
// Mock file system operations - data
const mockPackageJson = { version: '1.2.3' };
const mockVersionJson = {
ios: { build: 100, lastDeployed: '2024-01-01T00:00:00Z' },
android: { build: 200, lastDeployed: '2024-01-01T00:00:00Z' },
};
// Use manual mocking instead of jest.mock to avoid hoisting issues
const fs = require('fs');
// Store originals for restore
const originalReadFileSync = fs.readFileSync;
const originalWriteFileSync = fs.writeFileSync;
const originalExistsSync = fs.existsSync;
const originalAppendFileSync = fs.appendFileSync;
// Setup mocks before importing the module
function setupMocks() {
fs.readFileSync = function (filePath, encoding) {
if (filePath.includes('package.json')) {
return JSON.stringify(mockPackageJson);
}
if (filePath.includes('version.json')) {
return JSON.stringify(mockVersionJson);
}
return originalReadFileSync(filePath, encoding);
};
fs.writeFileSync = function () {};
fs.existsSync = function () {
return true;
};
fs.appendFileSync = function () {};
}
function restoreMocks() {
fs.readFileSync = originalReadFileSync;
fs.writeFileSync = originalWriteFileSync;
fs.existsSync = originalExistsSync;
fs.appendFileSync = originalAppendFileSync;
}
// Setup mocks before requiring the module
setupMocks();
// Import module after mocks are set up
const versionManager = require('./version-manager.cjs');
describe('version-manager', () => {
beforeEach(() => {
// Reset mock data
mockPackageJson.version = '1.2.3';
mockVersionJson.ios.build = 100;
mockVersionJson.android.build = 200;
});
afterAll(() => {
restoreMocks();
});
describe('getVersionInfo', () => {
it('should return current version information', () => {
const info = versionManager.getVersionInfo();
expect(info.version).toBe('1.2.3');
expect(info.iosBuild).toBe(100);
expect(info.androidBuild).toBe(200);
});
});
describe('bumpVersion', () => {
it('should bump major version correctly', () => {
const result = versionManager.bumpVersion('major', 'both');
expect(result.version).toBe('2.0.0');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(201);
});
it('should bump minor version correctly', () => {
const result = versionManager.bumpVersion('minor', 'both');
expect(result.version).toBe('1.3.0');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(201);
});
it('should bump patch version correctly', () => {
const result = versionManager.bumpVersion('patch', 'both');
expect(result.version).toBe('1.2.4');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(201);
});
it('should bump build numbers only', () => {
const result = versionManager.bumpVersion('build', 'both');
expect(result.version).toBe('1.2.3');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(201);
});
it('should respect platform parameter (ios only)', () => {
const result = versionManager.bumpVersion('build', 'ios');
expect(result.version).toBe('1.2.3');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(200); // unchanged
});
it('should respect platform parameter (android only)', () => {
const result = versionManager.bumpVersion('build', 'android');
expect(result.version).toBe('1.2.3');
expect(result.iosBuild).toBe(100); // unchanged
expect(result.androidBuild).toBe(201);
});
it('should throw on invalid bump type', () => {
expect(() => versionManager.bumpVersion('invalid', 'both')).toThrow(
/Invalid bump type/,
);
});
it('should throw on invalid platform', () => {
expect(() => versionManager.bumpVersion('build', 'invalid')).toThrow(
/Invalid platform/,
);
});
it('should handle version with major bump resetting minor and patch', () => {
mockPackageJson.version = '2.5.8';
const result = versionManager.bumpVersion('major', 'both');
expect(result.version).toBe('3.0.0');
});
it('should handle version with minor bump resetting patch', () => {
mockPackageJson.version = '2.5.8';
const result = versionManager.bumpVersion('minor', 'both');
expect(result.version).toBe('2.6.0');
});
});
describe('applyVersions', () => {
it('should reject invalid version format - not semver', () => {
expect(() => versionManager.applyVersions('invalid', 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid version format - two parts', () => {
expect(() => versionManager.applyVersions('1.2', 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid version format - four parts', () => {
expect(() => versionManager.applyVersions('1.2.3.4', 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid version format - empty string', () => {
expect(() => versionManager.applyVersions('', 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid version format - null', () => {
expect(() => versionManager.applyVersions(null, 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid iOS build number - zero', () => {
expect(() => versionManager.applyVersions('1.2.3', 0, 1)).toThrow(
/Invalid iOS build/,
);
});
it('should reject invalid iOS build number - negative', () => {
expect(() => versionManager.applyVersions('1.2.3', -1, 1)).toThrow(
/Invalid iOS build/,
);
});
it('should reject invalid iOS build number - non-numeric string', () => {
expect(() => versionManager.applyVersions('1.2.3', 'abc', 1)).toThrow(
/Invalid iOS build/,
);
});
it('should reject invalid iOS build number - float', () => {
expect(() => versionManager.applyVersions('1.2.3', 1.5, 1)).toThrow(
/Invalid iOS build/,
);
});
it('should reject invalid Android build number - zero', () => {
expect(() => versionManager.applyVersions('1.2.3', 1, 0)).toThrow(
/Invalid Android build/,
);
});
it('should reject invalid Android build number - negative', () => {
expect(() => versionManager.applyVersions('1.2.3', 1, -1)).toThrow(
/Invalid Android build/,
);
});
it('should reject invalid Android build number - non-numeric string', () => {
expect(() => versionManager.applyVersions('1.2.3', 1, 'xyz')).toThrow(
/Invalid Android build/,
);
});
it('should reject invalid Android build number - float', () => {
expect(() => versionManager.applyVersions('1.2.3', 1, 2.5)).toThrow(
/Invalid Android build/,
);
});
it('should accept string build numbers that parse to integers', () => {
expect(() =>
versionManager.applyVersions('1.2.3', '100', '200'),
).not.toThrow();
});
it('should accept large build numbers', () => {
expect(() =>
versionManager.applyVersions('1.2.3', 99999, 88888),
).not.toThrow();
});
it('should write correct values to files', () => {
// Track write calls
const writeCalls = [];
fs.writeFileSync = function (filePath, content) {
writeCalls.push({ filePath, content });
};
versionManager.applyVersions('2.0.0', 150, 250);
// Verify writes occurred
expect(writeCalls.length).toBe(2);
// Find and verify package.json write
const packageWrite = writeCalls.find(call =>
call.filePath.includes('package.json'),
);
expect(packageWrite).toBeDefined();
const updatedPackage = JSON.parse(packageWrite.content);
expect(updatedPackage.version).toBe('2.0.0');
// Find and verify version.json write
const versionWrite = writeCalls.find(call =>
call.filePath.includes('version.json'),
);
expect(versionWrite).toBeDefined();
const updatedVersion = JSON.parse(versionWrite.content);
expect(updatedVersion.ios.build).toBe(150);
expect(updatedVersion.android.build).toBe(250);
});
});
describe('readPackageJson', () => {
it('should read and parse package.json', () => {
const pkg = versionManager.readPackageJson();
expect(pkg.version).toBe('1.2.3');
});
it('should throw error if file does not exist', () => {
const originalExists = fs.existsSync;
fs.existsSync = function () {
return false;
};
expect(() => versionManager.readPackageJson()).toThrow(
/package.json not found/,
);
fs.existsSync = originalExists;
});
});
describe('readVersionJson', () => {
it('should read and parse version.json', () => {
const version = versionManager.readVersionJson();
expect(version.ios.build).toBe(100);
expect(version.android.build).toBe(200);
});
it('should throw error if file does not exist', () => {
const originalExists = fs.existsSync;
fs.existsSync = function () {
return false;
};
expect(() => versionManager.readVersionJson()).toThrow(
/version.json not found/,
);
fs.existsSync = originalExists;
});
});
});

View File

@@ -2,5 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
export const loadMiscAnimation = () => import('@/assets/animations/loading/misc.json');
export const loadPassportAnimation = () => import('@/assets/animations/passport_verify.json');
export const loadMiscAnimation = () =>
import('@selfxyz/mobile-sdk-alpha/animations/loading/misc.json');
export const loadPassportAnimation = () =>
import('@/assets/animations/passport_verify.json');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,10 +4,11 @@
import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button, XStack, YStack } from 'tamagui';
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
import { ChevronLeft, HelpCircle } from '@tamagui/lucide-icons';
import { Button, XStack, YStack } from '@selfxyz/mobile-sdk-alpha/components';
import { NavBar } from '@/components/NavBar/BaseNavBar';
import { black, slate100, slate300 } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';

View File

@@ -6,13 +6,17 @@ 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 type { ViewProps, XStackProps } from 'tamagui';
import { Button, View, XStack } from 'tamagui';
import { ChevronLeft, X } from '@tamagui/lucide-icons';
import { Title } from '@selfxyz/mobile-sdk-alpha/components';
import type { ViewProps } from '@selfxyz/mobile-sdk-alpha/components';
import {
Button,
Title,
View,
XStack,
} from '@selfxyz/mobile-sdk-alpha/components';
interface NavBarProps extends XStackProps {
interface NavBarProps extends ViewProps {
children: React.ReactNode;
backgroundColor?: string;
barStyle?: SystemBarStyle;
@@ -96,6 +100,10 @@ const Container: React.FC<NavBarProps> = ({
children,
backgroundColor,
barStyle,
justifyContent = 'flex-start',
alignItems = 'center',
flexShrink = 0,
flexDirection = 'row',
...props
}) => {
return (
@@ -103,8 +111,10 @@ const Container: React.FC<NavBarProps> = ({
<SystemBars style={barStyle} />
<XStack
backgroundColor={backgroundColor}
justifyContent="flex-start"
alignItems="center"
justifyContent={justifyContent}
alignItems={alignItems}
flexShrink={flexShrink}
flexDirection={flexDirection}
{...props}
>
{children}

View File

@@ -3,8 +3,8 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import type { TextStyle, ViewStyle } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { TextStyle, ViewStyle } from 'tamagui';
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
import { NavBar } from '@/components/NavBar/BaseNavBar';

View File

@@ -4,9 +4,10 @@
import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button, Text, View } from 'tamagui';
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
import { Button, Text, View } from '@selfxyz/mobile-sdk-alpha/components';
import { NavBar } from '@/components/NavBar/BaseNavBar';
import { black, charcoal, slate50 } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
@@ -29,7 +30,7 @@ export const IdDetailsNavBar = (props: NativeStackHeaderProps) => {
unstyled
marginLeft={'$3.5'}
padding={'$3'}
width={'$10'}
width={104}
onPress={() => {
buttonTap();
props.navigation.goBack();

View File

@@ -0,0 +1,88 @@
// 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 { StyleSheet, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ExternalLink, X } from '@tamagui/lucide-icons';
import { Button, XStack } from '@selfxyz/mobile-sdk-alpha/components';
import { black } from '@/utils/colors';
import { dinot } from '@/utils/fonts';
import { buttonTap } from '@/utils/haptic';
export interface WebViewNavBarProps {
title?: string;
onBackPress: () => void;
onOpenExternalPress?: () => void;
isOpenExternalDisabled?: boolean;
}
export const WebViewNavBar: React.FC<WebViewNavBarProps> = ({
title,
onBackPress,
onOpenExternalPress,
isOpenExternalDisabled,
}) => {
const insets = useSafeAreaInsets();
return (
<XStack
paddingHorizontal={20}
paddingVertical={10}
paddingTop={insets.top + 10}
gap={14}
alignItems="center"
backgroundColor="white"
>
{/* Left: Close Button */}
<Button
unstyled
hitSlop={{ top: 20, bottom: 20, left: 20, right: 10 }}
icon={<X size={24} color={black} />}
onPress={() => {
buttonTap();
onBackPress();
}}
/>
{/* Center: Title */}
<XStack flex={1} alignItems="center" justifyContent="center">
<Text style={styles.title} numberOfLines={1}>
{title?.toUpperCase() || 'PAGE TITLE'}
</Text>
</XStack>
{/* Right: Open External Button */}
<Button
unstyled
disabled={isOpenExternalDisabled}
hitSlop={{ top: 20, bottom: 20, left: 10, right: 20 }}
icon={
<ExternalLink
size={24}
color={isOpenExternalDisabled ? black : black}
opacity={isOpenExternalDisabled ? 0.3 : 1}
/>
}
onPress={() => {
buttonTap();
onOpenExternalPress?.();
}}
/>
</XStack>
);
};
const styles = StyleSheet.create({
title: {
fontFamily: dinot,
fontSize: 15,
color: black,
letterSpacing: 0.6,
textAlign: 'center',
textTransform: 'uppercase',
},
});

View File

@@ -0,0 +1,86 @@
// 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 { ArrowLeft, ArrowRight, RotateCcw } from '@tamagui/lucide-icons';
import { Button, XStack, YStack } from '@selfxyz/mobile-sdk-alpha/components';
import { black, slate50, slate400 } from '@/utils/colors';
import { buttonTap } from '@/utils/haptic';
export interface WebViewFooterProps {
canGoBack: boolean;
canGoForward: boolean;
onGoBack: () => void;
onGoForward: () => void;
onReload: () => void;
onOpenInBrowser: () => void;
}
const iconSize = 22;
const buttonSize = 36;
export const WebViewFooter: React.FC<WebViewFooterProps> = ({
canGoBack,
canGoForward,
onGoBack,
onGoForward,
onReload,
onOpenInBrowser: _onOpenInBrowser,
}) => {
const renderIconButton = (
key: string,
icon: React.ReactNode,
onPress: () => void,
disabled?: boolean,
) => (
<Button
key={key}
size="$4"
unstyled
disabled={disabled}
onPress={() => {
buttonTap();
onPress();
}}
backgroundColor={slate50}
borderRadius={buttonSize / 2}
width={buttonSize}
height={buttonSize}
alignItems="center"
justifyContent="center"
opacity={disabled ? 0.5 : 1}
>
{icon}
</Button>
);
return (
<YStack gap={12} paddingVertical={12} width="100%">
<XStack justifyContent="space-between" alignItems="center" width="100%">
{renderIconButton(
'back',
<ArrowLeft size={iconSize} color={canGoBack ? black : slate400} />,
onGoBack,
!canGoBack,
)}
{renderIconButton(
'reload',
<RotateCcw size={iconSize} color={black} />,
onReload,
)}
{renderIconButton(
'forward',
<ArrowRight
size={iconSize}
color={canGoForward ? black : slate400}
/>,
onGoForward,
!canGoForward,
)}
</XStack>
</YStack>
);
};

View File

@@ -7,7 +7,8 @@ import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Text, View, XStack, YStack } from 'tamagui';
import { DelayedLottieView } from '@/components/DelayedLottieView';
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
import CloseWarningIcon from '@/images/icons/close-warning.svg';
import Plus from '@/images/icons/plus_slate600.svg';
import {

8
app/src/global.d.ts vendored Normal file
View 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.
declare module '*.json' {
const value: any;
export default value;
}

View File

@@ -3,51 +3,32 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import {
Dimensions,
PixelRatio,
Platform,
ScrollView,
StyleSheet,
} from 'react-native';
import { SystemBars } from 'react-native-edge-to-edge';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { ViewProps } from 'tamagui';
import { View } from 'tamagui';
import { black, white } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
import type {
BottomSectionProps,
FullSectionProps,
LayoutProps,
TopSectionProps,
} from '@selfxyz/mobile-sdk-alpha';
import { ExpandableBottomLayout as BaseExpandableBottomLayout } from '@selfxyz/mobile-sdk-alpha';
// Get the current font scale factor
const fontScale = PixelRatio.getFontScale();
// fontScale > 1 means the user has increased text size in accessibility settings
const isLargerTextEnabled = fontScale > 1.3;
import { black } from '@/utils/colors';
interface ExpandableBottomLayoutProps extends ViewProps {
children: React.ReactNode;
backgroundColor: string;
}
interface TopSectionProps extends ViewProps {
children: React.ReactNode;
backgroundColor: string;
roundTop?: boolean;
}
interface BottomSectionProps extends ViewProps {
children: React.ReactNode;
backgroundColor: string;
}
const Layout: React.FC<ExpandableBottomLayoutProps> = ({
const Layout: React.FC<LayoutProps> = ({
children,
backgroundColor,
...props
}) => {
return (
<View flex={1} flexDirection="column" backgroundColor={backgroundColor}>
<BaseExpandableBottomLayout.Layout
backgroundColor={backgroundColor}
{...props}
>
<SystemBars style={backgroundColor === black ? 'light' : 'dark'} />
{children}
</View>
</BaseExpandableBottomLayout.Layout>
);
};
@@ -57,24 +38,18 @@ const TopSection: React.FC<TopSectionProps> = ({
...props
}) => {
const { top } = useSafeAreaInsets();
const { roundTop, ...restProps } = props;
return (
<View
{...restProps}
<BaseExpandableBottomLayout.TopSection
backgroundColor={backgroundColor}
style={[
styles.topSection,
roundTop && styles.roundTop,
roundTop ? { marginTop: top } : { paddingTop: top },
{ backgroundColor },
]}
safeAreaTop={top}
{...props}
>
{children}
</View>
</BaseExpandableBottomLayout.TopSection>
);
};
type FullSectionProps = ViewProps;
/*
* Rather than using a top and bottom section, this component is te entire thing.
* It leave space for the safe area insets and provides basic padding
@@ -85,75 +60,38 @@ const FullSection: React.FC<FullSectionProps> = ({
...props
}: FullSectionProps) => {
const { top, bottom } = useSafeAreaInsets();
return (
<View
paddingHorizontal={20}
<BaseExpandableBottomLayout.FullSection
backgroundColor={backgroundColor}
paddingTop={top}
paddingBottom={bottom}
safeAreaTop={top}
safeAreaBottom={bottom}
{...props}
>
{children}
</View>
</BaseExpandableBottomLayout.FullSection>
);
};
const BottomSection: React.FC<BottomSectionProps> = ({
children,
style,
...props
}) => {
const { bottom: safeAreaBottom } = useSafeAreaInsets();
const incomingBottom = props.paddingBottom ?? 0;
const minBottom = safeAreaBottom + extraYPadding;
const totalBottom =
typeof incomingBottom === 'number' ? minBottom + incomingBottom : minBottom;
let panelHeight: number | 'auto' = 'auto';
// set bottom section height to 38% of screen height
// and wrap children in a scroll view if larger text is enabled
if (isLargerTextEnabled) {
const windowHeight = Dimensions.get('window').height;
panelHeight = windowHeight * 0.38;
children = (
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ flexGrow: 1 }}
>
{children}
</ScrollView>
);
}
return (
<View
<BaseExpandableBottomLayout.BottomSection
safeAreaBottom={safeAreaBottom}
{...props}
height={panelHeight}
style={[styles.bottomSection, style]}
paddingBottom={totalBottom}
>
{children}
</View>
</BaseExpandableBottomLayout.BottomSection>
);
};
/**
* This component is a layout that has a top and bottom section. Bottom section
* automatically expands to as much space as it needs while the top section
* takes up the remaining space.
*
* Usage:
*
* import { ExpandableBottomLayout } from '../components/ExpandableBottomLayout';
*
* <ExpandableBottomLayout.Layout>
* <ExpandableBottomLayout.TopSection>
* <...top section content...>
* </ExpandableBottomLayout.TopSection>
* <ExpandableBottomLayout.BottomSection>
* <...bottom section content...>
* </ExpandableBottomLayout.BottomSection>
* </ExpandableBottomLayout.Layout>
* This component is a wrapper around the ExpandableBottomLayout component from the mobile SDK
* pacakge. It handles the safe area insets and system bars.
*/
export const ExpandableBottomLayout = {
Layout,
@@ -161,32 +99,3 @@ export const ExpandableBottomLayout = {
FullSection,
BottomSection,
};
const styles = StyleSheet.create({
roundTop: {
marginTop: 12,
overflow: 'hidden',
borderTopRightRadius: 30,
borderTopLeftRadius: 30,
},
layout: {
height: '100%',
flexDirection: 'column',
},
topSection: {
alignSelf: 'stretch',
flexGrow: 1,
flexShrink: Platform.select({ web: 0, default: 1 }),
alignItems: 'center',
justifyContent: 'center',
backgroundColor: black,
overflow: 'hidden',
padding: 20,
},
bottomSection: {
backgroundColor: white,
paddingTop: 30,
paddingLeft: 20,
paddingRight: 20,
},
});

View File

@@ -13,6 +13,7 @@ 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';
@@ -25,6 +26,9 @@ import homeScreens from '@/navigation/home';
import onboardingScreens from '@/navigation/onboarding';
import sharedScreens from '@/navigation/shared';
import verificationScreens from '@/navigation/verification';
import type { ModalNavigationParams } from '@/screens/app/ModalScreen';
import type { WebViewScreenParams } from '@/screens/shared/WebViewScreen';
import type { ProofHistory } from '@/stores/proofTypes';
import analytics from '@/utils/analytics';
import { setupUniversalLinkListenerInNavigation } from '@/utils/deeplinks';
@@ -38,6 +42,7 @@ export const navigationScreens = {
...sharedScreens,
...devScreens, // allow in production for testing
};
const AppNavigation = createNativeStackNavigator({
id: undefined,
initialRouteName: Platform.OS === 'web' ? 'Home' : 'Splash',
@@ -53,22 +58,93 @@ type BaseRootStackParamList = StaticParamList<typeof AppNavigation>;
// Explicitly declare route params that are not inferred from initialParams
export type RootStackParamList = Omit<
BaseRootStackParamList,
'ComingSoon' | 'IDPicker' | 'AadhaarUpload' | 'AadhaarUploadError'
| 'ComingSoon'
| 'IDPicker'
| 'AadhaarUpload'
| 'AadhaarUploadError'
| 'WebView'
| 'AccountRecovery'
| 'SaveRecoveryPhrase'
| 'CloudBackupSettings'
| 'ConfirmBelonging'
| 'ProofHistoryDetail'
| 'Loading'
| 'Modal'
| 'CreateMock'
| 'MockDataDeepLink'
| 'DocumentNFCScan'
| 'AadhaarUploadSuccess'
> & {
// 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;
// Aadhaar screens
AadhaarUpload: {
countryCode: string;
};
AadhaarUploadSuccess: undefined;
AadhaarUploadError: {
errorType: string;
};
// Account/Recovery screens
AccountRecovery:
| {
nextScreen?: string;
}
| undefined;
SaveRecoveryPhrase:
| {
nextScreen?: string;
}
| undefined;
CloudBackupSettings:
| {
nextScreen?: string;
}
| undefined;
// Proof/Verification screens
ProofHistoryDetail: {
data: ProofHistory;
};
// App screens
Loading: {
documentCategory?: DocumentCategory;
signatureAlgorithm?: string;
curveOrExponent?: string;
};
Modal: ModalNavigationParams;
// Dev screens
CreateMock: undefined;
MockDataDeepLink: undefined;
};
export type RootStackScreenProps<T extends keyof RootStackParamList> =

View File

@@ -2,19 +2,42 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import type { ComponentType } from 'react';
import type {
NativeStackNavigationOptions,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import type { SharedRoutesParamList } from '@/navigation/types';
import ComingSoonScreen from '@/screens/shared/ComingSoonScreen';
import { WebViewScreen } from '@/screens/shared/WebViewScreen';
const sharedScreens = {
type ScreenName = keyof SharedRoutesParamList;
type ScreenConfig<Name extends ScreenName> = {
screen: ComponentType<NativeStackScreenProps<SharedRoutesParamList, Name>>;
options?: NativeStackNavigationOptions;
initialParams?: SharedRoutesParamList[Name];
};
const sharedScreens: { [K in ScreenName]: ScreenConfig<K> } = {
ComingSoon: {
screen: ComingSoonScreen,
options: {
headerShown: false,
} as NativeStackNavigationOptions,
},
WebView: {
screen: WebViewScreen,
options: {
headerShown: false,
} as NativeStackNavigationOptions,
initialParams: {
countryCode: null,
documentCategory: null,
url: 'https://self.xyz',
title: undefined,
shareTitle: undefined,
shareMessage: undefined,
shareUrl: undefined,
},
},
};

View 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 type { DocumentCategory } from '@selfxyz/common/types';
export type SharedRoutesParamList = {
ComingSoon: {
countryCode?: string;
documentCategory?: DocumentCategory;
};
WebView: {
url: string;
title?: string;
shareTitle?: string;
shareMessage?: string;
shareUrl?: string;
};
};

View File

@@ -6,17 +6,18 @@ import type { PropsWithChildren } from 'react';
import { useMemo } from 'react';
import { Platform } from 'react-native';
import type {
Adapters,
TrackEventParams,
WsConn,
} from '@selfxyz/mobile-sdk-alpha';
import {
type Adapters,
createListenersMap,
impactLight,
type LogLevel,
type NFCScanContext,
reactNativeScannerAdapter,
SdkEvents,
SelfClientProvider as SDKSelfClientProvider,
type TrackEventParams,
webNFCScannerShim,
type WsConn,
} from '@selfxyz/mobile-sdk-alpha';
import type { RootStackParamList } from '@/navigation';
@@ -25,7 +26,7 @@ import { unsafe_getPrivateKey } from '@/providers/authProvider';
import { selfClientDocumentsAdapter } from '@/providers/passportDataProvider';
import { logNFCEvent, logProofEvent } from '@/Sentry';
import { useSettingStore } from '@/stores/settingStore';
import analytics from '@/utils/analytics';
import analytics, { trackNfcEvent } from '@/utils/analytics';
type GlobalCrypto = { crypto?: { subtle?: Crypto['subtle'] } };
/**
@@ -114,6 +115,17 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
trackEvent: (event: string, data?: TrackEventParams) => {
analytics().trackEvent(event, data);
},
trackNfcEvent: (name: string, data?: Record<string, unknown>) => {
trackNfcEvent(name, data);
},
logNFCEvent: (
level: LogLevel,
message: string,
context: NFCScanContext,
details?: Record<string, unknown>,
) => {
logNFCEvent(level, message, context, details);
},
},
auth: {
getPrivateKey: () => unsafe_getPrivateKey(),
@@ -216,21 +228,15 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
});
addListener(SdkEvents.DOCUMENT_MRZ_READ_SUCCESS, () => {
if (navigationRef.isReady()) {
navigationRef.navigate('DocumentNFCScan');
}
navigateIfReady('DocumentNFCScan');
});
addListener(SdkEvents.DOCUMENT_MRZ_READ_FAILURE, () => {
if (navigationRef.isReady()) {
navigationRef.navigate('DocumentCameraTrouble');
}
navigateIfReady('DocumentCameraTrouble');
});
addListener(SdkEvents.PROVING_AADHAAR_UPLOAD_SUCCESS, () => {
if (navigationRef.isReady()) {
navigationRef.navigate('AadhaarUploadSuccess');
}
navigateIfReady('AadhaarUploadSuccess');
});
addListener(SdkEvents.PROVING_AADHAAR_UPLOAD_FAILURE, ({ errorType }) => {
navigateIfReady('AadhaarUploadError', { errorType });
@@ -245,9 +251,7 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
countryCode: string;
documentTypes: string[];
}) => {
if (navigationRef.isReady()) {
navigationRef.navigate('IDPicker', { countryCode, documentTypes });
}
navigateIfReady('IDPicker', { countryCode, documentTypes });
},
);
addListener(
@@ -276,6 +280,18 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
},
);
addListener(
SdkEvents.DOCUMENT_OWNERSHIP_CONFIRMED,
({ documentCategory, signatureAlgorithm, curveOrExponent }) => {
impactLight();
navigateIfReady('Loading', {
documentCategory,
signatureAlgorithm,
curveOrExponent,
});
},
);
return map;
}, []);

View File

@@ -33,7 +33,6 @@ import Star from '@/images/icons/star.svg';
import Telegram from '@/images/icons/telegram.svg';
import Web from '@/images/icons/webpage.svg';
import X from '@/images/icons/x.svg';
import type { RootStackParamList } from '@/navigation';
import { useSettingStore } from '@/stores/settingStore';
import { amber500, black, neutral700, slate800, white } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
@@ -41,6 +40,8 @@ import { impactLight } from '@/utils/haptic';
import { getCountry, getLocales, getTimeZone } from '@/utils/locale';
import { version } from '../../../../package.json';
// Avoid importing RootStackParamList to prevent type cycles; use minimal typing
type MinimalRootStackParamList = Record<string, object | undefined>;
interface MenuButtonProps extends PropsWithChildren {
Icon: React.FC<SvgProps>;
@@ -52,11 +53,8 @@ interface SocialButtonProps {
}
const emailFeedback = 'support@self.xyz';
type RouteOption =
| keyof RootStackParamList
| 'share'
| 'email_feedback'
| 'ManageDocuments';
// Avoid importing RootStackParamList; we only need string route names plus a few literals
type RouteOption = string | 'share' | 'email_feedback' | 'ManageDocuments';
const storeURL = Platform.OS === 'ios' ? appStoreUrl : playStoreUrl;
@@ -144,7 +142,7 @@ const SocialButton: React.FC<SocialButtonProps> = ({ Icon, href }) => {
const SettingsScreen: React.FC = () => {
const { isDevMode, setDevModeOn } = useSettingStore();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
useNavigation<NativeStackNavigationProp<MinimalRootStackParamList>>();
const screenRoutes = useMemo(() => {
return isDevMode ? [...routes, ...DEBUG_MENU] : routes;
@@ -230,9 +228,11 @@ ${deviceInfo.map(([k, v]) => `${k}=${v}`).join('; ')}
justifyContent="flex-start"
width="100%"
>
{screenRoutes.map(([Icon, menuText, menuRoute]) => (
{screenRoutes.map(([Icon, menuText, menuRoute], idx) => (
<MenuButton
key={menuRoute}
key={
typeof menuRoute === 'string' ? menuRoute : String(idx)
}
Icon={Icon}
onPress={onMenuPress(menuRoute)}
>

View File

@@ -10,10 +10,10 @@ import { useFocusEffect, useIsFocused } from '@react-navigation/native';
import type { DocumentCategory } from '@selfxyz/common/utils/types';
import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import failAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/fail.json';
import proveLoadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/prove.json';
import type { ProvingStateType } from '@selfxyz/mobile-sdk-alpha/browser';
import failAnimation from '@/assets/animations/loading/fail.json';
import proveLoadingAnimation from '@/assets/animations/loading/prove.json';
import LoadingUI from '@/components/loading/LoadingUI';
import { useSettingStore } from '@/stores/settingStore';
import { black, slate400, white, zinc900 } from '@/utils/colors';

View File

@@ -8,12 +8,12 @@ import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import {
DelayedLottieView,
hasAnyValidRegisteredDocument,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import splashAnimation from '@/assets/animations/splash.json';
import { DelayedLottieView } from '@/components/DelayedLottieView';
import type { RootStackParamList } from '@/navigation';
import { migrateToSecureKeychain, useAuth } from '@/providers/authProvider';
import {

View File

@@ -29,10 +29,13 @@ import {
signatureAlgorithmToStrictSignatureAlgorithm,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import { Caption, PrimaryButton } from '@selfxyz/mobile-sdk-alpha/components';
import {
ButtonsContainer,
Caption,
PrimaryButton,
} from '@selfxyz/mobile-sdk-alpha/components';
import { MockDataEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import ButtonsContainer from '@/components/ButtonsContainer';
import { useMockDataForm } from '@/hooks/useMockDataForm';
import SelfDevCard from '@/images/card-dev.svg';
import IdIcon from '@/images/icons/id_icon.svg';

View File

@@ -16,13 +16,13 @@ import type { IdDocInput } from '@selfxyz/common/utils';
import { genMockIdDocAndInitDataParsing } from '@selfxyz/common/utils/passports';
import {
BodyText,
ButtonsContainer,
Description,
PrimaryButton,
Title,
} from '@selfxyz/mobile-sdk-alpha/components';
import { MockDataEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import ButtonsContainer from '@/components/ButtonsContainer';
import type { RootStackParamList } from '@/navigation';
import { storePassportData } from '@/providers/passportDataProvider';
import useUserStore from '@/stores/userStore';

View File

@@ -11,9 +11,9 @@ import type {
provingMachineCircuitType,
ProvingStateType,
} from '@selfxyz/mobile-sdk-alpha';
import failAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/fail.json';
import proveLoadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/prove.json';
import failAnimation from '@/assets/animations/loading/fail.json';
import proveLoadingAnimation from '@/assets/animations/loading/prove.json';
import LoadingUI from '@/components/loading/LoadingUI';
import { slate200, slate500 } from '@/utils/colors';
import { dinot } from '@/utils/fonts';

View File

@@ -3,7 +3,13 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { PropsWithChildren } from 'react';
import React, { cloneElement, isValidElement, useMemo, useState } from 'react';
import React, {
cloneElement,
isValidElement,
useEffect,
useMemo,
useState,
} from 'react';
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
import { Alert, ScrollView } from 'react-native';
import { Adapt, Button, Select, Sheet, Text, XStack, YStack } from 'tamagui';
@@ -17,6 +23,7 @@ import WarningIcon from '@/images/icons/warning.svg';
import type { RootStackParamList } from '@/navigation';
import { unsafe_clearSecrets } from '@/providers/authProvider';
import { usePassport } from '@/providers/passportDataProvider';
import { useSettingStore } from '@/stores/settingStore';
import {
red500,
slate100,
@@ -30,6 +37,56 @@ import {
yellow500,
} from '@/utils/colors';
import { dinot } from '@/utils/fonts';
import {
isNotificationSystemReady,
requestNotificationPermission,
subscribeToTopics,
unsubscribeFromTopics,
} from '@/utils/notifications/notificationService';
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;
@@ -235,6 +292,130 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
const { clearDocumentCatalogForMigrationTesting } = usePassport();
const navigation =
useNavigation() as NativeStackScreenProps<RootStackParamList>['navigation'];
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
const [hasNotificationPermission, setHasNotificationPermission] =
useState(false);
// Check notification permissions on mount
useEffect(() => {
const checkPermissions = async () => {
const readiness = await isNotificationSystemReady();
setHasNotificationPermission(readiness.ready);
};
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' }],
);
}
}
};
const handleClearSecretsPress = () => {
Alert.alert(
@@ -370,6 +551,41 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
</YStack>
</ParameterSection>
<ParameterSection
icon={<BugIcon />}
title="Push Notifications"
description="Manage topic subscriptions"
>
<YStack gap="$2">
<TopicToggleButton
label="Nova"
isSubscribed={
hasNotificationPermission && subscribedTopics.includes('nova')
}
onToggle={() => handleTopicToggle(['nova'], 'Nova')}
/>
<TopicToggleButton
label="General"
isSubscribed={
hasNotificationPermission &&
subscribedTopics.includes('general')
}
onToggle={() => handleTopicToggle(['general'], 'General')}
/>
<TopicToggleButton
label="Both (Nova + General)"
isSubscribed={
hasNotificationPermission &&
subscribedTopics.includes('nova') &&
subscribedTopics.includes('general')
}
onToggle={() =>
handleTopicToggle(['nova', 'general'], 'both topics')
}
/>
</YStack>
</ParameterSection>
<ParameterSection
icon={<WarningIcon color={yellow500} />}
title="Danger Zone"

View File

@@ -16,12 +16,12 @@ import type {
} from '@selfxyz/common/utils/types';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
ButtonsContainer,
PrimaryButton,
SecondaryButton,
} from '@selfxyz/mobile-sdk-alpha/components';
import { DocumentEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import ButtonsContainer from '@/components/ButtonsContainer';
import type { RootStackParamList } from '@/navigation';
import { usePassport } from '@/providers/passportDataProvider';
import { borderColor, textBlack, white } from '@/utils/colors';

View File

@@ -8,6 +8,7 @@ import { View, XStack, YStack } from 'tamagui';
import { useIsFocused } from '@react-navigation/native';
import {
DelayedLottieView,
hasAnyValidRegisteredDocument,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
@@ -24,7 +25,6 @@ import {
} from '@selfxyz/mobile-sdk-alpha/onboarding/read-mrz';
import passportScanAnimation from '@/assets/animations/passport_scan.json';
import { DelayedLottieView } from '@/components/DelayedLottieView';
import { PassportCamera } from '@/components/native/PassportCamera';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import Scan from '@/images/icons/passport_camera_scan.svg';

View File

@@ -11,13 +11,13 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
BodyText,
ButtonsContainer,
Description,
PrimaryButton,
SecondaryButton,
Title,
} from '@selfxyz/mobile-sdk-alpha/components';
import ButtonsContainer from '@/components/ButtonsContainer';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { white } from '@/utils/colors';

View File

@@ -34,19 +34,20 @@ import { CircleHelp } from '@tamagui/lucide-icons';
import type { PassportData } from '@selfxyz/common/types';
import {
hasAnyValidRegisteredDocument,
sanitizeErrorMessage,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import {
BodyText,
ButtonsContainer,
PrimaryButton,
SecondaryButton,
TextsContainer,
Title,
} from '@selfxyz/mobile-sdk-alpha/components';
import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import passportVerifyAnimation from '@/assets/animations/passport_verify.json';
import ButtonsContainer from '@/components/ButtonsContainer';
import TextsContainer from '@/components/TextsContainer';
import { useFeedbackAutoHide } from '@/hooks/useFeedbackAutoHide';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import NFC_IMAGE from '@/images/nfc.png';
@@ -71,7 +72,6 @@ import {
impactLight,
} from '@/utils/haptic';
import { parseScanResponse, scan } from '@/utils/nfcScanner';
import { sanitizeErrorMessage } from '@/utils/utils';
const emitter =
Platform.OS === 'android'

View File

@@ -11,13 +11,13 @@ import {
} from '@selfxyz/mobile-sdk-alpha';
import {
BodyText,
ButtonsContainer,
SecondaryButton,
TextsContainer,
Title,
} from '@selfxyz/mobile-sdk-alpha/components';
import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import ButtonsContainer from '@/components/ButtonsContainer';
import TextsContainer from '@/components/TextsContainer';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import NFC_IMAGE from '@/images/nfc.png';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';

View File

@@ -2,32 +2,16 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useEffect, useState } from 'react';
import React, { useCallback } from 'react';
import type { StaticScreenProps } from '@react-navigation/native';
import { usePreventRemove } from '@react-navigation/native';
import type { DocumentCategory } from '@selfxyz/common/utils/types';
import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
Description,
PrimaryButton,
Title,
} from '@selfxyz/mobile-sdk-alpha/components';
import {
PassportEvents,
ProofEvents,
} from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { getPreRegistrationDescription } from '@selfxyz/mobile-sdk-alpha/onboarding/confirm-identification';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { ConfirmIdentificationScreen } from '@selfxyz/mobile-sdk-alpha/onboarding/confirm-identification';
import successAnimation from '@/assets/animations/loading/success.json';
import { DelayedLottieView } from '@/components/DelayedLottieView';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { styles } from '@/screens/verification/ProofRequestStatusScreen';
import { useSettingStore } from '@/stores/settingStore';
import { flushAllAnalytics, trackNfcEvent } from '@/utils/analytics';
import { black, white } from '@/utils/colors';
import { notificationSuccess } from '@/utils/haptic';
import {
getFCMToken,
requestNotificationPermission,
@@ -35,82 +19,32 @@ import {
type ConfirmBelongingScreenProps = StaticScreenProps<Record<string, never>>;
// TODO -- need to set safe area insets for this screen.
const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
const selfClient = useSelfClient();
const [documentMetadata, setDocumentMetadata] = useState<{
documentCategory?: DocumentCategory;
signatureAlgorithm?: string;
curveOrExponent?: string;
}>({});
const { trackEvent } = selfClient;
const navigate = useHapticNavigation('Loading', {
params: {
documentCategory: documentMetadata.documentCategory,
signatureAlgorithm: documentMetadata.signatureAlgorithm,
curveOrExponent: documentMetadata.curveOrExponent,
},
});
const [_requestingPermission, setRequestingPermission] = useState(false);
// Prevents back navigation
usePreventRemove(true, () => {});
const setFcmToken = useSettingStore(state => state.setFcmToken);
useEffect(() => {
notificationSuccess();
const selfClient = useSelfClient();
const { trackEvent } = selfClient;
const initializeProving = async () => {
try {
const selectedDocument = await loadSelectedDocument(selfClient);
let metadata: {
documentCategory?: DocumentCategory;
signatureAlgorithm?: string;
curveOrExponent?: string;
};
if (selectedDocument?.data?.documentCategory === 'aadhaar') {
metadata = {
documentCategory: 'aadhaar',
signatureAlgorithm: 'rsa',
curveOrExponent: '65537',
};
} else {
const passportData = selectedDocument?.data;
metadata = {
documentCategory: passportData?.documentCategory,
signatureAlgorithm:
passportData?.passportMetadata?.cscaSignatureAlgorithm,
curveOrExponent:
passportData?.passportMetadata?.cscaCurveOrExponent,
};
}
setDocumentMetadata(metadata);
} catch {
// setting defaults on error
setDocumentMetadata({
documentCategory: 'passport',
signatureAlgorithm: 'rsa',
curveOrExponent: '65537',
});
const grantNotificationsPermission = useCallback(async () => {
trackEvent(ProofEvents.NOTIFICATION_PERMISSION_REQUESTED);
// Request notification permission
const permissionGranted = await requestNotificationPermission();
if (permissionGranted) {
const token = await getFCMToken();
if (token) {
setFcmToken(token);
trackEvent(ProofEvents.FCM_TOKEN_STORED);
}
};
}
}, [trackEvent, setFcmToken]);
initializeProving();
}, [selfClient]);
const onOkPress = async () => {
const onOkPress = useCallback(async () => {
try {
setRequestingPermission(true);
trackEvent(ProofEvents.NOTIFICATION_PERMISSION_REQUESTED);
trackNfcEvent(ProofEvents.NOTIFICATION_PERMISSION_REQUESTED);
// Request notification permission
const permissionGranted = await requestNotificationPermission();
if (permissionGranted) {
const token = await getFCMToken();
if (token) {
setFcmToken(token);
trackEvent(ProofEvents.FCM_TOKEN_STORED);
}
}
navigate();
await grantNotificationsPermission();
} catch (error: unknown) {
console.error('Error navigating:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
@@ -123,43 +57,8 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
flushAllAnalytics();
}
};
// Prevents back navigation
usePreventRemove(true, () => {});
return (
<>
<ExpandableBottomLayout.Layout backgroundColor={black}>
<ExpandableBottomLayout.TopSection backgroundColor={black}>
<DelayedLottieView
autoPlay
loop={false}
source={successAnimation}
style={styles.animation}
cacheComposition={true}
renderMode="HARDWARE"
/>
</ExpandableBottomLayout.TopSection>
<ExpandableBottomLayout.BottomSection
gap={20}
paddingBottom={20}
backgroundColor={white}
>
<Title style={{ textAlign: 'center' }}>Confirm your identity</Title>
<Description style={{ textAlign: 'center', paddingBottom: 20 }}>
{getPreRegistrationDescription()}
</Description>
<PrimaryButton
trackEvent={PassportEvents.OWNERSHIP_CONFIRMED}
onPress={onOkPress}
>
Confirm
</PrimaryButton>
</ExpandableBottomLayout.BottomSection>
</ExpandableBottomLayout.Layout>
</>
);
}, [grantNotificationsPermission, trackEvent]);
return <ConfirmIdentificationScreen onBeforeConfirm={onOkPress} />;
};
export default ConfirmBelongingScreen;

View File

@@ -2,190 +2,17 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { memo, useCallback } from 'react';
import { FlatList, TouchableOpacity, View } from 'react-native';
import { Spinner, XStack, YStack } from 'tamagui';
import { commonNames } from '@selfxyz/common/constants/countries';
import {
SdkEvents,
useCountries,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import { BodyText, RoundFlag } from '@selfxyz/mobile-sdk-alpha/components';
import { YStack } from '@selfxyz/mobile-sdk-alpha/components';
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
import { DocumentFlowNavBar } from '@/components/NavBar/DocumentFlowNavBar';
import { black, slate100, slate500 } from '@/utils/colors';
import { advercase, dinot } from '@/utils/fonts';
import { buttonTap } from '@/utils/haptic';
interface CountryListItem {
key: string;
countryCode: string;
}
const ITEM_HEIGHT = 65;
const FLAG_SIZE = 32;
const CountryItem = memo<{
countryCode: string;
onSelect: (code: string) => void;
}>(({ countryCode, onSelect }) => {
const countryName = commonNames[countryCode as keyof typeof commonNames];
if (!countryName) return null;
return (
<TouchableOpacity
onPress={() => onSelect(countryCode)}
style={{
paddingVertical: 13,
}}
>
<XStack alignItems="center" gap={16}>
<RoundFlag countryCode={countryCode} size={FLAG_SIZE} />
<BodyText style={{ fontSize: 16, color: black, flex: 1 }}>
{countryName}
</BodyText>
</XStack>
</TouchableOpacity>
);
});
CountryItem.displayName = 'CountryItem';
const CountryPickerScreen: React.FC = () => {
const selfClient = useSelfClient();
const { countryData, countryList, loading, userCountryCode, showSuggestion } =
useCountries();
const onPressCountry = useCallback(
(countryCode: string) => {
buttonTap();
if (__DEV__) {
console.log('Selected country code:', countryCode);
console.log('Current countryData:', countryData);
console.log('Available country codes:', Object.keys(countryData));
}
const documentTypes = countryData[countryCode];
if (__DEV__) {
console.log('documentTypes for', countryCode, ':', documentTypes);
}
if (documentTypes && documentTypes.length > 0) {
const countryName =
commonNames[countryCode as keyof typeof commonNames] || countryCode;
// Emit the country selection event
selfClient.emit(SdkEvents.DOCUMENT_COUNTRY_SELECTED, {
countryCode: countryCode,
countryName: countryName,
documentTypes: documentTypes,
});
} else {
selfClient.emit(SdkEvents.PROVING_PASSPORT_NOT_SUPPORTED, {
countryCode: countryCode,
documentCategory: null,
});
}
},
[countryData, selfClient],
);
const renderItem = useCallback(
({ item }: { item: CountryListItem }) => (
<CountryItem countryCode={item.countryCode} onSelect={onPressCountry} />
),
[onPressCountry],
);
const keyExtractor = useCallback(
(item: CountryListItem) => item.countryCode,
[],
);
const renderLoadingState = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Spinner size="small" />
</View>
);
const getItemLayout = useCallback(
(_data: ArrayLike<CountryListItem> | null | undefined, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
[],
);
import { slate100 } from '@/utils/colors';
export default function CountryPickerScreen() {
return (
<YStack flex={1} backgroundColor={slate100}>
<DocumentFlowNavBar title="GETTING STARTED" />
<YStack flex={1} paddingTop="$4" paddingHorizontal="$4">
<YStack marginTop="$4" marginBottom="$6">
<BodyText style={{ fontSize: 29, fontFamily: advercase }}>
Select the country that issued your ID
</BodyText>
<BodyText style={{ fontSize: 16, color: slate500, marginTop: 20 }}>
Self has support for over 300 ID types. You can select the type of
ID in the next step
</BodyText>
</YStack>
{loading ? (
renderLoadingState()
) : (
<YStack flex={1}>
{showSuggestion && (
<YStack marginBottom="$2">
<BodyText
style={{
fontSize: 16,
color: black,
fontFamily: dinot,
letterSpacing: 0.8,
marginBottom: 8,
}}
>
SUGGESTION
</BodyText>
<CountryItem
countryCode={
userCountryCode as string /*safe due to showSuggestion*/
}
onSelect={onPressCountry}
/>
<BodyText
style={{
fontSize: 16,
color: black,
fontFamily: dinot,
letterSpacing: 0.8,
marginTop: 20,
}}
>
SELECT AN ISSUING COUNTRY
</BodyText>
</YStack>
)}
<FlatList
data={countryList}
renderItem={renderItem}
keyExtractor={keyExtractor}
showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={10}
updateCellsBatchingPeriod={50}
getItemLayout={getItemLayout}
/>
</YStack>
)}
</YStack>
<SDKCountryPickerScreen />
</YStack>
);
};
export default CountryPickerScreen;
}

View File

@@ -10,16 +10,16 @@ import { useNavigation } from '@react-navigation/native';
import {
Additional,
ButtonsContainer,
Description,
PrimaryButton,
SecondaryButton,
TextsContainer,
Title,
} from '@selfxyz/mobile-sdk-alpha/components';
import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import passportOnboardingAnimation from '@/assets/animations/passport_onboarding.json';
import ButtonsContainer from '@/components/ButtonsContainer';
import TextsContainer from '@/components/TextsContainer';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { black, slate100, white } from '@/utils/colors';

View File

@@ -4,96 +4,23 @@
import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { View, XStack, YStack } from 'tamagui';
import type { RouteProp } from '@react-navigation/native';
import { useRoute } from '@react-navigation/native';
import { SdkEvents, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { BodyText, RoundFlag } from '@selfxyz/mobile-sdk-alpha/components';
import AadhaarLogo from '@selfxyz/mobile-sdk-alpha/svgs/icons/aadhaar.svg';
import EPassportLogoRounded from '@selfxyz/mobile-sdk-alpha/svgs/icons/epassport_rounded.svg';
import PlusIcon from '@selfxyz/mobile-sdk-alpha/svgs/icons/plus.svg';
import SelfLogo from '@selfxyz/mobile-sdk-alpha/svgs/logo.svg';
import { YStack } from '@selfxyz/mobile-sdk-alpha/components';
import IDSelection from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-screen';
import { DocumentFlowNavBar } from '@/components/NavBar/DocumentFlowNavBar';
import type { RootStackParamList } from '@/navigation';
import { black, slate100, slate300, slate400, white } from '@/utils/colors';
import { slate100 } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
import { advercase, dinot } from '@/utils/fonts';
import { buttonTap } from '@/utils/haptic';
type IDPickerScreenRouteProp = RouteProp<RootStackParamList, 'IDPicker'>;
const getDocumentName = (docType: string): string => {
switch (docType) {
case 'p':
return 'Passport';
case 'i':
return 'ID card';
case 'a':
return 'Aadhaar';
default:
return 'Unknown Document';
}
};
const getDocumentNameForEvent = (docType: string): string => {
switch (docType) {
case 'p':
return 'passport';
case 'i':
return 'id_card';
case 'a':
return 'aadhaar';
default:
return 'unknown_document';
}
};
const getDocumentDescription = (docType: string): string => {
switch (docType) {
case 'p':
return 'Verified Biometric Passport';
case 'i':
return 'Verified Biometric ID card';
case 'a':
return 'Verified mAadhaar QR code';
default:
return 'Unknown Document';
}
};
const getDocumentLogo = (docType: string): React.ReactNode => {
switch (docType) {
case 'p':
return <EPassportLogoRounded />;
case 'i':
return <EPassportLogoRounded />;
case 'a':
return <AadhaarLogo />;
default:
return null;
}
};
const IDPickerScreen: React.FC = () => {
const route = useRoute<IDPickerScreenRouteProp>();
const { countryCode = '', documentTypes = [] } = route.params || {};
const bottom = useSafeAreaInsets().bottom;
const selfClient = useSelfClient();
const onSelectDocumentType = (docType: string) => {
buttonTap();
const countryName = getDocumentName(docType);
selfClient.emit(SdkEvents.DOCUMENT_TYPE_SELECTED, {
documentType: docType,
documentName: getDocumentNameForEvent(docType),
countryCode: countryCode,
countryName: countryName,
});
};
return (
<YStack
@@ -102,91 +29,7 @@ const IDPickerScreen: React.FC = () => {
paddingBottom={bottom + extraYPadding + 24}
>
<DocumentFlowNavBar title="GETTING STARTED" />
<YStack
flex={1}
paddingTop="$4"
paddingHorizontal="$4"
justifyContent="center"
>
<YStack marginTop="$4" marginBottom="$6">
<XStack
justifyContent="center"
alignItems="center"
borderRadius={'$2'}
gap={'$2.5'}
>
<View width={48} height={48}>
<RoundFlag countryCode={countryCode} size={48} />
</View>
<PlusIcon width={18} height={18} color={slate400} />
<YStack
backgroundColor={black}
borderRadius={'$2'}
height={48}
width={48}
justifyContent="center"
alignItems="center"
>
<SelfLogo width={24} height={24} />
</YStack>
</XStack>
<BodyText
style={{
marginTop: 48,
fontSize: 29,
fontFamily: advercase,
textAlign: 'center',
}}
>
Select an ID type
</BodyText>
</YStack>
<YStack gap="$3">
{documentTypes.map((docType: string) => (
<XStack
key={docType}
backgroundColor={white}
borderWidth={1}
borderColor={slate300}
elevation={4}
borderRadius={'$5'}
padding={'$3'}
pressStyle={{ scale: 0.97, backgroundColor: slate100 }}
onPress={() => onSelectDocumentType(docType)}
>
<XStack alignItems="center" gap={'$3'} flex={1}>
{getDocumentLogo(docType)}
<YStack gap={'$1'}>
<BodyText
style={{ fontSize: 24, fontFamily: dinot, color: black }}
>
{getDocumentName(docType)}
</BodyText>
<BodyText
style={{
fontSize: 14,
fontFamily: dinot,
color: slate400,
}}
>
{getDocumentDescription(docType)}
</BodyText>
</YStack>
</XStack>
</XStack>
))}
<BodyText
style={{
fontSize: 18,
fontFamily: dinot,
color: slate400,
textAlign: 'center',
}}
>
Be sure your document is ready to scan
</BodyText>
</YStack>
</YStack>
<IDSelection countryCode={countryCode} documentTypes={documentTypes} />
</YStack>
);
};

View File

@@ -7,6 +7,7 @@ import { YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
import {
Description,
PrimaryButton,
@@ -15,7 +16,6 @@ import {
import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import proofSuccessAnimation from '@/assets/animations/proof_success.json';
import { DelayedLottieView } from '@/components/DelayedLottieView';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { styles } from '@/screens/verification/ProofRequestStatusScreen';

View File

@@ -8,6 +8,7 @@ import { YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
import {
Caution,
PrimaryButton,
@@ -16,7 +17,6 @@ import {
import { AppEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import warningAnimation from '@/assets/animations/warning.json';
import { DelayedLottieView } from '@/components/DelayedLottieView';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { useSettingStore } from '@/stores/settingStore';

View File

@@ -4,10 +4,9 @@
import React, { useEffect, useMemo } from 'react';
import { XStack, YStack } from 'tamagui';
import type { RouteProp } from '@react-navigation/native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { countryCodes } from '@selfxyz/common/constants';
import type { DocumentCategory } from '@selfxyz/common/types';
import {
hasAnyValidRegisteredDocument,
useSelfClient,
@@ -23,6 +22,7 @@ import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { SharedRoutesParamList } from '@/navigation/types';
import analytics from '@/utils/analytics';
import { black, slate500, white } from '@/utils/colors';
import { sendCountrySupportNotification } from '@/utils/email';
@@ -30,20 +30,11 @@ import { notificationError } from '@/utils/haptic';
const { flush: flushAnalytics } = analytics();
type ComingSoonScreenRouteProp = RouteProp<
{
ComingSoon: {
countryCode: string;
documentCategory?: DocumentCategory;
};
},
type ComingSoonScreenProps = NativeStackScreenProps<
SharedRoutesParamList,
'ComingSoon'
>;
interface ComingSoonScreenProps {
route: ComingSoonScreenRouteProp;
}
const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
const selfClient = useSelfClient();
const navigateToLaunch = useHapticNavigation('Launch');

View File

@@ -0,0 +1,204 @@
// 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, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
BackHandler,
Linking,
StyleSheet,
View,
} from 'react-native';
import WebView, { type WebView as WebViewType } from 'react-native-webview';
import type { WebViewNavigation } from 'react-native-webview/lib/WebViewTypes';
import { useFocusEffect } from '@react-navigation/native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { WebViewNavBar } from '@/components/NavBar/WebViewNavBar';
import { WebViewFooter } from '@/components/WebViewFooter';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { SharedRoutesParamList } from '@/navigation/types';
import { charcoal, slate200, white } from '@/utils/colors';
export interface WebViewScreenParams {
url: string;
title?: string;
shareTitle?: string;
shareMessage?: string;
shareUrl?: string;
}
type WebViewScreenProps = NativeStackScreenProps<
SharedRoutesParamList,
'WebView'
>;
const defaultUrl = 'https://self.xyz';
export const WebViewScreen: React.FC<WebViewScreenProps> = ({
navigation,
route,
}) => {
const params = route?.params as WebViewScreenParams | undefined;
const safeParams: WebViewScreenParams = params ?? { url: defaultUrl };
const { url, title } = safeParams;
const isHttpUrl = useCallback((value?: string) => {
return typeof value === 'string' && /^https?:\/\//i.test(value);
}, []);
const initialUrl = useMemo(
() => (isHttpUrl(url) ? url : defaultUrl),
[isHttpUrl, url],
);
const webViewRef = useRef<WebViewType>(null);
const [canGoBackInWebView, setCanGoBackInWebView] = useState(false);
const [canGoForwardInWebView, setCanGoForwardInWebView] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [currentUrl, setCurrentUrl] = useState(initialUrl);
const [pageTitle, setPageTitle] = useState<string | undefined>(title);
const derivedTitle = pageTitle || title || currentUrl;
const openUrl = useCallback(async (targetUrl: string) => {
// Allow only safe external schemes
if (!/^(https?|mailto|tel):/i.test(targetUrl)) {
return;
}
try {
const supported = await Linking.canOpenURL(targetUrl);
if (supported) {
await Linking.openURL(targetUrl);
}
} catch (error) {
console.error(
'Failed to open externally',
error instanceof Error ? error.message : 'Unknown error',
);
}
}, []);
const handleOpenExternal = useCallback(async () => {
await openUrl(currentUrl);
}, [currentUrl, openUrl]);
const handleReload = useCallback(() => {
setIsLoading(true);
webViewRef.current?.reload();
}, []);
const handleGoBack = useCallback(() => {
if (canGoBackInWebView) {
webViewRef.current?.goBack();
return;
}
if (typeof navigation?.canGoBack === 'function' && navigation.canGoBack()) {
navigation.goBack();
}
}, [canGoBackInWebView, navigation]);
const handleGoForward = useCallback(() => {
if (canGoForwardInWebView) {
webViewRef.current?.goForward();
}
}, [canGoForwardInWebView]);
useFocusEffect(
useCallback(() => {
const subscription = BackHandler.addEventListener(
'hardwareBackPress',
() => {
if (canGoBackInWebView) {
webViewRef.current?.goBack();
return true;
}
return false;
},
);
return () => subscription.remove();
}, [canGoBackInWebView]),
);
return (
<ExpandableBottomLayout.Layout backgroundColor={white}>
<ExpandableBottomLayout.TopSection
backgroundColor={white}
alignItems="stretch"
justifyContent="flex-start"
padding={0}
>
<WebViewNavBar
title={derivedTitle}
onBackPress={handleGoBack}
onOpenExternalPress={handleOpenExternal}
/>
<View style={styles.webViewContainer}>
{isLoading && (
<View pointerEvents="none" style={styles.loadingOverlay}>
<ActivityIndicator size="small" color={charcoal} />
</View>
)}
<WebView
ref={webViewRef}
onShouldStartLoadWithRequest={req => {
// Open non-http(s) externally, block in WebView
if (!/^https?:\/\//i.test(req.url)) {
openUrl(req.url);
return false;
}
return true;
}}
source={{ uri: initialUrl }}
onNavigationStateChange={(event: WebViewNavigation) => {
setCanGoBackInWebView(event.canGoBack);
setCanGoForwardInWebView(event.canGoForward);
setCurrentUrl(prev => (isHttpUrl(event.url) ? event.url : prev));
if (!title && event.title) {
setPageTitle(event.title);
}
}}
onLoadStart={() => setIsLoading(true)}
onLoadEnd={() => setIsLoading(false)}
startInLoadingState
style={styles.webView}
/>
</View>
</ExpandableBottomLayout.TopSection>
<ExpandableBottomLayout.BottomSection
backgroundColor={white}
borderTopLeftRadius={30}
borderTopRightRadius={30}
borderTopWidth={1}
borderColor={slate200}
style={{ paddingTop: 0 }}
>
<WebViewFooter
canGoBack={canGoBackInWebView}
canGoForward={canGoForwardInWebView}
onGoBack={handleGoBack}
onGoForward={handleGoForward}
onReload={handleReload}
onOpenInBrowser={handleOpenExternal}
/>
</ExpandableBottomLayout.BottomSection>
</ExpandableBottomLayout.Layout>
);
};
const styles = StyleSheet.create({
webViewContainer: {
flex: 1,
alignSelf: 'stretch',
backgroundColor: white,
},
webView: {
flex: 1,
backgroundColor: white,
},
loadingOverlay: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(255,255,255,0.5)',
},
});

View File

@@ -11,6 +11,7 @@ import { ScrollView, Spinner } from 'tamagui';
import { useIsFocused } from '@react-navigation/native';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
import {
BodyText,
Description,
@@ -20,7 +21,6 @@ import {
} from '@selfxyz/mobile-sdk-alpha/components';
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import loadingAnimation from '@/assets/animations/loading/misc.json';
import failAnimation from '@/assets/animations/proof_failed.json';
import succesAnimation from '@/assets/animations/proof_success.json';
import useHapticNavigation from '@/hooks/useHapticNavigation';

View File

@@ -24,6 +24,7 @@ import { Eye, EyeOff } from '@tamagui/lucide-icons';
import type { SelfAppDisclosureConfig } from '@selfxyz/common/utils/appType';
import { formatEndpoint } from '@selfxyz/common/utils/scope';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import miscAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
import {
BodyText,
Caption,
@@ -31,7 +32,6 @@ import {
} from '@selfxyz/mobile-sdk-alpha/components';
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import miscAnimation from '@/assets/animations/loading/misc.json';
import Disclosures from '@/components/Disclosures';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';

View File

@@ -24,6 +24,10 @@ interface PersistedSettingsState {
setKeychainMigrationCompleted: () => void;
fcmToken: string | null;
setFcmToken: (token: string | null) => void;
subscribedTopics: string[];
setSubscribedTopics: (topics: string[]) => void;
addSubscribedTopic: (topic: string) => void;
removeSubscribedTopic: (topic: string) => void;
}
interface NonPersistedSettingsState {
@@ -78,6 +82,19 @@ export const useSettingStore = create<SettingsState>()(
set({ hasCompletedKeychainMigration: true }),
fcmToken: null,
setFcmToken: (token: string | null) => set({ fcmToken: token }),
subscribedTopics: [],
setSubscribedTopics: (topics: string[]) =>
set({ subscribedTopics: topics }),
addSubscribedTopic: (topic: string) =>
set(state => ({
subscribedTopics: Array.from(
new Set([...state.subscribedTopics, topic]),
),
})),
removeSubscribedTopic: (topic: string) =>
set(state => ({
subscribedTopics: state.subscribedTopics.filter(t => t !== topic),
})),
// Non-persisted state (will not be saved to storage)
hideNetworkModal: false,

View File

@@ -5,7 +5,7 @@
import { Linking, Platform } from 'react-native';
import { getCountry, getLocales, getTimeZone } from 'react-native-localize';
import { sanitizeErrorMessage } from '@/utils/utils';
import { sanitizeErrorMessage } from '@selfxyz/mobile-sdk-alpha';
import { version } from '../../package.json';

View File

@@ -2,9 +2,4 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Platform } from 'react-native';
export const advercase = 'Advercase-Regular';
export const dinot = 'DINOT-Medium';
export const plexMono =
Platform.OS === 'ios' ? 'IBM Plex Mono' : 'IBMPlexMono-Regular';
export { advercase, dinot, plexMono } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -2,140 +2,21 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Platform, Vibration } from 'react-native';
import { triggerFeedback } from '@/utils/haptic/trigger';
// Keep track of the loading screen interval
let loadingScreenInterval: ReturnType<typeof setInterval> | null = null;
// Define the base functions first
export const impactLight = () => triggerFeedback('impactLight');
export const impactMedium = () => triggerFeedback('impactMedium');
export const selectionChange = () => triggerFeedback('selection');
// Then define the aliases
export const buttonTap = impactLight;
export const cancelTap = selectionChange;
export const confirmTap = impactMedium;
// consistent light feedback at a steady interval
export const feedbackProgress = () => {
if (Platform.OS === 'android') {
// Pattern: [delay, duration, delay, duration, ...]
// Three light impacts at 750ms intervals
triggerFeedback('custom', {
pattern: [
0,
50, // First light impact
750,
50, // Second light impact
750,
50, // Third light impact
],
});
return;
}
// Match the timing of the light impacts in the Android pattern
setTimeout(() => {
triggerFeedback('impactLight');
}, 750); // First light impact
setTimeout(() => {
triggerFeedback('impactLight');
}, 1500); // Second light impact (750ms after first)
setTimeout(() => {
triggerFeedback('impactLight');
}, 2250); // Third light impact (750ms after second)
};
// light -> medium -> heavy intensity in sequence
export const feedbackSuccess = () => {
if (Platform.OS === 'android') {
// Pattern: [delay, duration, delay, duration, ...]
// Increasing intensity sequence: light -> medium -> heavy
triggerFeedback('custom', {
pattern: [
500,
50, // Initial delay, then light impact
200,
100, // Medium impact
150,
150, // Heavy impact
],
});
return;
}
setTimeout(() => {
triggerFeedback('impactLight');
}, 500);
setTimeout(() => {
triggerFeedback('impactMedium');
}, 750);
setTimeout(() => {
triggerFeedback('impactHeavy');
}, 1000);
};
// heavy -> medium -> light intensity in sequence
export const feedbackUnsuccessful = () => {
if (Platform.OS === 'android') {
// Pattern: [delay, duration, delay, duration, ...]
// Decreasing intensity sequence: heavy -> medium -> light
triggerFeedback('custom', {
pattern: [
500,
150, // Initial delay, then heavy impact
100,
100, // Medium impact
150,
50, // Light impact
],
});
return;
}
setTimeout(() => {
triggerFeedback('impactHeavy');
}, 500);
setTimeout(() => {
triggerFeedback('impactMedium');
}, 750);
setTimeout(() => {
triggerFeedback('impactLight');
}, 1000);
};
/**
* Haptic actions
*/
// Custom feedback events
export const loadingScreenProgress = (shouldVibrate: boolean = true) => {
// Clear any existing interval
if (loadingScreenInterval) {
clearInterval(loadingScreenInterval);
loadingScreenInterval = null;
}
// If we shouldn't vibrate, just stop here
if (!shouldVibrate) {
Vibration.cancel();
return;
}
triggerFeedback('impactHeavy');
loadingScreenInterval = setInterval(() => {
triggerFeedback('impactHeavy');
}, 1000);
};
export const notificationError = () => triggerFeedback('notificationError');
export const notificationSuccess = () => triggerFeedback('notificationSuccess');
export const notificationWarning = () => triggerFeedback('notificationWarning');
export { triggerFeedback } from '@/utils/haptic/trigger';
// Re-export all haptic functionality from the mobile SDK
export {
buttonTap,
cancelTap,
confirmTap,
feedbackProgress,
feedbackSuccess,
feedbackUnsuccessful,
impactLight,
impactMedium,
loadingScreenProgress,
notificationError,
notificationSuccess,
notificationWarning,
selectionChange,
triggerFeedback,
} from '@selfxyz/mobile-sdk-alpha';
export type { HapticOptions, HapticType } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -2,25 +2,6 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
export type HapticOptions = {
enableVibrateFallback?: boolean;
ignoreAndroidSystemSettings?: boolean;
pattern?: number[];
increaseIosIntensity?: boolean;
};
export type HapticType =
| 'selection'
| 'impactLight'
| 'impactMedium'
| 'impactHeavy'
| 'notificationSuccess'
| 'notificationWarning'
| 'notificationError';
export const defaultOptions: HapticOptions = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
pattern: [50, 100, 50],
increaseIosIntensity: true,
};
// Re-export types and defaults from the mobile SDK
export type { HapticOptions, HapticType } from '@selfxyz/mobile-sdk-alpha';
export { defaultOptions } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -2,39 +2,5 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Platform, Vibration } from 'react-native';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import type { HapticOptions, HapticType } from '@/utils/haptic/shared';
import { defaultOptions } from '@/utils/haptic/shared';
/**
* Triggers haptic feedback or vibration based on platform.
* @param type - The haptic feedback type.
* @param options - Custom options (optional).
*/
export const triggerFeedback = (
type: HapticType | 'custom',
options: HapticOptions = {},
) => {
const mergedOptions = { ...defaultOptions, ...options };
if (Platform.OS === 'ios' && type !== 'custom') {
if (mergedOptions.increaseIosIntensity) {
if (type === 'impactLight') {
type = 'impactMedium';
} else if (type === 'impactMedium') {
type = 'impactHeavy';
}
}
ReactNativeHapticFeedback.trigger(type, {
enableVibrateFallback: mergedOptions.enableVibrateFallback,
ignoreAndroidSystemSettings: mergedOptions.ignoreAndroidSystemSettings,
});
} else {
if (mergedOptions.pattern) {
Vibration.vibrate(mergedOptions.pattern, false);
} else {
Vibration.vibrate(100);
}
}
};
// Re-export triggerFeedback from the mobile SDK
export { triggerFeedback } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -2,29 +2,5 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { HapticOptions, HapticType } from '@/utils/haptic/shared';
import { defaultOptions } from '@/utils/haptic/shared';
/**
* Triggers haptic feedback or vibration based on platform.
* @param type - The haptic feedback type. (only here for compatibility, not used in web)
* @param options - Custom options (optional).
*/
export const triggerFeedback = (
_type: HapticType | 'custom',
options: HapticOptions = {},
) => {
const mergedOptions = { ...defaultOptions, ...options };
// Check if Vibration API is available
if (!navigator.vibrate) {
console.warn('Vibration API not supported in this browser');
return;
}
if (mergedOptions.pattern) {
navigator.vibrate(mergedOptions.pattern);
} else {
navigator.vibrate(100);
}
};
// Re-export triggerFeedback from the mobile SDK
export { triggerFeedback } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -6,6 +6,7 @@ import { PermissionsAndroid, Platform } from 'react-native';
import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
import messaging from '@react-native-firebase/messaging';
import { useSettingStore } from '@/stores/settingStore';
import type { DeviceTokenRegistration } from '@/utils/notifications/notificationService.shared';
import {
API_URL,
@@ -37,6 +38,61 @@ const error = (...args: unknown[]) => {
export { getStateMessage };
/**
* Check if notifications are ready on iOS (APNs token registered with FCM)
* @returns true if ready, false otherwise
*/
export async function isNotificationSystemReady(): Promise<{
ready: boolean;
message: string;
}> {
try {
// Check permissions first
const authStatus = await messaging().hasPermission();
const hasPermission =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (!hasPermission) {
return {
ready: false,
message:
'Notification permissions not granted. Please enable notifications in Settings.',
};
}
// Check if FCM token is available (ensures APNs is registered on iOS)
const token = await messaging().getToken();
if (!token) {
return {
ready: false,
message:
Platform.OS === 'ios'
? 'APNs token not registered yet. Try restarting the app or check your network connection.'
: 'FCM token not available. Check your network connection.',
};
}
log(
'Notification system ready with token:',
token.substring(0, 10) + '...',
);
return {
ready: true,
message: 'Notification system is ready',
};
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
error('Failed to check notification system readiness:', errorMessage);
return {
ready: false,
message: `Error: ${errorMessage}`,
};
}
}
export async function registerDeviceToken(
sessionId: string,
deviceToken?: string,
@@ -132,3 +188,124 @@ export function setupNotifications(): () => void {
return unsubscribeForeground;
}
/**
* Subscribe to FCM topics client-side
*
* IMPORTANT: On iOS, this requires APNs token to be registered with FCM first.
* We ensure this by getting the FCM token before subscribing to topics.
*
* @param topics Array of topic names to subscribe to
* @returns Object with successes and failures arrays
*/
export async function subscribeToTopics(topics: string[]): Promise<{
successes: string[];
failures: Array<{ topic: string; error: string }>;
}> {
const successes: string[] = [];
const failures: Array<{ topic: string; error: string }> = [];
try {
// CRITICAL FOR iOS: Get FCM token first to ensure APNs is registered
// Without this, topic subscriptions silently fail on iOS
const fcmToken = await messaging().getToken();
if (!fcmToken) {
const errorMsg =
'No FCM token available. Cannot subscribe to topics without valid token.';
error(errorMsg);
return {
successes: [],
failures: topics.map(topic => ({ topic, error: errorMsg })),
};
}
log('FCM token available, proceeding with topic subscriptions...');
// iOS: Wait a moment for APNs registration to complete
if (Platform.OS === 'ios') {
await new Promise(resolve => setTimeout(resolve, 1000));
log('APNs registration delay complete');
}
for (const topic of topics) {
try {
await messaging().subscribeToTopic(topic);
log(`Successfully subscribed to topic: ${topic}`);
successes.push(topic);
// Track subscription in store
useSettingStore.getState().addSubscribedTopic(topic);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
error(`Failed to subscribe to topic ${topic}:`, errorMessage);
failures.push({ topic, error: errorMessage });
}
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
error('Failed to initialize topic subscription:', errorMessage);
return {
successes: [],
failures: topics.map(topic => ({
topic,
error: `Initialization failed: ${errorMessage}`,
})),
};
}
return { successes, failures };
}
/**
* Unsubscribe from FCM topics client-side
* @param topics Array of topic names to unsubscribe from
* @returns Object with successes and failures arrays
*/
export async function unsubscribeFromTopics(topics: string[]): Promise<{
successes: string[];
failures: Array<{ topic: string; error: string }>;
}> {
const successes: string[] = [];
const failures: Array<{ topic: string; error: string }> = [];
try {
// Ensure FCM token is available (same requirement as subscribe)
const fcmToken = await messaging().getToken();
if (!fcmToken) {
const errorMsg =
'No FCM token available. Cannot unsubscribe from topics without valid token.';
error(errorMsg);
return {
successes: [],
failures: topics.map(topic => ({ topic, error: errorMsg })),
};
}
for (const topic of topics) {
try {
await messaging().unsubscribeFromTopic(topic);
log(`Successfully unsubscribed from topic: ${topic}`);
successes.push(topic);
// Remove from store
useSettingStore.getState().removeSubscribedTopic(topic);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
error(`Failed to unsubscribe from topic ${topic}:`, errorMessage);
failures.push({ topic, error: errorMessage });
}
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
error('Failed to initialize topic unsubscription:', errorMessage);
return {
successes: [],
failures: topics.map(topic => ({
topic,
error: `Initialization failed: ${errorMessage}`,
})),
};
}
return { successes, failures };
}

View File

@@ -38,6 +38,50 @@ export async function getFCMToken(): Promise<string | null> {
// TODO: web handle notifications better. this file is more or less a fancy placeholder
export { getStateMessage };
/**
* Check if notifications are ready (web stub)
* @returns readiness status
*/
export async function isNotificationSystemReady(): Promise<{
ready: boolean;
message: string;
}> {
try {
if (!('Notification' in window)) {
return {
ready: false,
message: 'This browser does not support notifications',
};
}
if (Notification.permission === 'granted') {
return {
ready: true,
message: 'Notification system is ready',
};
}
if (Notification.permission === 'denied') {
return {
ready: false,
message:
'Notification permissions denied. Please enable them in browser settings.',
};
}
return {
ready: false,
message: 'Notification permissions not requested yet',
};
} catch (error) {
console.error('Failed to check notification readiness:', error);
return {
ready: false,
message: error instanceof Error ? error.message : 'Unknown error',
};
}
}
export async function registerDeviceToken(
sessionId: string,
deviceToken?: string,
@@ -144,3 +188,49 @@ export function setupNotifications(): () => void {
console.log('Web notification service cleanup');
};
}
/**
* Subscribe to FCM topics client-side (web stub)
* @param topics Array of topic names to subscribe to
* @returns Object with successes and failures arrays
*/
export async function subscribeToTopics(topics: string[]): Promise<{
successes: string[];
failures: Array<{ topic: string; error: string }>;
}> {
console.warn(
'FCM topic subscription is not fully implemented for web. Topics:',
topics,
);
// For web, you might want to implement this by calling your backend API
// or using Firebase Web SDK
return {
successes: [],
failures: topics.map(topic => ({
topic,
error: 'Web topic subscription not implemented',
})),
};
}
/**
* Unsubscribe from FCM topics client-side (web stub)
* @param topics Array of topic names to unsubscribe from
* @returns Object with successes and failures arrays
*/
export async function unsubscribeFromTopics(topics: string[]): Promise<{
successes: string[];
failures: Array<{ topic: string; error: string }>;
}> {
console.warn(
'FCM topic unsubscription is not fully implemented for web. Topics:',
topics,
);
return {
successes: [],
failures: topics.map(topic => ({
topic,
error: 'Web topic unsubscription not implemented',
})),
};
}

View File

@@ -2,6 +2,14 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
// Mock ConfirmIdentificationScreen to avoid PixelRatio issues
jest.mock(
'@selfxyz/mobile-sdk-alpha/onboarding/confirm-identification',
() => ({
ConfirmIdentificationScreen: ({ children }: any) => children,
}),
);
describe('navigation', () => {
it('should have the correct navigation screens', () => {
const navigationScreens = require('@/navigation').navigationScreens;
@@ -52,6 +60,7 @@ describe('navigation', () => {
'Settings',
'ShowRecoveryPhrase',
'Splash',
'WebView',
]);
});
});

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
// Mock ConfirmIdentificationScreen to avoid PixelRatio issues
import React, { type ReactNode } from 'react';
import { renderHook } from '@testing-library/react-native';
@@ -9,6 +10,13 @@ import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { SelfClientProvider } from '@/providers/selfClientProvider';
jest.mock(
'@selfxyz/mobile-sdk-alpha/onboarding/confirm-identification',
() => ({
ConfirmIdentificationScreen: ({ children }: any) => children,
}),
);
describe('SelfClientProvider', () => {
it('memoises the client instance', () => {
const wrapper = ({ children }: { children: ReactNode }) => (

View File

@@ -0,0 +1,137 @@
// 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 { Linking } from 'react-native';
import { render, screen, waitFor } from '@testing-library/react-native';
import { WebViewScreen } from '@/screens/shared/WebViewScreen';
jest.mock('react-native-webview', () => {
const React = require('react');
const { View } = require('react-native');
const MockWebView = React.forwardRef((props: any, _ref) => {
return React.createElement(View, { testID: 'webview', ...props });
});
MockWebView.displayName = 'MockWebView';
return {
__esModule: true,
default: MockWebView,
WebView: MockWebView,
};
});
describe('WebViewScreen URL sanitization and navigation interception', () => {
const createProps = (initialUrl?: string, title?: string) => {
return {
navigation: {
goBack: jest.fn(),
canGoBack: jest.fn(() => true),
} as any,
route: {
key: 'WebView-1',
name: 'WebView',
params: initialUrl
? { url: initialUrl, title }
: { url: 'https://self.xyz', title },
} as any,
};
};
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.resetAllMocks();
(console.error as jest.Mock).mockRestore?.();
});
it('sanitizes initial non-http(s) url and uses default', () => {
render(<WebViewScreen {...createProps('intent://foo')} />);
const webview = screen.getByTestId('webview');
expect(webview.props.source).toEqual({ uri: 'https://self.xyz' });
// Title falls back to currentUrl (uppercase via NavBar), i.e., defaultUrl
// We can't easily select NavBar text here without its internals; instead,
// verify current source reflects the defaultUrl which the title derives from
});
it('keeps currentUrl unchanged on non-http(s) navigation update', () => {
render(<WebViewScreen {...createProps('http://example.com')} />);
const webview = screen.getByTestId('webview');
// simulate a navigation update with disallowed scheme
webview.props.onNavigationStateChange?.({
url: 'intent://foo',
canGoBack: true,
canGoForward: false,
navigationType: 'other',
title: undefined,
});
// Source remains the initial http URL since non-http(s) updates are ignored for currentUrl
expect(webview.props.source).toEqual({ uri: 'http://example.com' });
});
it('allows http(s) navigation via onShouldStartLoadWithRequest', () => {
render(<WebViewScreen {...createProps('https://example.com')} />);
const webview = screen.getByTestId('webview');
const allowed = webview.props.onShouldStartLoadWithRequest?.({
url: 'https://example.org',
});
expect(allowed).toBe(true);
});
it('opens allowed external schemes externally and blocks in WebView (mailto, tel)', async () => {
jest.spyOn(Linking, 'canOpenURL').mockResolvedValue(true as any);
const openSpy = jest
.spyOn(Linking, 'openURL')
.mockResolvedValue(undefined as any);
render(<WebViewScreen {...createProps('https://self.xyz')} />);
const webview = screen.getByTestId('webview');
const resultMailto = await webview.props.onShouldStartLoadWithRequest?.({
url: 'mailto:test@example.com',
});
expect(resultMailto).toBe(false);
await waitFor(() =>
expect(openSpy).toHaveBeenCalledWith('mailto:test@example.com'),
);
const resultTel = await webview.props.onShouldStartLoadWithRequest?.({
url: 'tel:+123456789',
});
expect(resultTel).toBe(false);
await waitFor(() => expect(openSpy).toHaveBeenCalledWith('tel:+123456789'));
});
it('blocks disallowed external schemes and does not attempt to open', async () => {
const canOpenSpy = jest.spyOn(Linking, 'canOpenURL');
const openSpy = jest.spyOn(Linking, 'openURL');
render(<WebViewScreen {...createProps('https://self.xyz')} />);
const webview = screen.getByTestId('webview');
const result = await webview.props.onShouldStartLoadWithRequest?.({
url: 'ftp://example.com',
});
expect(result).toBe(false);
expect(canOpenSpy).not.toHaveBeenCalled();
expect(openSpy).not.toHaveBeenCalled();
});
it('scrubs error log wording when external open fails', async () => {
jest.spyOn(Linking, 'canOpenURL').mockResolvedValue(true as any);
jest.spyOn(Linking, 'openURL').mockRejectedValue(new Error('boom'));
render(<WebViewScreen {...createProps('https://self.xyz')} />);
const webview = screen.getByTestId('webview');
const result = await webview.props.onShouldStartLoadWithRequest?.({
url: 'mailto:test@example.com',
});
expect(result).toBe(false);
await waitFor(() => expect(console.error).toHaveBeenCalled());
const [msg] = (console.error as jest.Mock).mock.calls[0];
expect(String(msg)).toContain('Failed to open externally');
expect(String(msg)).not.toMatch(/Failed to open URL externally/);
});
});

View File

@@ -1,10 +1,10 @@
{
"ios": {
"build": 179,
"build": 181,
"lastDeployed": "2025-10-07T05:58:42Z"
},
"android": {
"build": 110,
"build": 111,
"lastDeployed": "2025-10-01T08:00:07Z"
}
}

View File

@@ -69,6 +69,7 @@ REGISTER_ID_CIRCUITS=(
"register_id_sha512_sha512_sha512_ecdsa_secp521r1:true"
"register_id_sha512_sha512_sha512_rsa_65537_4096:true"
"register_id_sha512_sha512_sha512_rsapss_65537_64_2048:true"
"register_id_sha512_sha512_sha256_rsapss_65537_32_2048:true"
)
REGISTER_AADHAAR_CIRCUITS=(

View File

@@ -105,6 +105,7 @@ REGISTER_ID_CIRCUITS=(
"register_id_sha512_sha512_sha512_ecdsa_secp521r1"
"register_id_sha512_sha512_sha512_rsa_65537_4096"
"register_id_sha512_sha512_sha512_rsapss_65537_64_2048"
"register_id_sha512_sha512_sha256_rsapss_65537_32_2048"
)
REGISTER_AADHAAR_CIRCUITS=(

View File

@@ -57,6 +57,7 @@ export {
commonNames,
countries,
countryCodes,
getCountryISO2,
} from './src/constants/index.js';
// Type exports

View File

@@ -35,4 +35,10 @@ export {
} from './constants.js';
// Re-export from other constant files
export { alpha2ToAlpha3, alpha3ToAlpha2, commonNames, countries } from './countries.js';
export {
alpha2ToAlpha3,
alpha3ToAlpha2,
commonNames,
countries,
getCountryISO2,
} from './countries.js';

View File

@@ -1,6 +1,7 @@
import { ethers } from 'ethers';
import forge from 'node-forge';
import { PCR0_MANAGER_ADDRESS, RPC_URL } from 'src/constants/constants.js';
import { PCR0_MANAGER_ADDRESS, RPC_URL } from '../constants/constants.js';
const GCP_ROOT_CERT = `
-----BEGIN CERTIFICATE-----

View File

@@ -40,6 +40,14 @@ export function brutforceSignatureAlgorithm(passportData: PassportData) {
};
}
}
const hashAlgorithm = brutforceHashAlgorithm(passportData, 'rsa');
if (hashAlgorithm) {
return {
signatureAlgorithm: 'rsa',
hashAlgorithm: hashAlgorithm,
saltLength: 0,
};
}
}
function brutforceHashAlgorithm(

View File

@@ -108,6 +108,13 @@ module.exports = {
'sort-exports/sort-exports': 'off',
},
},
{
// Disable export sorting for files with dependency issues
files: ['src/haptic/index.ts'],
rules: {
'sort-exports/sort-exports': 'off',
},
},
{
// Allow require imports only in the NFC decoder shim that conditionally imports node:util
files: ['src/processing/nfc.ts'],

View File

@@ -96,3 +96,9 @@ yarn-error.log
# Maestro
maestro-results.xml
# Exception: Include AAR files in dist/android/
!dist/android/*.aar
android/src
android/libs

View File

@@ -0,0 +1,16 @@
# 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.
android/src/
android/build.gradle.source
android/gradle.properties
android/proguard-rules.pro
android/libs/
# Exclude build artifacts
android/build/
# Keep only:
# - android/build.gradle (prebuilt wrapper)
# - dist/android/*.aar (prebuilt AAR files)

View File

@@ -1,2 +1,7 @@
dist
node_modules
src/animations/
**/*.xcframework/**
**/.build/**
**/OpenSSL.xcframework/**
**/SelfSDK.xcframework/**

View File

@@ -209,8 +209,8 @@ selfClient.on(SdkEvents.DOCUMENT_COUNTRY_SELECTED, payload => {
```ts
selfClient.on(SdkEvents.DOCUMENT_TYPE_SELECTED, payload => {
// payload: { documentType: string, documentName: string, countryCode: string, countryName: string }
console.log(`Document selected: ${payload.documentName} from ${payload.countryName}`);
// payload: { documentType: string, documentName: string, countryCode: string }
console.log(`Document selected: ${payload.documentName} from ${payload.countryCode}`);
});
```

View File

@@ -26,6 +26,8 @@ allprojects {
mavenCentral()
maven { url 'https://jitpack.io' }
maven {
// When building from SDK: ../../../node_modules
// $rootDir is packages/mobile-sdk-alpha/android
url("$rootDir/../../../node_modules/react-native/android")
}
maven {
@@ -64,6 +66,16 @@ android {
}
}
// Configure AAR generation for split AAB support
splits {
abi {
enable true
reset()
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
universalApk false
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@@ -109,13 +121,17 @@ dependencies {
// NFC and Passport Reading dependencies
// implementation 'org.jmrtd:jmrtd:1.7.4'
implementation 'org.jmrtd:jmrtd:0.7.35'
// implementation 'org.jmrtd:jmrtd:0.7.35'
implementation 'org.jmrtd:jmrtd:0.8.3'
implementation 'net.sf.scuba:scuba-sc-android:0.0.23'
// Bouncy Castle for cryptography
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
implementation 'org.bouncycastle:bcprov-jdk18on:1.82'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.82'
implementation ('org.ejbca.cvc:cert-cvc:1.4.13'){
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
}
// Commons IO for utilities
implementation 'commons-io:commons-io:2.11.0'
@@ -153,6 +169,22 @@ dependencies {
implementation 'com.google.guava:guava:31.1-android'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'com.google.android.material:material:1.9.0'
implementation 'com.github.mhshams:jnbis:2.0.2'
}
// Task to copy AAR files to dist directory for prebuilt distribution
task copyAarToDist(type: Copy) {
from 'build/outputs/aar'
into '../dist/android'
include '*.aar'
}
// Ensure AAR is copied after assembleRelease
afterEvaluate {
tasks.named('assembleRelease').configure {
finalizedBy copyAarToDist
}
}

View File

@@ -0,0 +1,186 @@
buildscript {
ext {
buildToolsVersion = "35.0.0"
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
ndkVersion = "27.0.12077973"
kotlinVersion = "1.9.24"
}
repositories {
google()
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath("com.android.tools.build:gradle:8.1.0")
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
maven {
// When building from SDK: ../../../node_modules
// $rootDir is packages/mobile-sdk-alpha/android
url("$rootDir/../../../node_modules/react-native/android")
}
maven {
url("$rootDir/../../../node_modules/jsc-android/dist")
}
}
configurations.configureEach {
resolutionStrategy.dependencySubstitution {
substitute(platform(module('com.gemalto.jp2:jp2-android'))) using module('com.github.Tgo1014:JP2ForAndroid:1.0.4')
}
resolutionStrategy.force 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
ndkVersion rootProject.ext.ndkVersion
namespace "com.selfxyz.selfSDK"
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// Configure AAR generation for split AAB support
splits {
abi {
enable true
reset()
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
universalApk false
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
// sourceCompatibility JavaVersion.VERSION_17
// targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
// jvmTarget = '1.8'
}
lintOptions {
abortOnError false
}
sourceSets {
main {
java.srcDirs = ['src/main/java']
res.srcDirs = ['src/main/res']
}
}
buildFeatures {
viewBinding true
}
packagingOptions {
exclude 'META-INF/proguard/androidx-annotations.pro'
exclude 'META-INF/androidx.exifinterface_exifinterface.version'
pickFirst '**/libc++_shared.so'
pickFirst '**/libjsc.so'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
// React Native
implementation 'com.facebook.react:react-native:+'
// NFC and Passport Reading dependencies
// implementation 'org.jmrtd:jmrtd:1.7.4'
implementation 'org.jmrtd:jmrtd:0.7.35'
implementation 'net.sf.scuba:scuba-sc-android:0.0.23'
// Bouncy Castle for cryptography
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
// Commons IO for utilities
implementation 'commons-io:commons-io:2.11.0'
// OkHttp for network operations
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// Gson for JSON handling
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.2'
implementation "com.github.fotoapparat:fotoapparat:2.7.0"
implementation 'androidx.multidex:multidex:2.0.1'
// RxJava dependencies
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
// React Native dependencies
implementation 'com.facebook.react:react-android'
implementation 'com.facebook.react:react-native:+'
// MLKit dependencies
implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.2'
implementation 'com.google.mlkit:text-recognition:16.0.0'
// Camera dependencies
implementation "androidx.camera:camera-core:1.3.2"
implementation "androidx.camera:camera-camera2:1.3.2"
implementation "androidx.camera:camera-lifecycle:1.3.2"
implementation "androidx.camera:camera-view:1.3.2"
// Utility dependencies
implementation 'com.google.guava:guava:31.1-android'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'com.google.android.material:material:1.9.0'
implementation 'com.github.mhshams:jnbis:2.0.2'
}
// Task to copy AAR files to dist directory for prebuilt distribution
task copyAarToDist(type: Copy) {
from 'build/outputs/aar'
into '../dist/android'
include '*.aar'
}
// Ensure AAR is copied after assembleRelease
afterEvaluate {
tasks.named('assembleRelease').configure {
finalizedBy copyAarToDist
}
}

View File

@@ -0,0 +1,64 @@
// 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.
// Mobile SDK Alpha - Bill of Materials (BOM)
// This file provides all required dependencies for the mobile-sdk-alpha AAR
// Usage: apply from: 'path/to/mobile-sdk-alpha-bom.gradle'
// Enable ViewBinding in the consuming app
android {
buildFeatures {
viewBinding true
}
}
dependencies {
// Mobile SDK Alpha AAR
// Detect if we're in monorepo or published package
def sdkRoot = file("$projectDir/../../../mobile-sdk-alpha").exists() ?
"$projectDir/../../../mobile-sdk-alpha" : // Monorepo (workspace dependency)
"$projectDir/../../node_modules/@selfxyz/mobile-sdk-alpha" // Published package
implementation files("${sdkRoot}/dist/android/mobile-sdk-alpha-release.aar")
// All required dependencies for mobile-sdk-alpha AAR
// Core JMRTD library for passport reading
implementation 'org.jmrtd:jmrtd:0.7.35'
implementation 'net.sf.scuba:scuba-sc-android:0.0.23'
// Cryptography libraries
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
// Utility libraries
implementation 'commons-io:commons-io:2.11.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.google.code.gson:gson:2.10.1'
// ML Kit for text recognition
implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.2'
implementation 'com.google.mlkit:text-recognition:16.0.0'
// Camera functionality
implementation "com.github.fotoapparat:fotoapparat:2.7.0"
implementation "androidx.camera:camera-core:1.3.2"
implementation "androidx.camera:camera-camera2:1.3.2"
implementation "androidx.camera:camera-lifecycle:1.3.2"
implementation "androidx.camera:camera-view:1.3.2"
// Android support libraries
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'com.google.android.material:material:1.9.0'
// Reactive programming
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
// Additional utilities
implementation 'com.google.guava:guava:31.1-android'
implementation 'com.github.mhshams:jnbis:2.0.2'
}

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.selfxyz.selfSDK">
<!-- NFC permissions -->
<uses-permission android:name="android.permission.NFC" />
<uses-feature
android:name="android.hardware.nfc"
android:required="false" />
<!-- Network permissions for certificate validation -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Camera permissions -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-permission android:name="android.permission.CAMERA_EXPOSURE" />
<!-- Other permissions that might be needed -->
<uses-permission android:name="android.permission.VIBRATE" />
</manifest>

View File

@@ -1,936 +0,0 @@
/*
* Copyright 2016 - 2022 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
package com.selfxyz.selfSDK
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.graphics.Bitmap
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.AsyncTask
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Base64
import android.util.Log
import android.widget.EditText
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import net.sf.scuba.smartcards.CardService
import org.apache.commons.io.IOUtils
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.cms.ContentInfo
import org.bouncycastle.asn1.cms.SignedData
import org.bouncycastle.asn1.ASN1Primitive
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1Set
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.icao.DataGroupHash;
import org.bouncycastle.asn1.icao.LDSSecurityObject;
import org.bouncycastle.asn1.x509.Certificate
import org.bouncycastle.jce.spec.ECNamedCurveSpec
import org.bouncycastle.jce.interfaces.ECPublicKey
import org.jmrtd.BACKey
import org.jmrtd.BACKeySpec
import org.jmrtd.AccessKeySpec
import org.jmrtd.PassportService
import org.jmrtd.lds.CardAccessFile
import org.jmrtd.lds.ChipAuthenticationPublicKeyInfo
import org.jmrtd.lds.PACEInfo
import org.jmrtd.PACEKeySpec
import org.jmrtd.lds.SODFile
import org.jmrtd.lds.SecurityInfo
import org.jmrtd.lds.icao.DG14File
import org.jmrtd.lds.icao.DG1File
import org.jmrtd.lds.icao.DG2File
import org.jmrtd.lds.iso19794.FaceImageInfo
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.DataInputStream
import java.io.InputStream
import java.io.IOException
import java.io.FileOutputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.security.KeyStore
import java.security.MessageDigest
import java.security.Signature
import java.security.cert.CertPathValidator
import java.security.cert.CertificateFactory
import java.security.cert.PKIXParameters
import java.security.cert.X509Certificate
import java.security.spec.MGF1ParameterSpec
import java.security.spec.PSSParameterSpec
import java.text.ParseException
import java.security.interfaces.RSAPublicKey
import java.text.SimpleDateFormat
import java.util.*
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import com.google.gson.Gson;
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReadableNativeMap
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.Arguments
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.Callback
// import net.sf.scuba.smartcards.APDUListener
// import net.sf.scuba.smartcards.APDUEvent
// import net.sf.scuba.smartcards.CommandAPDU
// import net.sf.scuba.smartcards.ResponseAPDU
// import org.jmrtd.WrappedAPDUEvent
object Messages {
const val SCANNING = "Scanning....."
const val STOP_MOVING = "Stop moving....."
const val AUTH = "Auth....."
const val COMPARING = "Comparing....."
const val COMPLETED = "Scanning completed"
const val RESET = ""
const val PACE_STARTED = "PACE started"
const val PACE_SUCCEEDED = "PACE succeeded"
const val PACE_FAILED = "PACE failed"
const val BAC_STARTED = "BAC started"
const val BAC_SUCCEEDED = "BAC succeeded"
const val BAC_FAILED = "BAC failed"
const val READING_COM = "Reading COM....."
const val READING_DG1 = "Reading DG1....."
const val READING_DG1_SUCCEEDED = "Reading DG1 succeeded"
const val READING_DG2 = "Reading DG2....."
const val READING_DG2_SUCCEEDED = "Reading DG2 succeeded"
const val READING_SOD = "Reading SOD....."
const val READING_SOD_SUCCEEDED = "Reading SOD succeeded"
const val READING_DG14 = "Reading DG14....."
const val CHIP_AUTH_SUCCEEDED = "Chip authentication succeeded"
}
class Response(json: String) : JSONObject(json) {
val type: String? = this.optString("type")
val data = this.optJSONArray("data")
?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } // returns an array of JSONObject
?.map { Foo(it.toString()) } // transforms each JSONObject of the array into Foo
}
class Foo(json: String) : JSONObject(json) {
val id = this.optInt("id")
val title: String? = this.optString("title")
}
class RNSelfPassportReaderModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener {
// private var passportNumberFromIntent = false
// private var encodePhotoToBase64 = false
private var scanPromise: Promise? = null
private var opts: ReadableMap? = null
data class Data(val id: String, val digest: String, val signature: String, val publicKey: String)
data class PassportData(
val dg1File: DG1File,
val dg2File: DG2File,
val sodFile: SODFile
)
interface DataCallback {
fun onDataReceived(data: String)
}
init {
instance = this
reactContext.addLifecycleEventListener(this)
}
override fun onCatalystInstanceDestroy() {
reactContext.removeLifecycleEventListener(this)
}
override fun getName(): String {
return "SelfPassportReader"
}
fun sendDataToJS(passportData: PassportData) {
val gson = Gson()
val dataMap = Arguments.createMap()
dataMap.putString("passportData", gson.toJson(passportData))
// Add all the other fields of the YourDataClass object to the map
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("ReadDataTaskCompleted", dataMap)
}
@ReactMethod
fun scan(opts: ReadableMap, promise: Promise) {
// Log scan start
logAnalyticsEvent("nfc_scan_started", mapOf(
"use_can" to (opts.getBoolean(PARAM_USE_CAN) ?: false),
"has_document_number" to (!opts.getString(PARAM_DOC_NUM).isNullOrEmpty()),
"has_can_number" to (!opts.getString(PARAM_CAN).isNullOrEmpty()),
"platform" to "android"
))
eventMessageEmitter(Messages.SCANNING)
val mNfcAdapter = NfcAdapter.getDefaultAdapter(reactApplicationContext)
// val mNfcAdapter = NfcAdapter.getDefaultAdapter(this.reactContext)
if (mNfcAdapter == null) {
logAnalyticsError("nfc_not_supported", "NFC chip reading not supported")
promise.reject("E_NOT_SUPPORTED", "NFC chip reading not supported")
return
}
if (!mNfcAdapter.isEnabled) {
logAnalyticsError("nfc_not_enabled", "NFC chip reading not enabled")
promise.reject("E_NOT_ENABLED", "NFC chip reading not enabled")
return
}
if (scanPromise != null) {
logAnalyticsError("nfc_already_scanning", "Already running a scan")
promise.reject("E_ONE_REQ_AT_A_TIME", "Already running a scan")
return
}
this.opts = opts
this.scanPromise = promise
Log.d("RNSelfPassportReaderModule", "opts set to: " + opts.toString())
}
private fun resetState() {
scanPromise = null
opts = null
}
override fun onHostDestroy() {
resetState()
}
override fun onHostResume() {
val mNfcAdapter = NfcAdapter.getDefaultAdapter(this.reactContext)
mNfcAdapter?.let {
val activity = reactApplicationContext.currentActivity
activity?.let {
val intent = Intent(it.applicationContext, it.javaClass)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
val pendingIntent = PendingIntent.getActivity(it, 0, intent, PendingIntent.FLAG_MUTABLE) // PendingIntent.FLAG_UPDATE_CURRENT
val filter = arrayOf(arrayOf(IsoDep::class.java.name))
mNfcAdapter.enableForegroundDispatch(it, pendingIntent, null, filter)
}
}
}
override fun onHostPause() {
val mNfcAdapter = NfcAdapter.getDefaultAdapter(this.reactContext)
mNfcAdapter?.disableForegroundDispatch(reactApplicationContext.currentActivity)
}
fun receiveIntent(intent: Intent) {
Log.d("RNSelfPassportReaderModule", "receiveIntent: " + intent.action)
if (scanPromise == null) {
Log.w("RNSelfPassportReaderModule", "No active scan - ignoring NFC intent")
return
}
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
val tag: Tag? = intent.extras?.getParcelable(NfcAdapter.EXTRA_TAG)
if (tag?.techList?.contains("android.nfc.tech.IsoDep") == true) {
val passportNumber = opts?.getString(PARAM_DOC_NUM)
val expirationDate = opts?.getString(PARAM_DOE)
val birthDate = opts?.getString(PARAM_DOB)
val cardAccessNumber = opts?.getString(PARAM_CAN)
val useCan = opts?.getBoolean(PARAM_USE_CAN) ?: false
if (useCan && !cardAccessNumber.isNullOrEmpty()) {
val paceKey: PACEKeySpec = PACEKeySpec.createCANKey(cardAccessNumber)
ReadTask(IsoDep.get(tag), paceKey).execute()
}
else if (!passportNumber.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && !birthDate.isNullOrEmpty()) {
val bacKey: BACKeySpec = BACKey(passportNumber, birthDate, expirationDate)
ReadTask(IsoDep.get(tag), bacKey).execute()
}
}
}
}
private fun toBase64(bitmap: Bitmap, quality: Int): String {
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, byteArrayOutputStream)
val byteArray = byteArrayOutputStream.toByteArray()
return JPEG_DATA_URI_PREFIX + Base64.encodeToString(byteArray, Base64.NO_WRAP)
}
@SuppressLint("StaticFieldLeak")
private inner class ReadTask(
private val isoDep: IsoDep,
private val authKey: AccessKeySpec
) : AsyncTask<Void?, Void?, Exception?>() {
private lateinit var dg1File: DG1File
private lateinit var dg2File: DG2File
private lateinit var dg14File: DG14File
private lateinit var sodFile: SODFile
private var imageBase64: String? = null
private var bitmap: Bitmap? = null
private var chipAuthSucceeded = false
private var passiveAuthSuccess = false
private lateinit var dg14Encoded: ByteArray
override fun doInBackground(vararg params: Void?): Exception? {
try {
logAnalyticsEvent("nfc_reading_started")
eventMessageEmitter(Messages.STOP_MOVING)
isoDep.timeout = 20000
Log.e("MY_LOGS", "This should obvsly log")
val cardService = try {
CardService.getInstance(isoDep)
} catch (e: Exception) {
logAnalyticsError("nfc_card_service_failed", "Failed to get CardService instance: ${e.message}")
Log.e("MY_LOGS", "Failed to get CardService instance", e)
throw e
}
try {
cardService.open()
} catch (e: Exception) {
logAnalyticsError("nfc_card_service_open_failed", "Failed to open CardService: ${e.message}")
Log.e("MY_LOGS", "Failed to open CardService", e)
isoDep.close()
Thread.sleep(500)
isoDep.connect()
cardService.open()
}
Log.e("MY_LOGS", "cardService opened")
logAnalyticsEvent("nfc_card_service_opened")
val service = PassportService(
cardService,
PassportService.NORMAL_MAX_TRANCEIVE_LENGTH * 2,
PassportService.DEFAULT_MAX_BLOCKSIZE * 2,
false,
false,
)
// val apduMonitor = APDUMonitor()
// apduMonitor.setupAPDULogging(service)
Log.e("MY_LOGS", "service gotten")
service.open()
Log.e("MY_LOGS", "service opened")
logAnalyticsEvent("nfc_passport_service_opened")
var paceSucceeded = false
var bacSucceeded = false
try {
Log.e("MY_LOGS", "trying to get cardAccessFile...")
val cardAccessFile = CardAccessFile(service.getInputStream(PassportService.EF_CARD_ACCESS))
Log.e("MY_LOGS", "cardAccessFile: ${cardAccessFile}")
val securityInfoCollection = cardAccessFile.securityInfos
for (securityInfo: SecurityInfo in securityInfoCollection) {
if (securityInfo is PACEInfo) {
Log.e("MY_LOGS", "trying PACE...")
eventMessageEmitter(Messages.PACE_STARTED)
val paceKeyToUse: PACEKeySpec? = when (authKey) {
is PACEKeySpec -> authKey
is BACKey -> PACEKeySpec.createMRZKey(authKey)
else -> null
}
if (paceKeyToUse != null) {
service.doPACE(
paceKeyToUse,
securityInfo.objectIdentifier,
PACEInfo.toParameterSpec(securityInfo.parameterId),
null,
)
} else {
throw IllegalStateException("Unsupported auth key for PACE: ${authKey::class.java.simpleName}")
}
Log.d("MY_LOGS", "PACE succeeded")
paceSucceeded = true
logAnalyticsEvent("nfc_pace_succeeded")
eventMessageEmitter(Messages.PACE_SUCCEEDED)
break
}
}
} catch (e: Exception) {
logAnalyticsError("nfc_pace_failed", "PACE authentication failed: ${e.message}")
logAnalyticsEvent("nfc_pace_attempted", mapOf(
"success" to false,
"error_type" to e.javaClass.simpleName
))
Log.w("MY_LOGS", e)
eventMessageEmitter(Messages.PACE_FAILED)
}
if (!paceSucceeded && authKey is BACKeySpec) {
var attempts = 0
val maxAttempts = 3
eventMessageEmitter(Messages.BAC_STARTED)
while (!bacSucceeded && attempts < maxAttempts) {
try {
attempts++
Log.e("MY_LOGS", "BAC attempt $attempts of $maxAttempts")
if (attempts > 1) {
// Wait before retry
Thread.sleep(500)
}
// Try to read EF_COM first
try {
eventMessageEmitter(Messages.READING_COM)
service.getInputStream(PassportService.EF_COM).read()
} catch (e: Exception) {
// EF_COM failed, do BAC
service.doBAC(authKey)
}
bacSucceeded = true
logAnalyticsEvent("nfc_bac_succeeded", mapOf("attempts" to attempts))
logAnalyticsEvent("nfc_bac_attempted", mapOf(
"success" to true,
"attempts" to attempts
))
Log.e("MY_LOGS", "BAC succeeded on attempt $attempts")
eventMessageEmitter(Messages.BAC_SUCCEEDED)
} catch (e: Exception) {
val errClass = e.javaClass.simpleName
val errMsg = e.message ?: ""
logAnalyticsError("nfc_bac_attempt_failed", "BAC attempt $attempts failed: ${e.message}")
logAnalyticsEvent("nfc_bac_attempted", mapOf(
"success" to false,
"attempt" to attempts,
"error_type" to e.javaClass.simpleName
))
Log.e("MY_LOGS", "BAC attempt $attempts failed: $errClass - $errMsg")
if (e is org.jmrtd.CardServiceProtocolException) {
logAnalyticsEvent("nfc_bac_protocol_error", mapOf(
"attempt" to attempts,
"message_contains_sw" to (errMsg.contains("SW = ")),
"message_length" to errMsg.length
))
}
if (attempts == maxAttempts) {
eventMessageEmitter(Messages.BAC_FAILED)
throw e // Re-throw on final attempt
}
}
}
}
if (!paceSucceeded && !bacSucceeded) {
throw IOException("Authentication not established; cannot read data groups")
}
try {
Log.e("MY_LOGS", "Sending select applet command after auth. paceSucceeded=$paceSucceeded, bacSucceeded=$bacSucceeded")
service.sendSelectApplet(true) //we have already checked either paceSucceeded OR bacSucceeded is true. So we should send true to use SecureMessaging
logAnalyticsEvent("nfc_select_applet_succeeded", mapOf(
"pace_succeeded" to paceSucceeded,
"bac_succeeded" to bacSucceeded
))
} catch (e: Exception) {
val msg = e.message ?: ""
logAnalyticsError("nfc_select_applet_failed", "Select applet failed: ${e.message}")
if (msg.contains("6982") || msg.contains("SECURITY STATUS NOT SATISFIED", ignoreCase = true)) {
Log.w(TAG, "Select applet returned 6982; proceeding after established auth")
} else {
throw e
}
}
logAnalyticsEvent("nfc_reading_data_groups")
eventMessageEmitter(Messages.READING_DG1)
logAnalyticsEvent("nfc_reading_dg1_started")
val dg1In = service.getInputStream(PassportService.EF_DG1)
dg1File = DG1File(dg1In)
logAnalyticsEvent("nfc_reading_dg1_completed")
eventMessageEmitter(Messages.READING_DG1_SUCCEEDED)
// eventMessageEmitter("Reading DG2.....")
// val dg2In = service.getInputStream(PassportService.EF_DG2)
// dg2File = DG2File(dg2In)
logAnalyticsEvent("nfc_reading_sod_started")
eventMessageEmitter(Messages.READING_SOD)
val sodIn = service.getInputStream(PassportService.EF_SOD)
sodFile = SODFile(sodIn)
logAnalyticsEvent("nfc_reading_sod_completed")
eventMessageEmitter(Messages.READING_SOD_SUCCEEDED)
// val gson = Gson()
// Log.d(TAG, "============FIRST CONSOLE LOG=============")
// Log.d(TAG, "dg1File: " + gson.toJson(dg1File))
// Log.d(TAG, "dg2File: " + gson.toJson(dg2File))
// Log.d(TAG, "sodFile.docSigningCertificate: ${sodFile.docSigningCertificate}")
// Log.d(TAG, "publicKey: ${sodFile.docSigningCertificate.publicKey}")
// Log.d(TAG, "publicKey: ${sodFile.docSigningCertificate.publicKey.toString()}")
// Log.d(TAG, "publicKey: ${sodFile.docSigningCertificate.publicKey.format}")
// Log.d(TAG, "publicKey: ${Base64.encodeToString(sodFile.docSigningCertificate.publicKey.encoded, Base64.DEFAULT)}")
// Log.d(TAG, "sodFile.docSigningCertificate: ${gson.toJson(sodFile.docSigningCertificate)}")
// Log.d(TAG, "sodFile.dataGroupHashes: ${sodFile.dataGroupHashes}")
// Log.d(TAG, "sodFile.dataGroupHashes: ${gson.toJson(sodFile.dataGroupHashes)}")
// Log.d(TAG, "concatenated: $concatenated")
// Log.d(TAG, "concatenated: ${gson.toJson(concatenated)}")
// Log.d(TAG, "concatenated: ${gson.toJson(concatenated.joinToString("") { "%02x".format(it) })}")
// Log.d(TAG, "sodFile.eContent: ${sodFile.eContent}")
// Log.d(TAG, "sodFile.eContent: ${gson.toJson(sodFile.eContent)}")
// Log.d(TAG, "sodFile.eContent: ${gson.toJson(sodFile.eContent.joinToString("") { "%02x".format(it) })}")
// Log.d(TAG, "sodFile.encryptedDigest: ${sodFile.encryptedDigest}")
// Log.d(TAG, "sodFile.encryptedDigest: ${gson.toJson(sodFile.encryptedDigest)}")
// Log.d(TAG, "sodFile.encryptedDigest: ${gson.toJson(sodFile.encryptedDigest.joinToString("") { "%02x".format(it) })}")
// var id = passportNumberView.text.toString()
// try {
// postData(id, sodFile.eContent.joinToString("") { "%02x".format(it) }, sodFile.encryptedDigest.joinToString("") { "%02x".format(it) }, sodFile.docSigningCertificate.publicKey.toString())
// } catch (e: IOException) {
// e.printStackTrace()
// }
// Log.d(TAG, "============LET'S VERIFY THE SIGNATURE=============")
eventMessageEmitter(Messages.AUTH)
logAnalyticsEvent("nfc_authentication_started")
doChipAuth(service)
doPassiveAuth()
logAnalyticsEvent("nfc_authentication_completed")
// Log.d(TAG, "============SIGNATURE VERIFIED=============")
// sendDataToJS(PassportData(dg1File, dg2File, sodFile))
// Log.d(TAG, "============DATA SENT TO JS=============")
// val allFaceImageInfo: MutableList<FaceImageInfo> = ArrayList()
// dg2File.faceInfos.forEach {
// allFaceImageInfo.addAll(it.faceImageInfos)
// }
// if (allFaceImageInfo.isNotEmpty()) {
// val faceImageInfo = allFaceImageInfo.first()
// val imageLength = faceImageInfo.imageLength
// val dataInputStream = DataInputStream(faceImageInfo.imageInputStream)
// val buffer = ByteArray(imageLength)
// dataInputStream.readFully(buffer, 0, imageLength)
// val inputStream: InputStream = ByteArrayInputStream(buffer, 0, imageLength)
// bitmap = decodeImage(reactContext, faceImageInfo.mimeType, inputStream)
// imageBase64 = Base64.encodeToString(buffer, Base64.DEFAULT)
// }
} catch (e: Exception) {
logAnalyticsError("nfc_reading_failed", "NFC reading failed: ${e.message}")
eventMessageEmitter(Messages.RESET)
return e
}
return null
}
private fun doChipAuth(service: PassportService) {
try {
logAnalyticsEvent("nfc_reading_dg14_started")
eventMessageEmitter(Messages.READING_DG14)
val dg14In = service.getInputStream(PassportService.EF_DG14)
dg14Encoded = IOUtils.toByteArray(dg14In)
val dg14InByte = ByteArrayInputStream(dg14Encoded)
dg14File = DG14File(dg14InByte)
logAnalyticsEvent("nfc_reading_dg14_completed")
val dg14FileSecurityInfo = dg14File.securityInfos
for (securityInfo: SecurityInfo in dg14FileSecurityInfo) {
if (securityInfo is ChipAuthenticationPublicKeyInfo) {
service.doEACCA(
securityInfo.keyId,
ChipAuthenticationPublicKeyInfo.ID_CA_ECDH_AES_CBC_CMAC_256,
securityInfo.objectIdentifier,
securityInfo.subjectPublicKey,
)
chipAuthSucceeded = true
logAnalyticsEvent("nfc_chip_auth_succeeded")
eventMessageEmitter(Messages.CHIP_AUTH_SUCCEEDED)
}
}
} catch (e: Exception) {
logAnalyticsError("nfc_chip_auth_failed", "Chip authentication failed: ${e.message}")
Log.w(TAG, e)
}
}
private fun doPassiveAuth() {
try {
logAnalyticsEvent("nfc_passive_auth_started")
Log.d(TAG, "Starting passive authentication...")
val digest = MessageDigest.getInstance(sodFile.digestAlgorithm)
Log.d(TAG, "Using digest algorithm: ${sodFile.digestAlgorithm}")
val dataHashes = sodFile.dataGroupHashes
val dg14Hash = if (chipAuthSucceeded) digest.digest(dg14Encoded) else ByteArray(0)
val dg1Hash = digest.digest(dg1File.encoded)
// val dg2Hash = digest.digest(dg2File.encoded)
// val gson = Gson()
// Log.d(TAG, "dataHashes " + gson.toJson(dataHashes))
// val hexMap = sodFile.dataGroupHashes.mapValues { (_, value) ->
// value.joinToString("") { "%02x".format(it) }
// }
// Log.d(TAG, "hexMap: ${gson.toJson(hexMap)}")
// Log.d(TAG, "concatenated: $concatenated")
// Log.d(TAG, "concatenated: ${gson.toJson(concatenated)}")
// Log.d(TAG, "concatenated: ${gson.toJson(concatenated.joinToString("") { "%02x".format(it) })}")
// Log.d(TAG, "dg1File.encoded " + gson.toJson(dg1File.encoded))
// Log.d(TAG, "dg1File.encoded.joinToString " + gson.toJson(dg1File.encoded.joinToString("") { "%02x".format(it) }))
// Log.d(TAG, "dg1Hash " + gson.toJson(dg1Hash))
// Log.d(TAG, "dg1Hash.joinToString " + gson.toJson(dg1Hash.joinToString("") { "%02x".format(it) }))
// Log.d(TAG, "dg2File.encoded " + gson.toJson(dg2File.encoded))
// Log.d(TAG, "dg2File.encoded.joinToString " + gson.toJson(dg2File.encoded.joinToString("") { "%02x".format(it) }))
// Log.d(TAG, "dg2Hash " + gson.toJson(dg2Hash))
// Log.d(TAG, "dg2HashjoinToString " + gson.toJson(dg2Hash.joinToString("") { "%02x".format(it) }))
Log.d(TAG, "Comparing data group hashes...")
eventMessageEmitter(Messages.COMPARING)
logAnalyticsEvent("nfc_data_group_hash_verification_started")
// if (Arrays.equals(dg1Hash, dataHashes[1]) && Arrays.equals(dg2Hash, dataHashes[2])
if (Arrays.equals(dg1Hash, dataHashes[1])
&& (!chipAuthSucceeded || Arrays.equals(dg14Hash, dataHashes[14]))) {
Log.d(TAG, "Data group hashes match.")
logAnalyticsEvent("nfc_data_group_hash_verification_succeeded")
val asn1InputStream = ASN1InputStream(getReactApplicationContext().assets.open("masterList"))
val keystore = KeyStore.getInstance(KeyStore.getDefaultType())
keystore.load(null, null)
val cf = CertificateFactory.getInstance("X.509")
var p: ASN1Primitive?
var obj = asn1InputStream.readObject()
while (obj != null) {
p = obj
val asn1 = ASN1Sequence.getInstance(p)
if (asn1 == null || asn1.size() == 0) {
throw IllegalArgumentException("Null or empty sequence passed.")
}
if (asn1.size() != 2) {
throw IllegalArgumentException("Incorrect sequence size: " + asn1.size())
}
val certSet = ASN1Set.getInstance(asn1.getObjectAt(1))
for (i in 0 until certSet.size()) {
val certificate = Certificate.getInstance(certSet.getObjectAt(i))
val pemCertificate = certificate.encoded
val javaCertificate = cf.generateCertificate(ByteArrayInputStream(pemCertificate))
keystore.setCertificateEntry(i.toString(), javaCertificate)
}
obj = asn1InputStream.readObject()
}
val docSigningCertificates = sodFile.docSigningCertificates
Log.d(TAG, "Checking document signing certificates for validity...")
logAnalyticsEvent("nfc_certificate_validation_started", mapOf(
"certificate_count" to docSigningCertificates.size
))
for (docSigningCertificate: X509Certificate in docSigningCertificates) {
docSigningCertificate.checkValidity()
Log.d(TAG, "Certificate: ${docSigningCertificate.subjectDN} is valid.")
}
logAnalyticsEvent("nfc_certificate_validation_succeeded")
val cp = cf.generateCertPath(docSigningCertificates)
val pkixParameters = PKIXParameters(keystore)
pkixParameters.isRevocationEnabled = false
val cpv = CertPathValidator.getInstance(CertPathValidator.getDefaultType())
Log.d(TAG, "Validating certificate path...")
logAnalyticsEvent("nfc_certificate_path_validation_started")
cpv.validate(cp, pkixParameters)
logAnalyticsEvent("nfc_certificate_path_validation_succeeded")
var sodDigestEncryptionAlgorithm = sodFile.docSigningCertificate.sigAlgName
var isSSA = false
if ((sodDigestEncryptionAlgorithm == "SSAwithRSA/PSS")) {
sodDigestEncryptionAlgorithm = "SHA256withRSA/PSS"
isSSA = true
}
val sign = Signature.getInstance(sodDigestEncryptionAlgorithm)
if (isSSA) {
sign.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1))
}
sign.initVerify(sodFile.docSigningCertificate)
sign.update(sodFile.eContent)
logAnalyticsEvent("nfc_signature_verification_started", mapOf(
"algorithm" to sodDigestEncryptionAlgorithm
))
passiveAuthSuccess = sign.verify(sodFile.encryptedDigest)
if (passiveAuthSuccess) {
logAnalyticsEvent("nfc_signature_verification_succeeded")
logAnalyticsEvent("nfc_passive_auth_succeeded")
} else {
logAnalyticsError("nfc_signature_verification_failed", "Signature verification failed")
logAnalyticsError("nfc_passive_auth_failed", "Signature verification failed")
}
Log.d(TAG, "Passive authentication success: $passiveAuthSuccess")
} else {
logAnalyticsError("nfc_passive_auth_failed", "Data group hashes do not match")
}
} catch (e: Exception) {
logAnalyticsError("nfc_passive_auth_failed", "Passive authentication failed: ${e.message}")
eventMessageEmitter(Messages.RESET)
Log.w(TAG, "Exception in passive authentication", e)
}
}
override fun onPostExecute(result: Exception?) {
if (scanPromise == null) return
if (result != null) {
// Log.w(TAG, exceptionStack(result))
if (result is IOException) {
logAnalyticsError("nfc_scan_failed_disconnect", "Lost connection to chip on card")
scanPromise?.reject("E_SCAN_FAILED_DISCONNECT", "Lost connection to chip on card")
} else {
logAnalyticsError("nfc_scan_failed", "Scan failed: ${result.message}")
scanPromise?.reject("E_SCAN_FAILED", result)
}
resetState()
return
}
logAnalyticsEvent("nfc_scan_completed", mapOf(
"chip_auth_succeeded" to chipAuthSucceeded,
"passive_auth_success" to passiveAuthSuccess
))
val mrzInfo = dg1File.mrzInfo
val gson = Gson()
// val signedDataField = SODFile::class.java.getDeclaredField("signedData")
// signedDataField.isAccessible = true
// val signedData = signedDataField.get(sodFile) as SignedData
val eContentAsn1InputStream = ASN1InputStream(sodFile.eContent.inputStream())
// val eContentDecomposed: ASN1Primitive = eContentAsn1InputStream.readObject()
val passport = Arguments.createMap()
passport.putString("mrz", mrzInfo.toString())
passport.putString("signatureAlgorithm", sodFile.docSigningCertificate.sigAlgName) // this one is new
Log.d(TAG, "sodFile.docSigningCertificate: ${sodFile.docSigningCertificate}")
val certificate = sodFile.docSigningCertificate
val certificateBytes = certificate.encoded
val certificateBase64 = Base64.encodeToString(certificateBytes, Base64.DEFAULT)
Log.d(TAG, "certificateBase64: ${certificateBase64}")
passport.putString("documentSigningCertificate", certificateBase64)
val publicKey = sodFile.docSigningCertificate.publicKey
if (publicKey is RSAPublicKey) {
passport.putString("modulus", publicKey.modulus.toString())
} else if (publicKey is ECPublicKey) {
// Handle the elliptic curve public key case
// val w = publicKey.getW()
// passport.putString("publicKeyW", w.toString())
// val ecParams = publicKey.getParams()
// passport.putInt("cofactor", ecParams.getCofactor())
// passport.putString("curve", ecParams.getCurve().toString())
// passport.putString("generator", ecParams.getGenerator().toString())
// passport.putString("order", ecParams.getOrder().toString())
// if (ecParams is ECNamedCurveSpec) {
// passport.putString("curveName", ecParams.getName())
// }
// Old one, probably wrong:
// passport.putString("curveName", (publicKey.parameters as ECNamedCurveSpec).name)
// passport.putString("curveName", (publicKey.parameters.algorithm)) or maybe this
passport.putString("publicKeyQ", publicKey.q.toString())
}
passport.putString("dataGroupHashes", gson.toJson(sodFile.dataGroupHashes))
passport.putString("eContent", gson.toJson(sodFile.eContent))
passport.putString("encryptedDigest", gson.toJson(sodFile.encryptedDigest))
// passport.putString("encapContentInfo", gson.toJson(sodFile.encapContentInfo))
// passport.putString("contentInfo", gson.toJson(sodFile.contentInfo))
passport.putString("digestAlgorithm", gson.toJson(sodFile.digestAlgorithm))
passport.putString("signerInfoDigestAlgorithm", gson.toJson(sodFile.signerInfoDigestAlgorithm))
passport.putString("digestEncryptionAlgorithm", gson.toJson(sodFile.digestEncryptionAlgorithm))
passport.putString("LDSVersion", gson.toJson(sodFile.getLDSVersion()))
passport.putString("unicodeVersion", gson.toJson(sodFile.unicodeVersion))
// Get EncapContent (data group hashes) using reflection in Kotlin
val getENC: Method = SODFile::class.java.getDeclaredMethod("getLDSSecurityObject", SignedData::class.java)
getENC.isAccessible = true
val signedDataField: Field = sodFile::class.java.getDeclaredField("signedData")
signedDataField.isAccessible = true
val signedData: SignedData = signedDataField.get(sodFile) as SignedData
val ldsso: LDSSecurityObject = getENC.invoke(sodFile, signedData) as LDSSecurityObject
passport.putString("encapContent", gson.toJson(ldsso.encoded))
// Convert the document signing certificate to PEM format
val docSigningCert = sodFile.docSigningCertificate
val pemCert = "-----BEGIN CERTIFICATE-----\n" + Base64.encodeToString(docSigningCert.encoded, Base64.DEFAULT) + "-----END CERTIFICATE-----"
passport.putString("documentSigningCertificate", pemCert)
// passport.putString("getDocSigningCertificate", gson.toJson(sodFile.getDocSigningCertificate))
// passport.putString("getIssuerX500Principal", gson.toJson(sodFile.getIssuerX500Principal))
// passport.putString("getSerialNumber", gson.toJson(sodFile.getSerialNumber))
// Another way to get signing time is to get into signedData.signerInfos, then search for the ICO identifier 1.2.840.113549.1.9.5
// passport.putString("signerInfos", gson.toJson(signedData.signerInfos))
// Log.d(TAG, "signedData.digestAlgorithms: ${gson.toJson(signedData.digestAlgorithms)}")
// Log.d(TAG, "signedData.signerInfos: ${gson.toJson(signedData.signerInfos)}")
// Log.d(TAG, "signedData.certificates: ${gson.toJson(signedData.certificates)}")
// var quality = 100
// val base64 = bitmap?.let { toBase64(it, quality) }
// val photo = Arguments.createMap()
// photo.putString("base64", base64 ?: "")
// photo.putInt("width", bitmap?.width ?: 0)
// photo.putInt("height", bitmap?.height ?: 0)
// passport.putMap("photo", photo)
// passport.putString("dg2File", gson.toJson(dg2File))
eventMessageEmitter(Messages.COMPLETED)
scanPromise?.resolve(passport)
eventMessageEmitter(Messages.RESET)
resetState()
}
}
private fun convertDate(input: String?): String? {
if (input == null) {
return null
}
return try {
SimpleDateFormat("yyMMdd", Locale.US).format(SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(input)!!)
} catch (e: ParseException) {
// Log.w(RNSelfPassportReaderModule::class.java.simpleName, e)
null
}
}
private fun eventMessageEmitter(message: String) {
if (reactContext.hasActiveCatalystInstance()) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("NativeEvent", message)
} else {
Log.d(TAG, "Error")
}
}
private fun logAnalyticsEvent(eventName: String, params: Map<String, Any> = emptyMap()) {
try {
val logData = JSONObject()
logData.put("level", "info")
logData.put("category", "NFC")
logData.put("message", "Analytics Event: $eventName")
if (params.isNotEmpty()) {
logData.put("data", JSONObject(Gson().toJson(params)))
}
// Send to React Native via logEvent emission using the same working approach
emitLogEvent(logData.toString())
// Also log to Android logs for debugging
Log.d(TAG, "Analytics event: $eventName with params: $params")
} catch (e: Exception) {
Log.e(TAG, "Error logging analytics event", e)
}
}
private fun logAnalyticsError(eventName: String, message: String) {
try {
val logData = JSONObject()
logData.put("level", "error")
logData.put("category", "NFC")
logData.put("message", "Analytics Error: $message")
logData.put("data", JSONObject().apply {
put("event", eventName)
put("error_description", message)
})
emitLogEvent(logData.toString())
Log.e(TAG, "Analytics error: $eventName - $message")
} catch (e: Exception) {
Log.e(TAG, "Error logging analytics error", e)
}
}
private fun emitLogEvent(message: String) {
if (reactContext.hasActiveCatalystInstance()) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("logEvent", message)
} else {
Log.d(TAG, "Cannot emit logEvent - no active catalyst instance")
}
}
@ReactMethod
fun reset() {
logAnalyticsEvent("nfc_scan_reset")
resetState()
}
companion object {
private val TAG = RNSelfPassportReaderModule::class.java.simpleName
private const val PARAM_DOC_NUM = "documentNumber";
private const val PARAM_DOB = "dateOfBirth";
private const val PARAM_DOE = "dateOfExpiry";
private const val PARAM_CAN = "canNumber";
private const val PARAM_USE_CAN = "useCan";
const val JPEG_DATA_URI_PREFIX = "data:image/jpeg;base64,"
private const val KEY_IS_SUPPORTED = "isSupported"
private var instance: RNSelfPassportReaderModule? = null
fun getInstance(): RNSelfPassportReaderModule {
return instance ?: throw IllegalStateException("RNSelfPassportReaderModule instance is not initialized")
}
}
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.selfxyz.selfSDK
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class RNSelfPassportReaderPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(
RNSelfPassportReaderModule(reactContext),
SelfMRZScannerModule(reactContext)
)
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(SelfOCRViewManager(reactContext))
}
}

Some files were not shown because too many files have changed in this diff Show More