mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 14:48:06 -05:00
Merge pull request #1323 from selfxyz/staging
Release to Production - 2025-10-26
This commit is contained in:
@@ -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
|
||||
|
||||
5
.github/workflows/mobile-ci.yml
vendored
5
.github/workflows/mobile-ci.yml
vendored
@@ -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
|
||||
|
||||
56
.github/workflows/mobile-deploy.yml
vendored
56
.github/workflows/mobile-deploy.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
2
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
@@ -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:
|
||||
|
||||
37
.github/workflows/npm-publish.yml
vendored
37
.github/workflows/npm-publish.yml
vendored
@@ -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 }}
|
||||
|
||||
28
.github/workflows/release-calendar.yml
vendored
28
.github/workflows/release-calendar.yml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/web.yml
vendored
1
.github/workflows/web.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -28,6 +28,7 @@ paths = [
|
||||
'''Database.refactorlog''',
|
||||
'''vendor''',
|
||||
'''.*tamagui-components\.config\.cjs$''',
|
||||
'''packages/mobile-sdk-alpha/src/animations/.*\.json$''',
|
||||
]
|
||||
|
||||
[[rules]]
|
||||
|
||||
@@ -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
4
.gitmodules
vendored
Normal 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
|
||||
@@ -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
6
app/.gitignore
vendored
@@ -74,6 +74,12 @@ yarn-error.log
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# linting cache
|
||||
.eslintcache
|
||||
|
||||
# prettier cache
|
||||
.cache/
|
||||
|
||||
.env
|
||||
**/Pods/
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
316
app/scripts/version-manager.test.cjs
Normal file
316
app/scripts/version-manager.test.cjs
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
88
app/src/components/NavBar/WebViewNavBar.tsx
Normal file
88
app/src/components/NavBar/WebViewNavBar.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
86
app/src/components/WebViewFooter.tsx
Normal file
86
app/src/components/WebViewFooter.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
8
app/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
declare module '*.json' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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> =
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
19
app/src/navigation/types.ts
Normal file
19
app/src/navigation/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import 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;
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
204
app/src/screens/shared/WebViewScreen.tsx
Normal file
204
app/src/screens/shared/WebViewScreen.tsx
Normal 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)',
|
||||
},
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
137
app/tests/src/screens/WebViewScreen.test.tsx
Normal file
137
app/tests/src/screens/WebViewScreen.test.tsx
Normal 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/);
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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=(
|
||||
|
||||
@@ -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=(
|
||||
|
||||
@@ -57,6 +57,7 @@ export {
|
||||
commonNames,
|
||||
countries,
|
||||
countryCodes,
|
||||
getCountryISO2,
|
||||
} from './src/constants/index.js';
|
||||
|
||||
// Type exports
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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-----
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'],
|
||||
|
||||
6
packages/mobile-sdk-alpha/.gitignore
vendored
6
packages/mobile-sdk-alpha/.gitignore
vendored
@@ -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
|
||||
|
||||
16
packages/mobile-sdk-alpha/.npmignore
Normal file
16
packages/mobile-sdk-alpha/.npmignore
Normal 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)
|
||||
@@ -1,2 +1,7 @@
|
||||
dist
|
||||
node_modules
|
||||
src/animations/
|
||||
**/*.xcframework/**
|
||||
**/.build/**
|
||||
**/OpenSSL.xcframework/**
|
||||
**/SelfSDK.xcframework/**
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
186
packages/mobile-sdk-alpha/android/build.gradle.source
Normal file
186
packages/mobile-sdk-alpha/android/build.gradle.source
Normal 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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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'
|
||||
}
|
||||
@@ -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>
|
||||
Binary file not shown.
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user