Merge pull request #1479 from selfxyz/staging

Release to Production - 2025-12-07
This commit is contained in:
Justin Hernandez
2025-12-25 09:25:43 -08:00
committed by GitHub
517 changed files with 33808 additions and 36091 deletions

View File

@@ -1,291 +0,0 @@
---
description: Comprehensive migration strategy and testing-first approach for porting identity verification logic from app to mobile-sdk-alpha package
version: 1.0.0
status: active
owners:
- team: mobile-identity
- team: sdk-platform
lastUpdated: 2025-01-12
specId: mobile-sdk-migration
importanceScore: 90
importanceJustification: Critical framework for systematically migrating core identity verification functionality to a partner-consumable SDK while maintaining quality and testing coverage.
contextUsageNote: If this file is used to add in-context notes, include a single italicized line stating what specific information was used from this file in sentence case.
---
# Mobile SDK Migration Context
## Migration Strategy Overview
### Testing-First Approach
- **Create tests BEFORE migrating logic** to verify functionality works correctly
- **Dual testing environment**: Jest (app) + Vitest (mobile-sdk-alpha)
- **Validation commands**: `yarn test:build` in both app and mobile-sdk-alpha directories
- **Incremental migration**: One checklist item at a time with thorough validation
### Test Environment Differences
#### App (Jest)
- **Location**: `app/` directory
- **Config**: `jest.config.cjs` with React Native preset
- **Setup**: `jest.setup.js` with comprehensive mocks
- **Module mapping**: `@/` → `src/`, `@tests/` → `tests/src/`
- **Test command**: `yarn test:build` (builds deps + types + bundle analysis + tests)
#### Mobile SDK Alpha (Vitest)
- **Location**: `packages/mobile-sdk-alpha/` directory
- **Config**: `vitest.config.ts` with Node environment
- **Setup**: `tests/setup.ts` with console noise suppression
- **Test command**: `yarn test:build` (build + test + types + lint)
### Migration Validation Workflow
1. **Pre-migration**: Create comprehensive tests in mobile-sdk-alpha for target functionality
2. **Migration**: Port logic from app to mobile-sdk-alpha
3. **Validation**: Run `yarn test:build` in both directories
4. **Integration**: Update app to consume mobile-sdk-alpha
5. **Final validation**: Ensure app tests pass with new SDK consumption
## Migration Checklist Items
### 1. Processing Helpers (MRZ)
**Current Location**: `app/src/utils/` (MRZ utilities)
**Target Location**: `packages/mobile-sdk-alpha/src/processing/`
**Testing Strategy**:
- Create MRZ parsing tests with sample passport data
- Test cross-platform compatibility (React Native vs Web)
### 2. Validation Module
**Current Location**: `app/src/utils/` (document validation logic)
**Target Location**: `packages/mobile-sdk-alpha/src/validation/`
**Testing Strategy**:
- Unit tests for each validation rule
- Test with valid/invalid document data
- Test edge cases and error conditions
### 3. Proof Input Generation
**Current Location**: `app/src/utils/proving/`
**Target Location**: `packages/mobile-sdk-alpha/src/proving/`
**Testing Strategy**:
- Test register input generation with mock data
- Test disclose input generation with various scenarios
- Validate TEE input format compliance
### 4. Crypto Adapters
**Current Location**: `app/src/utils/` (crypto utilities)
**Target Location**: `packages/mobile-sdk-alpha/src/crypto/`
**Testing Strategy**:
- Test WebCrypto vs @noble/* fallback detection
- Test CSPRNG generation across platforms
- Test timing-safe comparison functions
- Parity tests between implementations
### 5. TEE Session Management
**Current Location**: `app/src/utils/` (WebSocket handling)
**Target Location**: `packages/mobile-sdk-alpha/src/tee/`
**Testing Strategy**:
- Test WebSocket wrapper with mock server
- Test abort, timeout, and progress events
- Test connection lifecycle management
### 6. Attestation Verification
**Current Location**: `app/src/utils/` (certificate validation)
**Target Location**: `packages/mobile-sdk-alpha/src/attestation/`
**Testing Strategy**:
- Test PCR0 validation with sample data
- Test public key extraction
- Test certificate chain validation
### 7. Protocol Synchronization
**Current Location**: `app/src/utils/` (protocol tree handling)
**Target Location**: `packages/mobile-sdk-alpha/src/protocol/`
**Testing Strategy**:
- Test protocol tree fetching with pagination
- Test TTL cache behavior
- Test rate limiting and exponential backoff
- Test memory bounds enforcement
### 8. Artifact Management
**Current Location**: `app/src/utils/` (manifest handling)
**Target Location**: `packages/mobile-sdk-alpha/src/artifacts/`
**Testing Strategy**:
- Test manifest schema validation
- Test CDN download with caching
- Test signature verification
- Test storage adapter integration
### 9. Sample Applications
**Target Location**: `packages/mobile-sdk-alpha/samples/`
**Testing Strategy**:
- Create React Native demo with MRZ → proof flow
- Create web demo with browser-based MRZ input
- Test iOS `OpenPassport` URL scheme
### 10. SDK Integration into App
**Migration Strategy**:
- Replace existing modules with SDK imports
- Update import paths throughout app
- Validate all existing functionality works
- Ensure no regression in app behavior
### 11. In-SDK Lightweight Demo
**Target Location**: `packages/mobile-sdk-alpha/demo/`
**Testing Strategy**:
- Embedded React Native demo using MRZ → proof flow
- Test theming hooks integration
- Validate build and run instructions
## Testing Best Practices
### Test Data Management
- **Mock data**: Create comprehensive test fixtures for each module
- **Sensitive data**: Never log PII, credentials, or private keys
- **Redaction**: Use consistent patterns for sensitive field masking
- **Environment flags**: Use `DEBUG_SECRETS_TOKEN` for debug-level secrets
### Cross-Platform Testing
- **React Native**: Test on both iOS and Android simulators
- **Web**: Test with browser adapters
- **Platform detection**: Test platform-specific code paths
- **Native modules**: Mock native dependencies appropriately
### Performance Testing
- **Bundle size**: Monitor SDK bundle size impact
- **Memory usage**: Test memory bounds for large operations
- **Network efficiency**: Test rate limiting and caching
- **Startup time**: Measure SDK initialization impact
### Integration Testing
- **End-to-end flows**: Test complete user journeys
- **Error handling**: Test graceful degradation
- **Recovery mechanisms**: Test error recovery and retry logic
- **Backward compatibility**: Ensure existing app functionality works
## Migration Validation Checklist
### Pre-Migration
- [ ] Create comprehensive test suite in mobile-sdk-alpha
- [ ] Define test fixtures and mock data
- [ ] Set up cross-platform testing environment
- [ ] Document current functionality and edge cases
### During Migration
- [ ] Port logic incrementally (one checklist item at a time)
- [ ] Run `yarn test:build` in mobile-sdk-alpha after each item
- [ ] Validate functionality matches original implementation
- [ ] Update documentation and type definitions
- [ ] Re-export new modules via `packages/mobile-sdk-alpha/src/index.ts` and document them in `packages/mobile-sdk-alpha/README.md`
### Post-Migration
- [ ] Update app to consume mobile-sdk-alpha
- [ ] Run `yarn test:build` in app directory
- [ ] Validate all existing app tests pass
- [ ] Test integration with existing app functionality
- [ ] Performance validation and bundle size analysis
### Final Validation
- [ ] End-to-end testing of complete flows
- [ ] Cross-platform compatibility verification
- [ ] Partner SDK consumption testing
- [ ] Documentation and example updates
- [ ] Release preparation and versioning
## Common Migration Patterns
### Module Structure
```typescript
// Before (app/src/utils/module.ts)
export function processData(data: InputType): OutputType {
// Implementation
}
// After (packages/mobile-sdk-alpha/src/module/index.ts)
export function processData(data: InputType): OutputType {
// Same implementation with enhanced error handling
}
// Test (packages/mobile-sdk-alpha/tests/module.test.ts)
describe('processData', () => {
it('should process valid data correctly', () => {
// Test implementation
});
});
```
### Adapter Pattern
```typescript
// Cross-platform adapter interface
export interface ScannerAdapter {
scan(): Promise<ScanResult>;
isSupported(): boolean;
}
// Platform-specific implementations
export class ReactNativeScannerAdapter implements ScannerAdapter {
// React Native implementation
}
export class WebScannerAdapter implements ScannerAdapter {
// Web implementation
}
```
### Error Handling
```typescript
// Consistent error types across SDK
export class SDKError extends Error {
constructor(
message: string,
public code: string,
public details?: Record<string, unknown>
) {
super(message);
this.name = 'SDKError';
}
}
```
## Security & Privacy Considerations
### Data Protection
- **Sensitive data**: Never log PII, credentials, or private keys in production
- **Secure storage**: Use appropriate storage mechanisms for sensitive data
- **Cleanup**: Properly clean up sensitive data after use
- **Validation**: Validate all inputs and outputs for security
### Privacy Features
- **Zero-knowledge proofs**: Ensure privacy-preserving verification
- **Selective disclosure**: Support minimal necessary attribute revelation
- **Identity commitments**: Maintain privacy of identity data
- **Audit trails**: Log access to sensitive operations without exposing data
## Performance Optimization
### Bundle Size
- **Tree shaking**: Ensure all exports support tree shaking
- **Code splitting**: Split large modules into smaller chunks
- **Dependency analysis**: Monitor and optimize dependencies
- **Bundle analysis**: Regular bundle size monitoring
### Runtime Performance
- **Lazy loading**: Load modules only when needed
- **Caching**: Implement appropriate caching strategies
- **Memory management**: Prevent memory leaks in long-running operations
- **Async operations**: Use proper async patterns for non-blocking operations
## Partner SDK Requirements
### API Design
- **Consistent interfaces**: Maintain consistent API patterns
- **Type safety**: Provide comprehensive TypeScript definitions
- **Error handling**: Clear error messages and error codes
- **Documentation**: Comprehensive API documentation
### Integration Support
- **Branding**: Support for partner branding and theming
- **Callbacks**: Async operation callbacks for integration
- **Configuration**: Flexible configuration options
- **Examples**: Comprehensive integration examples
This context provides a comprehensive framework for executing the migration checklist with a testing-first approach, ensuring quality and reliability throughout the migration process.
$END$

View File

@@ -0,0 +1,203 @@
---
description: Critical rules for avoiding out-of-memory issues in tests, specifically preventing nested require() calls that cause pipeline failures
version: 1.0.0
status: active
owners:
- team: mobile-identity
- team: platform-infrastructure
lastUpdated: 2025-01-12
specId: test-memory-optimization
importanceScore: 100
importanceJustification: Prevents catastrophic pipeline failures due to out-of-memory errors caused by nested require() calls, especially with react-native modules in test environments.
contextUsageNote: If this file is used to add in-context notes, include a single italicized line stating what specific information was used from this file in sentence case.
---
# Test Memory Optimization Rules
## Critical: Never Nest require() Calls
### The Problem
Nested `require('react-native')` calls within tests cause **out-of-memory (OOM) errors** in CI/CD pipelines. This happens because:
1. **Module Resolution Loops**: Each nested require can trigger additional module resolution and initialization
2. **Memory Accumulation**: React Native modules are large and complex; nested requires multiply memory usage
3. **Test Environment Overhead**: Jest/Vitest test runners already load modules; nested requires create duplicate module instances
4. **Hermes Parser Issues**: Nested requires can trigger WASM memory issues with hermes-parser
### The Rule
**NEVER create nested `require('react-native')` calls within test files or test setup files.**
### Examples of FORBIDDEN Patterns
#### ❌ FORBIDDEN: Nested require in test files
```typescript
// BAD - This will cause OOM issues
describe('MyComponent', () => {
beforeEach(() => {
const RN = require('react-native');
const Component = require('./MyComponent');
// Component internally does: require('react-native') again
// This creates nested requires = OOM
});
});
```
#### ❌ FORBIDDEN: require() inside module that's required in tests
```typescript
// BAD - If this module is required in tests, it creates nested requires
// app/src/utils/myUtil.ts
export function myFunction() {
const RN = require('react-native'); // Nested if called from test
return RN.Platform.OS;
}
```
#### ❌ FORBIDDEN: Dynamic requires in test hooks
```typescript
// BAD - Dynamic requires in beforeEach/afterEach create nested requires
beforeEach(() => {
jest.resetModules();
const RN = require('react-native'); // First require
const service = require('@/utils/service'); // May internally require RN again
});
```
### Examples of CORRECT Patterns
#### ✅ CORRECT: Use ES6 imports at top level
```typescript
// GOOD - Single import at top level
import { Platform } from 'react-native';
describe('MyComponent', () => {
it('should work', () => {
expect(Platform.OS).toBe('ios');
});
});
```
**Key Rule**: Use `import` statements, not `require()`. React Native is already mocked in setup files (`jest.setup.js` for Jest, `tests/setup.ts` for Vitest), so imports work correctly.
## React Native Module Handling in Tests
### Jest Setup Pattern (app/jest.setup.js)
The project uses a custom require override in `jest.setup.js` to handle React Native mocks:
```javascript
// This is OK - it's in setup file, runs once
const Module = require('module');
const originalRequire = Module.prototype.require;
Module.prototype.require = function (id) {
if (id === 'react-native') {
const RN = originalRequire.apply(this, arguments);
// Add mocks if needed
return RN;
}
return originalRequire.apply(this, arguments);
};
```
**Key Point**: This override runs ONCE during test setup. Tests should NOT create additional require() calls that would trigger this override multiple times.
### Vitest Setup Pattern (packages/mobile-sdk-alpha/tests/setup.ts)
Vitest uses `vi.mock()` to mock React Native:
```typescript
// This is OK - runs once during setup
vi.mock('react-native', () => ({
Platform: { OS: 'web' },
// ... other mocks
}));
```
**Key Point**: Tests should use `import` statements, not `require()`, after mocks are set up.
## Best Practices
1. **Always use ES6 `import` statements** - Never use `require('react')` or `require('react-native')` in test files
2. **Put all imports at the top of the file** - No dynamic imports in hooks
3. **Avoid `jest.resetModules()`** - Only use when absolutely necessary for module initialization tests
4. **Use setup file mocks** - React Native is already mocked in `jest.setup.js` (Jest) or `tests/setup.ts` (Vitest)
## Automated Enforcement
The project has multiple layers of protection against nested require() patterns:
### 1. ESLint Rule (app/.eslintrc.cjs)
ESLint will fail on `require('react')` and `require('react-native')` in test files:
```javascript
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.name='require'][arguments.0.value='react']",
message: "Do not use require('react') in tests..."
},
{
selector: "CallExpression[callee.name='require'][arguments.0.value='react-native']",
message: "Do not use require('react-native') in tests..."
}
]
```
Run `yarn lint` to check for violations.
### 2. Validation Script (app/scripts/check-test-requires.cjs)
Automated script to detect nested require patterns:
```bash
node scripts/check-test-requires.cjs
```
This script:
- Scans all test files for `require('react')` and `require('react-native')`
- Reports exact file locations and line numbers
- Exits with error code 1 if issues found
### 3. CI Fast-Fail Check
GitHub Actions runs the validation script before tests:
```yaml
- name: Check for nested require() in tests
run: node scripts/check-test-requires.cjs
working-directory: ./app
```
This prevents wasting CI time on tests that will OOM.
## Quick Checklist
Before committing test changes:
- [ ] No `require('react')` calls in test files (use `import React from 'react'` instead)
- [ ] No `require('react-native')` calls in test files (use `import { ... } from 'react-native'` instead)
- [ ] All imports at top of file (not in hooks or jest.mock() factories)
- [ ] Run validation: `node scripts/check-test-requires.cjs`
- [ ] Run lint: `yarn lint`
## Detection
**Signs of nested require issues**: CI OOM errors, test timeouts, memory spikes, "Call stack size exceeded" errors, tests hiding actual failures
**Fix**:
1. Search for `require('react')` and `require('react-native')` in tests
2. Replace with `import` statements at the top of the file
3. Run `node scripts/check-test-requires.cjs` to verify
## Related Files
- `app/.eslintrc.cjs` - ESLint rules blocking nested requires
- `app/scripts/check-test-requires.cjs` - Validation script
- `.github/workflows/mobile-ci.yml` - CI enforcement
- `app/jest.setup.js` - Jest setup with React Native mocks
- `packages/mobile-sdk-alpha/tests/setup.ts` - Vitest setup with React Native mocks
- `app/jest.config.cjs` - Jest configuration
- `packages/mobile-sdk-alpha/vitest.config.ts` - Vitest configuration
$END$

View File

@@ -214,9 +214,6 @@ NOTICE
**/*.zip
**/*.tar.gz
# Patch files
patches/
# ========================================
# Project Specific Patterns
# ========================================

View File

@@ -2,7 +2,10 @@
# This file configures which files and secrets to ignore during scanning
# Ignore specific file patterns
paths-ignore:
paths_ignore:
# Gitleaks configuration file (contains example secrets/patterns for detection)
- ".gitleaks.toml"
# Mock certificates for testing (these are intentionally committed test data)
- "**/mock_certificates/**/*.key"
- "**/mock_certificates/**/*.crt"
@@ -46,47 +49,47 @@ paths-ignore:
- "**/packages/mobile-sdk-alpha/ios/Frameworks/**"
- "**/packages/mobile-sdk-alpha/ios/SelfSDK/**"
# Ignore specific secret types for mock files
secrets-ignore:
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
- 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'
- ".gitleaks.toml"
- "**/*.swiftinterface"
- "**/*.xcframework/**"
- "**/packages/mobile-sdk-alpha/ios/Frameworks/**"
- "**/OpenSSL.xcframework/**"
- "**/demo-app/**/mock/**"
- common/src/mock_certificates/aadhaar/mockAadhaarCert.ts
- "**/NFCPassportReader.xcframework/**"
- common/src/utils/passports/genMockIdDoc.ts
- "**/tests/**/*.crt"
- "**/mock_certificates/**/*.crt"
- "**/mock_certificates/**/*.key"
- "**/demo-app/**/test-data/**"
- "**/generated/**/*.key"
- "**/SelfSDK.xcframework/**"
- "**/mock/**/*.crt"
- "**/generated/**/*.crt"
- "**/test/**/*.key"
- "**/mock/**/*.key"
- "**/test/**/*.crt"
- "**/test/**/*.pem"
- "**/constants/mockCertificates.ts"
- "**/mock/**/*.pem"
- "**/mock_certificates/**/*.pem"
- "**/mock-data/**"
- "**/packages/mobile-sdk-alpha/ios/SelfSDK/**"
- "**/tests/**/*.key"
- "**/generated/**/*.pem"
- "**/tests/**/*.pem"
- "**/test-data/**"
- common/src/mock_certificates/**
- "**/*.xcframework"
version: 2

View File

@@ -24,12 +24,22 @@ outputs:
runs:
using: "composite"
steps:
- id: get-hash
name: Hash lock file
shell: bash
run: |
if [ -f "${{ inputs.lock-file }}" ]; then
echo "hash=$(shasum -a 256 "${{ inputs.lock-file }}" | awk '{ print $1 }')" >> $GITHUB_OUTPUT
else
echo "::warning::Lock file '${{ inputs.lock-file }}' not found."
echo "hash=no-lock-file" >> $GITHUB_OUTPUT
fi
- id: cache
name: Cache Ruby gems
uses: actions/cache@v4
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-gems-${{ inputs.cache-version }}-${{ hashFiles(inputs.lock-file) }}
key: ${{ runner.os }}-gems-${{ inputs.cache-version }}-${{ steps.get-hash.outputs.hash }}
restore-keys: |
${{ runner.os }}-gems-${{ inputs.cache-version }}-
${{ runner.os }}-gems-

View File

@@ -29,7 +29,7 @@ runs:
uses: actions/cache@v4
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-gradle-${{ inputs.cache-version }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
key: ${{ runner.os }}-gradle-${{ inputs.cache-version }}-${{ hashFiles('**/build.gradle', '**/settings.gradle', '**/gradle-wrapper.properties', '**/gradle.properties') }}
restore-keys: |
${{ runner.os }}-gradle-${{ inputs.cache-version }}-
${{ runner.os }}-gradle-

View File

@@ -9,7 +9,7 @@ inputs:
default: |
ios/Pods
~/Library/Caches/CocoaPods
lock-file:
lockfile:
description: Path to Podfile.lock
required: false
default: ios/Podfile.lock
@@ -31,7 +31,7 @@ runs:
uses: actions/cache@v4
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-pods-${{ inputs.cache-version }}-${{ hashFiles(inputs.lock-file) }}
key: ${{ runner.os }}-pods-${{ inputs.cache-version }}-${{ hashFiles(inputs.lockfile) }}
restore-keys: |
${{ runner.os }}-pods-${{ inputs.cache-version }}-
${{ runner.os }}-pods-

View File

@@ -25,12 +25,22 @@ outputs:
runs:
using: "composite"
steps:
- id: get-hash
name: Hash lock file
shell: bash
run: |
if [ -f "${{ inputs.lock-file }}" ]; then
echo "hash=$(shasum -a 256 "${{ inputs.lock-file }}" | awk '{ print $1 }')" >> $GITHUB_OUTPUT
else
echo "::warning::Lock file '${{ inputs.lock-file }}' not found."
echo "hash=no-lock-file" >> $GITHUB_OUTPUT
fi
- id: cache
name: Cache Yarn dependencies
uses: actions/cache@v4
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-yarn-${{ inputs.cache-version }}-${{ hashFiles(inputs.lock-file) }}
key: ${{ runner.os }}-yarn-${{ inputs.cache-version }}-${{ steps.get-hash.outputs.hash }}
restore-keys: |
${{ runner.os }}-yarn-${{ inputs.cache-version }}-
${{ runner.os }}-yarn-

View File

@@ -0,0 +1,56 @@
name: "Generate GitHub App Token"
description: "Generates a GitHub App token for accessing repositories in the selfxyz organization"
inputs:
app-id:
description: "The GitHub App ID"
required: true
private-key:
description: "The GitHub App private key"
required: true
configure-netrc:
description: "If true, writes a ~/.netrc entry for github.com using the generated token (useful for CocoaPods / git HTTPS fetches)"
required: false
default: "false"
netrc-machine:
description: "The machine hostname to write into ~/.netrc (default: github.com)"
required: false
default: "github.com"
owner:
description: "The owner (organization) of the repositories"
required: false
default: "selfxyz"
repositories:
description: "Comma-separated list of repository names to grant access to"
required: false
default: "NFCPassportReader,android-passport-nfc-reader,react-native-passport-reader,mobile-sdk-native"
outputs:
token:
description: "The generated GitHub App installation token"
value: ${{ steps.app-token.outputs.token }}
runs:
using: "composite"
steps:
- name: Generate GitHub App Token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
id: app-token
with:
app-id: ${{ inputs.app-id }}
private-key: ${{ inputs.private-key }}
owner: ${{ inputs.owner }}
repositories: ${{ inputs.repositories }}
- name: Configure Git auth via ~/.netrc (optional)
if: ${{ inputs.configure-netrc == 'true' }}
shell: bash
run: |
set -euo pipefail
TOKEN="${{ steps.app-token.outputs.token }}"
MACHINE="${{ inputs.netrc-machine }}"
# Mask the token in logs defensively (it shouldn't print, but this protects against future edits).
echo "::add-mask::${TOKEN}"
printf "machine %s\n login x-access-token\n password %s\n" "${MACHINE}" "${TOKEN}" > "${HOME}/.netrc"
chmod 600 "${HOME}/.netrc"

View File

@@ -22,9 +22,11 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: .nvmrc
cache: "yarn"
cache-dependency-path: yarn.lock
cache-dependency-path: |
yarn.lock
.yarnrc.yml
- name: Install dependencies
uses: nick-fields/retry@v3

25
.github/actions/yarnrc-hash/action.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Compute .yarnrc.yml hash
description: Compute a stable hash for .yarnrc.yml to use in cache keys.
outputs:
hash:
description: Hash of .yarnrc.yml (or "no-yarnrc" if the file is missing)
value: ${{ steps.compute-yarnrc-hash.outputs.hash }}
runs:
using: composite
steps:
- name: Compute .yarnrc.yml hash
id: compute-yarnrc-hash
shell: bash
run: |
if [ -f .yarnrc.yml ]; then
if command -v shasum >/dev/null 2>&1; then
echo "hash=$(shasum -a 256 .yarnrc.yml | awk '{ print $1 }')" >> "$GITHUB_OUTPUT"
else
echo "hash=$(sha256sum .yarnrc.yml | awk '{ print $1 }')" >> "$GITHUB_OUTPUT"
fi
else
echo "hash=no-yarnrc" >> "$GITHUB_OUTPUT"
fi

11
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,11 @@
### Description
_A brief description of the changes, what and how is being changed._
### Tested
_Explain how the change has been tested (for example by manual testing, unit tests etc) or why it's not necessary (for example version bump)._
### How to QA
_How can the change be tested in a repeatable manner?_

View File

@@ -42,7 +42,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install cpp dependencies
run: |

View File

@@ -5,12 +5,45 @@ on:
- dev
- staging
- main
paths:
- "circuits/**"
jobs:
check_changes:
runs-on: ubuntu-slim
outputs:
should_run: ${{ steps.filter.outputs.should_run }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Check if should run
id: filter
run: |
set -e
if [[ "${{ github.base_ref }}" == "main" ]] || [[ "${{ github.base_ref }}" == "staging" ]]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "Running for ${{ github.base_ref }} - no path filter"
else
# For dev branch, check if circuits files changed
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) || {
echo "Error: Failed to diff against base branch"
exit 1
}
if echo "$CHANGED_FILES" | grep -qE "^circuits/"; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "Running for dev - circuits files changed"
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "Skipping for dev - no circuits files changed"
fi
fi
run_circuit_tests:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
needs: check_changes
if: github.event.pull_request.draft == false && needs.check_changes.outputs.should_run == 'true'
runs-on:
- "self-hosted"
- "selfxyz-org"
- "ubuntu-24-04"
environment: development
permissions:
contents: read
@@ -18,7 +51,7 @@ jobs:
CIRCOM_VERSION: "2.1.9"
CIRCOM_SHA256: "e5575829252d763b7818049df9de2ef9304df834697de77fa63ce7babc23c967"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
# Circom installation from https://github.com/erhant/circomkit/blob/main/.github/workflows/tests.yml
- name: Install dependencies
@@ -106,6 +139,14 @@ jobs:
- name: Print Circom version
run: circom --version
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Enable Corepack
run: corepack enable
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:

View File

@@ -8,7 +8,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:
@@ -34,7 +34,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:
@@ -54,7 +54,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:
@@ -79,6 +79,8 @@ jobs:
run: yarn workspace @selfxyz/common build
- name: Build @selfxyz/mobile-sdk-alpha
run: yarn workspace @selfxyz/mobile-sdk-alpha build
- name: Build @selfxyz/qrcode
run: yarn workspace @selfxyz/qrcode build:deps
- name: Yarn types
run: yarn types
@@ -88,7 +90,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:

View File

@@ -5,21 +5,50 @@ on:
- dev
- staging
- main
paths:
- "contracts/**"
- "common/**"
concurrency:
group: contracts-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check_changes:
runs-on: ubuntu-slim
outputs:
should_run: ${{ steps.filter.outputs.should_run }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Check if should run
id: filter
run: |
set -e
if [[ "${{ github.base_ref }}" == "main" ]] || [[ "${{ github.base_ref }}" == "staging" ]]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "Running for ${{ github.base_ref }} - no path filter"
else
# For dev branch, check if contracts or common files changed
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) || {
echo "Error: Failed to diff against base branch"
exit 1
}
if echo "$CHANGED_FILES" | grep -qE "^(contracts|common)/"; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "Running for dev - contracts or common files changed"
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "Skipping for dev - no contracts or common files changed"
fi
fi
test_contracts:
if: github.event.pull_request.draft == false
needs: check_changes
if: github.event.pull_request.draft == false && needs.check_changes.outputs.should_run == 'true'
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |

View File

@@ -14,7 +14,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
@@ -38,7 +38,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Corepack
run: |
corepack enable
@@ -67,7 +67,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Corepack
run: |
corepack enable
@@ -96,7 +96,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Corepack
run: |
corepack enable

View File

@@ -1,21 +0,0 @@
name: GitGuardian Scan
on:
pull_request:
jobs:
gitguardian:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # fetch all history so multiple commits can be scanned
- name: GitGuardian scan
uses: GitGuardian/ggshield/actions/secret@v1.41.0
env:
GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }}
GITHUB_PUSH_BASE_SHA: ${{ github.event.base }}
GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}

View File

@@ -7,13 +7,13 @@ jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install gitleaks
uses: gitleaks/gitleaks-action@v2.3.9
with:
config-path: .gitleaks.toml
config-path: gitleaks-override.toml
fail: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -5,6 +5,7 @@ env:
JAVA_VERSION: 17
WORKSPACE: ${{ github.workspace }}
APP_PATH: ${{ github.workspace }}/app
NODE_ENV: "production"
on:
pull_request:
@@ -19,7 +20,7 @@ jobs:
analyze-android:
runs-on: macos-latest-large
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -37,32 +38,34 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache Node Modules
uses: actions/cache@v4
- name: Cache Yarn
uses: ./.github/actions/cache-yarn
with:
path: |
.yarn/cache
node_modules
app/node_modules
key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-
- name: Cache Ruby Bundler
uses: actions/cache@v4
cache-version: node-${{ env.NODE_VERSION_SANITIZED }}
- name: Cache Bundler
uses: ./.github/actions/cache-bundler
with:
path: app/vendor/bundle
key: ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ hashFiles('app/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-
lock-file: app/Gemfile.lock
cache-version: ruby${{ env.RUBY_VERSION }}
- name: Cache Gradle
uses: actions/cache@v4
uses: ./.github/actions/cache-gradle
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('app/android/**/gradle-wrapper.properties', 'app/android/**/gradle-wrapper.jar') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
configure-netrc: "true"
- name: Install Mobile Dependencies
uses: ./.github/actions/mobile-setup
with:
@@ -71,7 +74,7 @@ jobs:
ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }}
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Build dependencies
shell: bash
run: yarn workspace @selfxyz/common build
@@ -82,7 +85,7 @@ jobs:
analyze-ios:
runs-on: macos-latest-large
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -100,30 +103,33 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache Node Modules
uses: actions/cache@v4
- name: Cache Yarn
uses: ./.github/actions/cache-yarn
with:
path: |
.yarn/cache
node_modules
app/node_modules
key: ${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-node${{ env.NODE_VERSION }}-yarn-
- name: Cache Ruby Bundler
uses: actions/cache@v4
cache-version: node-${{ env.NODE_VERSION_SANITIZED }}
- name: Cache Bundler
uses: ./.github/actions/cache-bundler
with:
path: app/vendor/bundle
key: ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ hashFiles('app/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-
lock-file: app/Gemfile.lock
cache-version: ruby${{ env.RUBY_VERSION }}
- name: Cache CocoaPods
uses: actions/cache@v4
uses: ./.github/actions/cache-pods
with:
path: app/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('app/ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
lockfile: app/ios/Podfile.lock
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
configure-netrc: "true"
- name: Install Mobile Dependencies
uses: ./.github/actions/mobile-setup
with:
@@ -132,7 +138,7 @@ jobs:
ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }}
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Build dependencies
shell: bash
run: yarn workspace @selfxyz/common build

View File

@@ -10,7 +10,7 @@ env:
WORKSPACE: ${{ github.workspace }}
APP_PATH: ${{ github.workspace }}/app
# Cache versions
GH_CACHE_VERSION: v1 # Global cache version
GH_CACHE_VERSION: v2 # Global cache version - bumped to invalidate caches
GH_GEMS_CACHE_VERSION: v1 # Ruby gems cache version
# Performance optimizations
GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=4 -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.caching=true
@@ -35,10 +35,10 @@ concurrency:
jobs:
build-deps:
runs-on: macos-latest-large
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -76,7 +76,7 @@ jobs:
with:
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }}
- name: Build dependencies (cache miss)
# Temporarily disabled due to `yarn types` failures when cache is used.
# 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..."
@@ -90,16 +90,13 @@ jobs:
- name: Check App Types
run: yarn types
working-directory: ./app
- name: Check license headers
run: node scripts/check-license-headers.mjs --check
working-directory: ./
test:
runs-on: macos-latest-large
runs-on: ubuntu-latest
needs: build-deps
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -147,10 +144,11 @@ jobs:
ls -la common/dist/ || echo "❌ common dist not found"
ls -la common/dist/cjs/ || echo "❌ common dist/cjs not found"
ls -la common/dist/cjs/index.cjs || echo "❌ common dist/cjs/index.cjs not found"
- name: Build dependencies (cache miss)
if: steps.built-deps.outputs.cache-hit != 'true'
- name: Build dependencies (always - debugging CI)
# Temporarily always build to debug CI issues
# TODO: Re-enable cache after fixing: if: steps.built-deps.outputs.cache-hit != 'true'
run: |
echo "Cache miss for built dependencies. Building now..."
echo "Building dependencies (cache temporarily disabled for debugging)..."
yarn workspace @selfxyz/mobile-app run build:deps
# Verify build completed successfully
if [ ! -f "packages/mobile-sdk-alpha/dist/cjs/index.cjs" ] || [ ! -f "common/dist/cjs/index.cjs" ]; then
@@ -182,7 +180,15 @@ jobs:
else
echo "✅ All required dependency files exist"
fi
- name: Check for nested require() in tests
run: node scripts/check-test-requires.cjs
working-directory: ./app
- name: App Tests
env:
# Increase Node.js memory to prevent hermes-parser WASM memory errors
NODE_OPTIONS: --max-old-space-size=4096
# Override production NODE_ENV for tests - React's production build doesn't include testing utilities
NODE_ENV: test
run: |
# Final verification from app directory perspective
echo "Final verification before running tests (from app directory)..."
@@ -205,7 +211,7 @@ jobs:
IOS_PROJECT_NAME: "Self"
IOS_PROJECT_SCHEME: "OpenPassport"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -261,6 +267,7 @@ jobs:
- name: Cache Ruby gems
uses: ./.github/actions/cache-bundler
with:
# TODO(jcortejoso): Confirm the path of the bundle cache
path: app/ios/vendor/bundle
lock-file: app/Gemfile.lock
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
@@ -270,7 +277,7 @@ jobs:
path: |
app/ios/Pods
~/Library/Caches/CocoaPods
lock-file: app/ios/Podfile.lock
lockfile: app/ios/Podfile.lock
cache-version: ${{ env.GH_CACHE_VERSION }}
- name: Cache Xcode build
uses: actions/cache@v4
@@ -308,6 +315,14 @@ jobs:
bundle config set --local path 'vendor/bundle'
bundle install --jobs 4 --retry 3
working-directory: ./app
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
configure-netrc: "true"
- name: Install iOS Dependencies
uses: nick-fields/retry@v3
with:
@@ -318,7 +333,7 @@ jobs:
cd app/ios
bundle exec bash scripts/pod-install-with-cache-fix.sh
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Resolve iOS workspace
run: |
WORKSPACE_OPEN="ios/OpenPassport.xcworkspace"
@@ -392,7 +407,7 @@ jobs:
needs: build-deps
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -463,11 +478,20 @@ jobs:
run: |
echo "Cache miss for built dependencies. Building now..."
yarn workspace @selfxyz/mobile-app run build:deps
- name: Clone android-passport-nfc-reader
uses: ./.github/actions/clone-android-passport-nfc-reader
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
working_directory: ${{ env.APP_PATH }}
selfxyz_internal_pat: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
- name: Setup Android private modules
run: |
cd ${{ env.APP_PATH }}
PLATFORM=android node scripts/setup-private-modules.cjs
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
CI: true
- name: Build Android (with AAPT2 symlink fix)
run: yarn android:ci
working-directory: ./app

View File

@@ -31,6 +31,7 @@ name: Mobile Deploy
env:
# Build environment versions
RUBY_VERSION: 3.2
NODE_ENV: "production"
JAVA_VERSION: 17
ANDROID_API_LEVEL: 35
ANDROID_NDK_VERSION: 27.0.12077973
@@ -150,9 +151,16 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
# Guard against auto-closed PRs (GitHub reports merged == false when a PR closes without merging)
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
!contains(github.event.pull_request.labels.*.name, 'deploy:skip')
(
github.event_name != 'pull_request' ||
(github.event.action == 'closed' && github.event.pull_request.merged == true)
) &&
(
github.event_name != 'pull_request' ||
!contains(github.event.pull_request.labels.*.name, 'deploy:skip')
)
outputs:
version: ${{ steps.bump.outputs.version }}
ios_build: ${{ steps.bump.outputs.ios_build }}
@@ -160,7 +168,7 @@ jobs:
version_bump_type: ${{ steps.determine-bump.outputs.version_bump }}
platform: ${{ steps.determine-platform.outputs.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
# Build from the branch that triggered the workflow (staging, feature branch, etc.)
@@ -262,7 +270,8 @@ jobs:
contents: read
actions: write
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
(github.event_name != 'pull_request' ||
(github.event.action == 'closed' && github.event.pull_request.merged == true)) &&
(
(inputs.platform == 'ios' || inputs.platform == 'both') ||
(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'deploy:android'))
@@ -282,7 +291,7 @@ jobs:
fi
fi
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
# Checkout the branch that triggered the workflow
@@ -359,6 +368,10 @@ jobs:
echo "Xcode path:"
xcode-select -p
- name: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- name: Cache Yarn artifacts
id: yarn-cache
uses: ./.github/actions/cache-yarn
@@ -367,12 +380,13 @@ jobs:
.yarn/cache
.yarn/install-state.gz
.yarn/unplugged
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
- name: Cache Ruby gems
id: gems-cache
uses: ./.github/actions/cache-bundler
with:
# TODO(jcortejoso): Confirm the path of the bundle cache
path: ${{ env.APP_PATH }}/ios/vendor/bundle
lock-file: app/Gemfile.lock
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
@@ -384,7 +398,7 @@ jobs:
path: |
${{ env.APP_PATH }}/ios/Pods
~/Library/Caches/CocoaPods
lock-file: app/ios/Podfile.lock
lockfile: app/ios/Podfile.lock
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_PODS_CACHE_VERSION }}
- name: Log cache status
@@ -416,6 +430,14 @@ jobs:
fi
echo "✅ Lock files exist"
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
configure-netrc: "true"
- name: Install Mobile Dependencies (main repo)
if: inputs.platform != 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
@@ -426,7 +448,7 @@ jobs:
ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }}
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Install Mobile Dependencies (forked PRs - no secrets)
if: inputs.platform != 'android' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
@@ -679,8 +701,11 @@ jobs:
IOS_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }}
NODE_OPTIONS: "--max-old-space-size=8192"
SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }}
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }}
TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }}
TURNKEY_ORGANIZATION_ID: ${{ secrets.TURNKEY_ORGANIZATION_ID }}
timeout-minutes: 90
run: |
cd ${{ env.APP_PATH }}
@@ -826,7 +851,8 @@ jobs:
actions: write
id-token: write
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
(github.event_name != 'pull_request' ||
(github.event.action == 'closed' && github.event.pull_request.merged == true)) &&
(
(inputs.platform == 'android' || inputs.platform == 'both') ||
(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'deploy:ios'))
@@ -846,7 +872,7 @@ jobs:
fi
fi
- uses: actions/checkout@v4
- uses: actions/checkout@v6
if: inputs.platform != 'ios'
with:
fetch-depth: 0
@@ -964,6 +990,10 @@ jobs:
# Use version-manager script to apply versions
node ${{ env.APP_PATH }}/scripts/version-manager.cjs apply "$VERSION" "$IOS_BUILD" "$ANDROID_BUILD"
- name: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- name: Cache Yarn artifacts
id: yarn-cache
uses: ./.github/actions/cache-yarn
@@ -972,7 +1002,7 @@ jobs:
.yarn/cache
.yarn/install-state.gz
.yarn/unplugged
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
- name: Cache Ruby gems
id: gems-cache
@@ -1026,6 +1056,14 @@ jobs:
echo "✅ Lock files exist"
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
- name: Install Mobile Dependencies (main repo)
if: inputs.platform != 'ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
uses: ./.github/actions/mobile-setup
@@ -1035,7 +1073,7 @@ jobs:
ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }}
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
PLATFORM: ${{ inputs.platform }}
- name: Install Mobile Dependencies (forked PRs - no secrets)
@@ -1086,12 +1124,14 @@ jobs:
python -m pip install --upgrade pip
pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client
- name: Clone android-passport-nfc-reader
- name: Setup Android private modules
if: inputs.platform != 'ios'
uses: ./.github/actions/clone-android-passport-nfc-reader
with:
working_directory: ${{ env.APP_PATH }}
selfxyz_internal_pat: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
run: |
cd ${{ env.APP_PATH }}
PLATFORM=android node scripts/setup-private-modules.cjs
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
CI: true
- name: Build Dependencies (Android)
if: inputs.platform != 'ios'
@@ -1121,6 +1161,9 @@ jobs:
NODE_OPTIONS: "--max-old-space-size=6144"
SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }}
TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }}
TURNKEY_ORGANIZATION_ID: ${{ secrets.TURNKEY_ORGANIZATION_ID }}
run: |
cd ${{ env.APP_PATH }}
@@ -1233,12 +1276,13 @@ jobs:
needs: [bump-version, build-ios, build-android]
if: |
always() &&
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
(github.event_name != 'pull_request' ||
(github.event.action == 'closed' && github.event.pull_request.merged == true)) &&
(needs.build-ios.result == 'success' || needs.build-android.result == 'success')
env:
APP_PATH: ${{ github.workspace }}/app
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
# Checkout target branch for version bump PR (default: dev, override with bump_target_branch input)
@@ -1276,17 +1320,21 @@ jobs:
const version = JSON.parse(fs.readFileSync('version.json', 'utf8'));
const timestamp = new Date().toISOString();
version.ios.build = $IOS_BUILD;
version.android.build = $ANDROID_BUILD;
// Update lastDeployed timestamp for successful builds
// Only bump build numbers for successful builds
if ('$IOS_SUCCESS' === 'success') {
version.ios.build = $IOS_BUILD;
version.ios.lastDeployed = timestamp;
console.log('✅ Updated iOS lastDeployed timestamp');
console.log('✅ Updated iOS build number to $IOS_BUILD and lastDeployed timestamp');
} else {
console.log('⏭️ Skipped iOS build number bump (build did not succeed)');
}
if ('$ANDROID_SUCCESS' === 'success') {
version.android.build = $ANDROID_BUILD;
version.android.lastDeployed = timestamp;
console.log('✅ Updated Android lastDeployed timestamp');
console.log('✅ Updated Android build number to $ANDROID_BUILD and lastDeployed timestamp');
} else {
console.log('⏭️ Skipped Android build number bump (build did not succeed)');
}
fs.writeFileSync('version.json', JSON.stringify(version, null, 2) + '\n');
@@ -1423,7 +1471,7 @@ jobs:
(needs.build-ios.result == 'success' || needs.build-android.result == 'success') &&
(inputs.deployment_track == 'production')
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
# Checkout target branch for tagging (usually dev)

View File

@@ -7,7 +7,7 @@ env:
ANDROID_NDK_VERSION: 27.0.12077973
XCODE_VERSION: 16.4
# Cache versions
GH_CACHE_VERSION: v1 # Global cache version
GH_CACHE_VERSION: v2 # Global cache version - bumped to invalidate caches
GH_GEMS_CACHE_VERSION: v1 # Ruby gems cache version
# Performance optimizations
GRADLE_OPTS: -Dorg.gradle.workers.max=4 -Dorg.gradle.parallel=true -Dorg.gradle.caching=true
@@ -37,7 +37,7 @@ jobs:
timeout-minutes: 120
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -56,6 +56,9 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
- run: corepack enable
- run: corepack prepare yarn@4.6.0 --activate
- name: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:
@@ -63,10 +66,18 @@ jobs:
.yarn/cache
.yarn/install-state.gz
.yarn/unplugged
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
- name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
configure-netrc: "true"
- name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3
@@ -76,7 +87,7 @@ jobs:
retry_wait_seconds: 5
command: yarn install --immutable --silent
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3
@@ -130,11 +141,13 @@ jobs:
corepack prepare yarn@4.6.0 --activate
yarn workspace @selfxyz/mobile-app run build:deps || { echo "❌ Dependency build failed"; exit 1; }
echo "✅ Dependencies built successfully"
- name: Clone android-passport-nfc-reader
uses: ./.github/actions/clone-android-passport-nfc-reader
with:
working_directory: app
selfxyz_internal_pat: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
- name: Setup Android private modules
run: |
cd app
PLATFORM=android node scripts/setup-private-modules.cjs
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
CI: true
- name: Build Android APK
run: |
echo "Building Android APK..."
@@ -144,6 +157,8 @@ jobs:
- name: Clean up Gradle build artifacts
uses: ./.github/actions/cleanup-gradle-artifacts
- name: Verify APK and android-passport-nfc-reader integration
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
run: |
echo "🔍 Verifying build artifacts..."
APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk"
@@ -154,16 +169,29 @@ jobs:
APK_SIZE=$(stat -f%z "$APK_PATH" 2>/dev/null || stat -c%s "$APK_PATH" 2>/dev/null || echo "unknown")
echo "📱 APK size: $APK_SIZE bytes"
# Verify android-passport-nfc-reader was properly integrated (skip for forks)
if [ -z "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]; then
echo "🔕 No PAT available — skipping private module verification"
elif [ -d "app/android/android-passport-nfc-reader" ]; then
echo "✅ android-passport-nfc-reader directory exists"
echo "📁 android-passport-nfc-reader contents:"
ls -la app/android/android-passport-nfc-reader/ | head -10
# Verify private modules were properly integrated (skip for forks)
if [ -z "${SELFXYZ_APP_TOKEN:-}" ]; then
echo "🔕 No SELFXYZ_APP_TOKEN available — skipping private module verification"
else
echo "❌ android-passport-nfc-reader directory not found"
exit 1
# Verify android-passport-nfc-reader
if [ -d "app/android/android-passport-nfc-reader" ]; then
echo "✅ android-passport-nfc-reader directory exists"
echo "📁 android-passport-nfc-reader contents:"
ls -la app/android/android-passport-nfc-reader/ | head -10
else
echo "❌ android-passport-nfc-reader directory not found"
exit 1
fi
# Verify react-native-passport-reader
if [ -d "app/android/react-native-passport-reader" ]; then
echo "✅ react-native-passport-reader directory exists"
echo "📁 react-native-passport-reader contents:"
ls -la app/android/react-native-passport-reader/ | head -10
else
echo "❌ react-native-passport-reader directory not found"
exit 1
fi
fi
echo "🎉 Build verification completed successfully!"
@@ -212,7 +240,7 @@ jobs:
IOS_PROJECT_NAME: "Self"
IOS_PROJECT_SCHEME: "OpenPassport"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -231,6 +259,9 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
- run: corepack enable
- run: corepack prepare yarn@4.6.0 --activate
- name: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:
@@ -238,10 +269,18 @@ jobs:
.yarn/cache
.yarn/install-state.gz
.yarn/unplugged
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
- name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
configure-netrc: "true"
- name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3
@@ -251,7 +290,7 @@ jobs:
retry_wait_seconds: 5
command: yarn install --immutable --silent
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3
@@ -296,12 +335,13 @@ jobs:
xcodebuild -version
echo "Xcode path:"
xcode-select -p
- name: Setup ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ${{ github.job }}-${{ runner.os }}
- name: Add ccache to PATH
run: echo "/usr/local/opt/ccache/libexec" >> $GITHUB_PATH
# Temporarily disabled ccache to debug CI issues
# - name: Setup ccache
# uses: hendrikmuhs/ccache-action@v1.2
# with:
# key: ${{ github.job }}-${{ runner.os }}
# - name: Add ccache to PATH
# run: echo "/usr/local/opt/ccache/libexec" >> $GITHUB_PATH
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
@@ -314,15 +354,9 @@ jobs:
path: |
app/ios/Pods
~/Library/Caches/CocoaPods
lock-file: app/ios/Podfile.lock
- name: Cache DerivedData
uses: actions/cache@v4
with:
path: app/ios/build
key: ${{ runner.os }}-derived-data-${{ env.GH_CACHE_VERSION }}-${{ env.XCODE_VERSION }}-${{ hashFiles('app/ios/Podfile.lock', 'yarn.lock') }}
restore-keys: |
${{ runner.os }}-derived-data-${{ env.GH_CACHE_VERSION }}-${{ env.XCODE_VERSION }}-
${{ runner.os }}-derived-data-${{ env.GH_CACHE_VERSION }}-
lockfile: app/ios/Podfile.lock
# DerivedData caching disabled - caused intermittent build failures due to stale cache
# Pod caching still speeds up pod install significantly
- name: Verify iOS Runtime
run: |
echo "📱 Verifying iOS Runtime availability..."
@@ -344,7 +378,7 @@ jobs:
echo "📦 Installing pods via centralized script…"
BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Setup iOS Simulator
run: |
echo "Setting up iOS Simulator..."

View File

@@ -12,7 +12,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
@@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
@@ -77,7 +77,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts

View File

@@ -12,7 +12,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v4
with:

View File

@@ -27,10 +27,11 @@ on:
paths:
- "packages/mobile-sdk-demo/**"
- "packages/mobile-sdk-alpha/**"
- ".github/workflows/mobile-sdk-e2e.yml"
- ".github/workflows/mobile-sdk-demo-e2e.yml"
jobs:
android-e2e:
name: Android E2E Tests Demo App
# Currently build-only for Android. E2E steps are preserved but skipped (if: false).
# To re-enable full E2E: change `if: false` to `if: true` on emulator steps.
concurrency:
@@ -39,7 +40,7 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -58,6 +59,9 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
- run: corepack enable
- run: corepack prepare yarn@4.6.0 --activate
- name: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:
@@ -65,10 +69,17 @@ jobs:
.yarn/cache
.yarn/install-state.gz
.yarn/unplugged
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
- name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
- name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3
@@ -78,7 +89,7 @@ jobs:
retry_wait_seconds: 5
command: yarn install --immutable --silent
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3
@@ -192,6 +203,7 @@ jobs:
ios-e2e:
timeout-minutes: 60
runs-on: macos-latest-large
name: iOS E2E Tests Demo App
concurrency:
group: ${{ github.workflow }}-ios-${{ github.ref }}
cancel-in-progress: true
@@ -199,7 +211,7 @@ jobs:
IOS_WORKSPACE_PATH: packages/mobile-sdk-demo/ios/SelfDemoApp.xcworkspace
IOS_PROJECT_SCHEME: SelfDemoApp
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -218,6 +230,9 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
- run: corepack enable
- run: corepack prepare yarn@4.6.0 --activate
- name: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:
@@ -225,10 +240,17 @@ jobs:
.yarn/cache
.yarn/install-state.gz
.yarn/unplugged
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
- name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
- name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3
@@ -238,7 +260,7 @@ jobs:
retry_wait_seconds: 5
command: yarn install --immutable --silent
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3
@@ -291,15 +313,9 @@ jobs:
path: |
packages/mobile-sdk-demo/ios/Pods
~/Library/Caches/CocoaPods
lock-file: packages/mobile-sdk-demo/ios/Podfile.lock
- name: Cache DerivedData
uses: actions/cache@v4
with:
path: packages/mobile-sdk-demo/ios/build
key: ${{ runner.os }}-derived-data-${{ env.GH_CACHE_VERSION }}-${{ env.XCODE_VERSION }}-${{ hashFiles('packages/mobile-sdk-demo/ios/Podfile.lock', 'yarn.lock') }}
restore-keys: |
${{ runner.os }}-derived-data-${{ env.GH_CACHE_VERSION }}-${{ env.XCODE_VERSION }}-
${{ runner.os }}-derived-data-${{ env.GH_CACHE_VERSION }}-
lockfile: packages/mobile-sdk-demo/ios/Podfile.lock
# DerivedData caching disabled - caused intermittent build failures due to stale cache
# Pod caching still speeds up pod install significantly
- name: Verify iOS Runtime
run: |
echo "📱 Verifying iOS Runtime availability..."
@@ -320,14 +336,15 @@ jobs:
max_attempts: 3
retry_wait_seconds: 10
command: |
if [ -n "${SELFXYZ_INTERNAL_REPO_PAT}" ]; then
echo "🔑 Using SELFXYZ_INTERNAL_REPO_PAT for private pod access"
echo "::add-mask::${SELFXYZ_INTERNAL_REPO_PAT}"
if [ -n "${SELFXYZ_APP_TOKEN}" ]; then
echo "🔑 Using GitHub App token for private pod access"
echo "::add-mask::${SELFXYZ_APP_TOKEN}"
fi
cd packages/mobile-sdk-demo/ios
pod install
echo "📦 Installing pods via cache-fix script…"
bash scripts/pod-install-with-cache-fix.sh
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
GIT_TERMINAL_PROMPT: 0
- name: Setup iOS Simulator
run: |

View File

@@ -8,11 +8,15 @@ on:
- "sdk/core/package.json"
- "sdk/qrcode/package.json"
- "common/package.json"
- 'packages/mobile-sdk-alpha/package.json'
- "packages/mobile-sdk-alpha/package.json"
- "sdk/qrcode-angular/package.json"
- "contracts/package.json"
workflow_dispatch:
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
detect-changes:
runs-on: ubuntu-latest
@@ -24,7 +28,7 @@ jobs:
qrcode_angular_changed: ${{ steps.check-version.outputs.qrcode_angular_changed }}
msdk_changed: ${{ steps.check-version.outputs.msdk_changed }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 2
@@ -59,7 +63,7 @@ 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
if git diff HEAD^ HEAD -- packages/mobile-sdk-alpha/package.json | grep -q '"version":' || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "msdk_changed=true" >> $GITHUB_OUTPUT
fi
@@ -68,7 +72,7 @@ jobs:
if: needs.detect-changes.outputs.core_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@@ -85,7 +89,6 @@ jobs:
- name: Publish to npm
working-directory: sdk/core
run: |
yarn config set npmScopes.selfxyz.npmAuthToken ${{ secrets.NPM_TOKEN }}
yarn config set npmPublishAccess public
yarn npm publish --access public
env:
@@ -97,7 +100,7 @@ jobs:
if: needs.detect-changes.outputs.qrcode_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@@ -114,7 +117,6 @@ jobs:
- name: Publish to npm
working-directory: sdk/qrcode
run: |
yarn config set npmScopes.selfxyz.npmAuthToken ${{ secrets.NPM_TOKEN }}
yarn config set npmPublishAccess public
yarn npm publish --access public
env:
@@ -126,13 +128,12 @@ jobs:
if: needs.detect-changes.outputs.common_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
registry-url: "https://registry.npmjs.org"
- uses: actions/checkout@v4
- name: Install Dependencies
uses: ./.github/actions/yarn-install
@@ -143,7 +144,6 @@ jobs:
- name: Publish to npm
working-directory: common
run: |
yarn config set npmScopes.selfxyz.npmAuthToken ${{ secrets.NPM_TOKEN }}
yarn config set npmPublishAccess public
yarn npm publish --access public
env:
@@ -154,13 +154,12 @@ jobs:
if: needs.detect-changes.outputs.contracts_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
registry-url: "https://registry.npmjs.org"
- uses: actions/checkout@v4
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Build package
@@ -169,7 +168,6 @@ jobs:
- name: Publish to npm
working-directory: contracts
run: |
yarn config set npmScopes.selfxyz.npmAuthToken ${{ secrets.NPM_TOKEN }}
yarn config set npmPublishAccess public
yarn npm publish --access public
env:
@@ -180,7 +178,7 @@ jobs:
if: needs.detect-changes.outputs.qrcode_angular_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@@ -197,7 +195,6 @@ jobs:
- name: Publish to npm
working-directory: sdk/qrcode-angular
run: |
yarn config set npmScopes.selfxyz.npmAuthToken ${{ secrets.NPM_TOKEN }}
yarn config set npmPublishAccess public
yarn npm publish --access public
env:
@@ -209,7 +206,7 @@ jobs:
if: needs.detect-changes.outputs.msdk_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@@ -227,7 +224,6 @@ jobs:
- 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:

View File

@@ -25,7 +25,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -68,6 +68,7 @@ jobs:
shell: bash
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/sdk-common build
yarn workspace @selfxyz/qrcode build
- name: Cache build artifacts
@@ -75,6 +76,7 @@ jobs:
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
@@ -83,7 +85,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -127,6 +129,7 @@ jobs:
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
fail-on-cache-miss: true
@@ -150,7 +153,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -194,6 +197,7 @@ jobs:
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
fail-on-cache-miss: true
@@ -202,6 +206,7 @@ jobs:
run: |
echo "Verifying build artifacts..."
ls -la common/dist/
ls -la sdk/sdk-common/dist/
ls -la sdk/qrcode/dist/
echo "✅ Build artifacts verified"
@@ -210,7 +215,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -254,6 +259,7 @@ jobs:
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
fail-on-cache-miss: true

View File

@@ -77,7 +77,7 @@ jobs:
- name: Check out repository
if: ${{ steps.guard_schedule.outputs.continue == 'true' }}
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -256,7 +256,7 @@ jobs:
- name: Check out repository
if: ${{ steps.guard_schedule.outputs.continue == 'true' }}
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
if: false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies

View File

@@ -18,7 +18,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
@@ -47,7 +47,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
@@ -76,7 +76,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
@@ -106,7 +106,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
@@ -147,7 +147,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
@@ -176,7 +176,7 @@ jobs:
# permissions:
# contents: read
# steps:
# - uses: actions/checkout@v4
# - uses: actions/checkout@v6
# - name: Cache Yarn dependencies
# uses: ./.github/actions/cache-yarn

5
.gitignore vendored
View File

@@ -24,3 +24,8 @@ packages/mobile-sdk-alpha/docs/docstrings-report.json
# Private Android modules (cloned at build time)
app/android/android-passport-nfc-reader/
# Foundry
contracts/out/
contracts/cache_forge/
contracts/broadcast/

File diff suppressed because it is too large Load Diff

View File

@@ -3,3 +3,8 @@
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
0e4555eee6589aa9cca68f451227b149277d8c90:app/tests/src/utils/points/api.test.ts:generic-api-key:34
circuits/circuits/gcp_jwt_verifier/example_jwt.txt:jwt:1
cadd7ae5b768c261230f84426eac879c1853ce70:app/ios/Podfile.lock:generic-api-key:2586
aeb8287078f088ecd8e9430e3f6a9f2c593ef1fc:app/src/utils/points/constants.ts:generic-api-key:7
app/src/services/points/constants.ts:generic-api-key:10

7
.gitmodules vendored
View File

@@ -1,3 +1,10 @@
[submodule "contracts/lib/openzeppelin-foundry-upgrades"]
path = contracts/lib/openzeppelin-foundry-upgrades
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
[submodule "contracts/lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[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

View File

@@ -1,5 +1,11 @@
nodeLinker: node-modules
nmHoistingLimits: workspaces
checksumBehavior: update
enableGlobalCache: true
enableScripts: true
checksumBehavior: "update"
nmHoistingLimits: workspaces
nodeLinker: node-modules
npmPublishAccess: public

View File

@@ -130,6 +130,20 @@ yarn types # Verify type checking
- For Noir circuits, run `nargo test -p <crate>` in each `noir/crates/*` directory.
- Tests for `@selfxyz/contracts` are currently disabled in CI and may be skipped.
- E2E tests (mobile app) - **Run automatically in CI/CD, not required locally**:
- E2E tests execute automatically in GitHub Actions on PRs and main branch
- Local E2E testing is optional (see `app/AGENTS.md` for local setup if needed)
- Commands available: `yarn workspace @selfxyz/mobile-app test:e2e:ios` / `test:e2e:android`
#### Test Memory Optimization
**CRITICAL**: Never create nested `require('react-native')` calls in tests. This causes out-of-memory (OOM) errors in CI/CD pipelines.
- Use ES6 `import` statements instead of `require()` when possible
- Avoid dynamic `require()` calls in `beforeEach`/`afterEach` hooks
- Prefer top-level imports over nested requires
- See `.cursor/rules/test-memory-optimization.mdc` for detailed guidelines
### CI Caching
Use the shared composite actions in `.github/actions` when caching dependencies in GitHub workflows. They provide consistent cache paths and keys:
@@ -151,6 +165,43 @@ Each action accepts an optional `cache-version` input (often combined with `GH_C
- Write short, imperative commit messages (e.g. `Fix address validation`).
- The pull request body should summarize the changes and mention test results.
## Workspace-Specific Instructions
Some workspaces have additional instructions in their own `AGENTS.md` files:
- `app/AGENTS.md` - Mobile app development, E2E testing, deployment
- `packages/mobile-sdk-alpha/AGENTS.md` - SDK development, testing guidelines, package validation
- `noir/AGENTS.md` - Noir circuit development
These workspace-specific files override or extend the root instructions for their respective areas.
## Troubleshooting
### Common Issues
#### Yarn Install Fails
- Ensure Node.js 22.x is installed: `nvm use`
- Clear Yarn cache: `yarn cache clean`
- Remove `node_modules` and reinstall: `rm -rf node_modules && yarn install`
#### Build Failures
- Run `yarn build:deps` in affected workspace first
- Check workspace-specific `AGENTS.md` for platform requirements
- For mobile app: ensure iOS/Android prerequisites are met (see `app/AGENTS.md`)
#### Test Failures
- Check workspace-specific test setup requirements
- For mobile app tests: ensure native modules are properly mocked
- See `.cursor/rules/test-memory-optimization.mdc` for test memory issues
#### Type Errors
- Run `yarn types` to see all type errors across workspaces
- Some packages may need to be built first: `yarn build:deps`
## Scope
These instructions apply to the entire repository unless overridden by a nested `AGENTS.md`.

View File

@@ -24,7 +24,7 @@ Currently, Self supports electronic passports, biometric ID cards following the
**Passports:** Biometric passports have the [biometric passport logo](https://en.wikipedia.org/wiki/Biometric_passport) on their front cover.
**Aadhaar:** Indian [Aadhaar](https://en.wikipedia.org/wiki/Aadhaar) cards are supported for privacy-preserving identity verification.
**Aadhaar:** Indian [Aadhaar](https://en.wikipedia.org/wiki/Aadhaar) cards are supported for privacy-preserving identity verification. Use the mAadhaar app to generate a QR code and import it into Self.
**Coverage:** Checkout our [coverage map here](http://map.self.xyz/) to see supported documents and countries.
@@ -77,6 +77,8 @@ Gitleaks will scan staged changes on each commit via `yarn gitleaks`.
## Development Documentation
> **Note:** We do not accept text-only pull request changes. While we appreciate the feedback, we will not merge external pull requests that only modify markdown files or code comments (e.g., typo fixes in documentation or comments). Pull requests must include functional code changes.
For detailed development patterns and conventions, see:
- **[Development Patterns](docs/development-patterns.md)** - React Native architecture, navigation, state management, and code organization
@@ -88,12 +90,15 @@ These guides provide comprehensive context for AI-assisted development with Chat
We are actively looking for contributors. Please check the [open issues](https://github.com/selfxyz/self/issues) if you don't know where to start! We offer bounties for significant contributions.
> **Important:** Please open your pull request from the `staging` branch. Pull requests from other branches will be automatically closed.
> **Important:** Please read and follow the guidelines in [contribute.md](contribute.md) when opening your pull request.
## Contact us
[Contact us](https://t.me/selfprotocolbuilder) on telegram for feedback or questions.
- [Discord](https://discord.gg/AQ3TrX6dce) for technical support or reporting a bug.
- [Telegram's Self builder channel](https://t.me/selfprotocolbuilder) for technical questions about the sdk implementation.
- [Telegram's Self public group](https://t.me/selfxyz) for general questions and updates.
Thanks [Rémi](https://github.com/remicolin), [Florent](https://github.com/0xturboblitz), [Ayman](https://github.com/Nesopie), [Justin](https://github.com/transphorm), [Seshanth](https://github.com/seshanthS), [Nico](https://github.com/motemotech) and all other contributors for building Self.
Thanks [Aayush](https://twitter.com/yush_g), [Vivek](https://twitter.com/viv_boop), [Andy](https://twitter.com/AndyGuzmanEth) and [Vitalik](https://github.com/vbuterin) for contributing ideas and inspiring us to build this technology, and [PSE](https://pse.dev/) for supporting the initial work through grants!

View File

@@ -165,6 +165,18 @@ module.exports = {
},
],
// Custom rule to prevent export * (bad for tree shaking)
// This rule prevents the use of export * which disables tree shaking
// and can significantly increase bundle size. Use selective exports instead.
'no-restricted-syntax': [
'error',
{
selector: 'ExportAllDeclaration',
message:
'export * is forbidden. Use selective exports for better tree shaking. Example: export { specific1, specific2 } from "./module"',
},
],
// Override rules conflicting with TypeScript union formatting
'@typescript-eslint/indent': 'off',
@@ -191,11 +203,11 @@ module.exports = {
{
// Disable export sorting for files with dependency issues
files: [
'src/components/NavBar/BaseNavBar.tsx',
'src/components/navbar/BaseNavBar.tsx',
'src/navigation/index.tsx',
'src/providers/passportDataProvider.tsx',
'src/utils/cloudBackup/helpers.ts',
'src/utils/haptic/index.ts',
'src/services/cloud-backup/helpers.ts',
'src/integrations/haptics/index.ts',
],
rules: {
'sort-exports/sort-exports': 'off',
@@ -213,10 +225,54 @@ module.exports = {
rules: {
// Allow console logging and relaxed typing in tests
'no-console': 'off',
// Allow require() imports in tests for mocking
// Allow require() imports in tests for mocking, but block react/react-native
'@typescript-eslint/no-require-imports': 'off',
// Block require('react') and require('react-native') to prevent OOM issues
'no-restricted-syntax': [
'error',
{
selector:
"CallExpression[callee.name='require'][arguments.0.value='react']",
message:
"Do not use require('react') in tests. Use 'import React from \"react\"' at the top of the file to avoid out-of-memory issues in CI.",
},
{
selector:
"CallExpression[callee.name='require'][arguments.0.value='react-native']",
message:
"Do not use require('react-native') in tests. Use 'import { ... } from \"react-native\"' at the top of the file to avoid out-of-memory issues in CI.",
},
],
// Allow any types in tests for mocking
'@typescript-eslint/no-explicit-any': 'off',
// Allow test skipping without warnings
'jest/no-disabled-tests': 'off',
},
},
{
files: ['tests/**/*.js'],
env: {
jest: true,
},
rules: {
// Allow console logging in test JS files
'no-console': 'off',
// Block require('react') and require('react-native') to prevent OOM issues
'no-restricted-syntax': [
'error',
{
selector:
"CallExpression[callee.name='require'][arguments.0.value='react']",
message:
"Do not use require('react') in tests. Use 'import React from \"react\"' at the top of the file to avoid out-of-memory issues in CI.",
},
{
selector:
"CallExpression[callee.name='require'][arguments.0.value='react-native']",
message:
"Do not use require('react-native') in tests. Use 'import { ... } from \"react-native\"' at the top of the file to avoid out-of-memory issues in CI.",
},
],
},
},
{
@@ -226,13 +282,6 @@ module.exports = {
'no-console': 'off',
},
},
{
// Allow require imports for dynamic imports in proving machine
files: ['src/utils/proving/provingMachine.ts'],
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
},
{
// Allow require imports for conditional loading in navigation
files: ['src/navigation/index.tsx'],

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v4

View File

@@ -18,6 +18,7 @@ Before creating a PR for the mobile app:
- [ ] `yarn nice` passes (fixes linting and formatting)
- [ ] `yarn types` passes (TypeScript validation)
- [ ] `yarn test` passes (unit tests)
- [ ] No nested `require('react-native')` calls in tests (causes OOM in CI) - check with `grep -r "require('react-native')" app/tests/` and verify no nested patterns
- [ ] App builds successfully on target platforms
### Mobile-Specific Validation
@@ -25,12 +26,14 @@ Before creating a PR for the mobile app:
- [ ] Android build succeeds: `yarn android` (emulator/device)
- [ ] Web build succeeds: `yarn web`
- [ ] No sensitive data in logs (PII, credentials, tokens)
- [ ] Environment variables properly configured (check `.env` setup)
- [ ] E2E tests run in CI (not required locally - CI will run E2E tests automatically)
### AI Review Preparation
- [ ] Complex native module changes documented
- [ ] Platform-specific code paths explained
- [ ] Security-sensitive operations flagged
- [ ] Performance implications noted
- [ ] Performance implications noted (including test memory patterns if tests were modified)
## Post-PR Validation
@@ -45,8 +48,11 @@ After PR creation:
### Mobile-Specific Checks
- [ ] App launches without crashes
- [ ] Core functionality works on target platforms
- [ ] No memory leaks introduced
- [ ] No memory leaks introduced (including test memory patterns - see Test Memory Optimization section)
- [ ] Bundle size within acceptable limits
- [ ] No nested `require('react-native')` calls in tests (causes OOM in CI)
- [ ] Native modules work correctly (if native code was modified)
- [ ] Platform-specific code paths tested (iOS/Android/Web)
### Review Integration
- [ ] Address CodeRabbitAI feedback
@@ -92,4 +98,143 @@ yarn types # Verify type checking
## Running the App
- `yarn ios`
- `yarn ios` - Run on iOS simulator (builds dependencies automatically)
- `yarn android` - Run on Android emulator/device (builds dependencies automatically)
- `yarn web` - Run web version
### Development Tips
- Use `yarn build:deps` to build all workspace dependencies before running the app
- For iOS: Ensure Xcode scheme is set to "OpenPassport" (see memory)
- For Android: Ensure emulator is running or device is connected before `yarn android`
- Metro bundler starts automatically; use `yarn start` to run it separately
## E2E Testing
The app uses Maestro for end-to-end testing. **E2E tests run automatically in CI/CD pipelines - they are not required to run locally.**
### CI/CD E2E Testing
- E2E tests run automatically in GitHub Actions workflows
- iOS and Android E2E tests run on PRs and main branch
- No local setup required - CI handles all E2E test execution
### Local E2E Testing (Optional)
If you need to run E2E tests locally for debugging:
**Prerequisites:**
- Maestro CLI installed: `curl -Ls "https://get.maestro.mobile.dev" | bash`
- iOS: Simulator running or device connected
- Android: Emulator running or device connected
- App built and installed on target device/simulator
**Running Locally:**
```bash
# iOS E2E tests
yarn test:e2e:ios
# Android E2E tests
yarn test:e2e:android
# Or use the local test script (handles setup automatically)
./scripts/test-e2e-local.sh ios
./scripts/test-e2e-local.sh android
```
**E2E Test Files:**
- iOS: `tests/e2e/launch.ios.flow.yaml`
- Android: `tests/e2e/launch.android.flow.yaml`
## Environment Variables
The app uses `react-native-dotenv` for environment configuration.
### Setup
- Create `.env` file in `app/` directory (see `.env.example` if available)
- Environment variables are loaded via `@env` import
- For secrets: Use `.env.secrets` (gitignored) for local development
- In CI: Environment variables are set in workflow files
### Common Environment Variables
- `GOOGLE_SIGNIN_ANDROID_CLIENT_ID` - Google Sign-In configuration
- Various API endpoints and keys (check `app/env.ts` for full list)
### Testing with Environment Variables
- Tests use mocked environment variables (see `jest.setup.js`)
- E2E tests use actual environment configuration
- Never commit `.env.secrets` or sensitive values
## Deployment
### Mobile Deployment
The app uses Fastlane for iOS and Android deployment.
### Deployment Commands
```bash
# Deploy both platforms (requires confirmation)
yarn mobile-deploy
# Deploy iOS only
yarn mobile-deploy:ios
# Deploy Android only
yarn mobile-deploy:android
# Force local deployment (for testing deployment scripts)
yarn mobile-local-deploy
```
### Deployment Prerequisites
- See `app/docs/MOBILE_DEPLOYMENT.md` for detailed deployment guide
- Required secrets configured in CI/CD or `.env.secrets` for local
- iOS: App Store Connect API keys, certificates, provisioning profiles
- Android: Play Store service account, keystore
### Deployment Checklist
- [ ] Version bumped in `package.json` and `app.json`
- [ ] Changelog updated
- [ ] All unit tests pass (`yarn test`)
- [ ] Build succeeds for target platform
- [ ] Required secrets/environment variables configured
- [ ] Fastlane configuration verified
- [ ] CI E2E tests pass (automatically run in CI, no local action needed)
## Test Memory Optimization
**CRITICAL**: Never create nested `require('react')` or `require('react-native')` calls in tests. This causes out-of-memory (OOM) errors in CI/CD pipelines that hide actual test failures.
### Automated Enforcement
The project has multiple layers of protection:
1. **ESLint Rule**: Blocks `require('react')` and `require('react-native')` in test files
2. **Pre-commit Script**: Run `node scripts/check-test-requires.cjs` to validate
3. **CI Fast-Fail**: GitHub Actions checks for nested requires before running tests
### Quick Check
Before committing, verify no nested requires:
```bash
# Automated check (recommended)
node scripts/check-test-requires.cjs
# Manual check
grep -r "require('react')" app/tests/
grep -r "require('react-native')" app/tests/
```
### Best Practices
- **Always use ES6 `import` statements** - Never use `require('react')` or `require('react-native')` in test files
- Put all imports at the top of the file - No dynamic imports in hooks
- Avoid `require()` calls in `beforeEach`/`afterEach` hooks
- React and React Native are already mocked in `jest.setup.js` - use imports in test files
### Detailed Guidelines
See `.cursor/rules/test-memory-optimization.mdc` for comprehensive guidelines, examples, and anti-patterns.

View File

@@ -5,9 +5,25 @@
// CI/CD Pipeline Test - July 31, 2025 - With Permissions Fix
import { Buffer } from 'buffer';
import React from 'react';
import { Platform } from 'react-native';
import { YStack } from 'tamagui';
import type {
TurnkeyCallbacks,
TurnkeyProviderConfig,
} from '@turnkey/react-native-wallet-kit';
import { TurnkeyProvider } from '@turnkey/react-native-wallet-kit';
import {
TURNKEY_AUTH_PROXY_CONFIG_ID,
TURNKEY_GOOGLE_CLIENT_ID,
TURNKEY_ORGANIZATION_ID,
} from './env';
import ErrorBoundary from './src/components/ErrorBoundary';
import { initSentry, wrapWithSentry } from './src/config/sentry';
import {
TURNKEY_OAUTH_REDIRECT_URI_ANDROID,
TURNKEY_OAUTH_REDIRECT_URI_IOS,
} from './src/devtools/mocks';
import AppNavigation from './src/navigation';
import { AuthProvider } from './src/providers/authProvider';
import { DatabaseProvider } from './src/providers/databaseProvider';
@@ -17,12 +33,63 @@ import { NotificationTrackingProvider } from './src/providers/notificationTracki
import { PassportProvider } from './src/providers/passportDataProvider';
import { RemoteConfigProvider } from './src/providers/remoteConfigProvider';
import { SelfClientProvider } from './src/providers/selfClientProvider';
import { initSentry, wrapWithSentry } from './src/Sentry';
import 'react-native-get-random-values';
import 'react-native-url-polyfill/auto';
import '@walletconnect/react-native-compat';
import '@noble/curves/p256';
import 'sha256-uint8array';
import '@turnkey/encoding';
import '@turnkey/api-key-stamper';
initSentry();
global.Buffer = Buffer;
export const TURNKEY_CALLBACKS: TurnkeyCallbacks = {
beforeSessionExpiry: ({ sessionKey: _sessionKey }) => {
console.log('[Turnkey] Session nearing expiry');
},
onSessionExpired: ({ sessionKey: _sessionKey }) => {
console.log('[Turnkey] Session expired');
},
onAuthenticationSuccess: ({
action: _action,
method: _method,
identifier: _identifier,
}) => {
// console.log('[Turnkey] Auth success:', { action, method, identifier });
},
onError: error => {
console.error('[Turnkey] Error:', error);
},
};
export const TURNKEY_CONFIG: TurnkeyProviderConfig = {
organizationId: TURNKEY_ORGANIZATION_ID!,
authProxyConfigId: TURNKEY_AUTH_PROXY_CONFIG_ID!,
autoRefreshManagedState: false,
auth: {
passkey: false,
oauth: {
// Should use custom scheme, NOT 'https' for IOS
appScheme:
Platform.OS === 'ios' ? 'com.warroom.proofofpassport' : 'https',
redirectUri:
Platform.OS === 'ios'
? TURNKEY_OAUTH_REDIRECT_URI_IOS
: TURNKEY_OAUTH_REDIRECT_URI_ANDROID,
google: {
clientId: TURNKEY_GOOGLE_CLIENT_ID!,
redirectUri:
Platform.OS === 'ios'
? TURNKEY_OAUTH_REDIRECT_URI_IOS
: TURNKEY_OAUTH_REDIRECT_URI_ANDROID,
},
},
},
};
function App(): React.JSX.Element {
return (
<ErrorBoundary>
@@ -35,7 +102,12 @@ function App(): React.JSX.Element {
<DatabaseProvider>
<NotificationTrackingProvider>
<FeedbackProvider>
<AppNavigation />
<TurnkeyProvider
config={TURNKEY_CONFIG}
callbacks={TURNKEY_CALLBACKS}
>
<AppNavigation />
</TurnkeyProvider>
</FeedbackProvider>
</NotificationTrackingProvider>
</DatabaseProvider>

View File

@@ -1,10 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
CFPropertyList (3.0.8)
activesupport (7.2.3)
base64
benchmark (>= 0.3)
@@ -17,16 +14,16 @@ GEM
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1178.0)
aws-sdk-core (3.235.0)
aws-partitions (1.1194.0)
aws-sdk-core (3.239.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -34,10 +31,10 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.115.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.202.0)
aws-sdk-s3 (1.206.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@@ -89,8 +86,8 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
@@ -114,9 +111,9 @@ GEM
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
@@ -225,27 +222,26 @@ GEM
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.15.2)
json (2.18.0)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.26.0)
minitest (5.27.0)
molinillo (0.8.0)
multi_json (1.17.0)
multi_json (1.18.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
nap (1.1.0)
naturally (2.3.0)
netrc (0.11.0)
nkf (0.2.0)
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
optparse (0.6.0)
optparse (0.8.1)
os (1.1.4)
plist (3.7.2)
public_suffix (4.0.7)

View File

@@ -1,4 +1,4 @@
# OpenPassport App
# Self.xyz Mobile App
## Requirements

View File

@@ -1 +1,8 @@
build
build
# Private modules cloned dynamically by setup-private-modules.cjs
android-passport-nfc-reader/
react-native-passport-reader/
# Temporary credential helper scripts created during CI setup
.git-credential-helper-*.sh

View File

@@ -125,12 +125,17 @@ android {
preDexLibraries false
}
buildFeatures {
buildConfig = true
viewBinding = true
}
defaultConfig {
applicationId "com.proofofpassportapp"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 113
versionName "2.7.3"
versionCode 121
versionName "2.9.5"
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
externalNativeBuild {
cmake {

View File

@@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools"
>
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="whatsapp" />
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />

Binary file not shown.

View File

@@ -4,11 +4,11 @@ buildscript {
ext {
buildToolsVersion = "35.0.0"
minSdkVersion = 24
compileSdkVersion = 35
targetSdkVersion = 35
compileSdkVersion = 36
targetSdkVersion = 36
// Updated NDK to support 16k page size devices
ndkVersion = "27.0.12077973"
kotlinVersion = "1.9.24"
kotlinVersion = "2.0.0"
firebaseMessagingVersion = "23.4.0"
firebaseBomVersion = "32.7.3"
}

View File

@@ -1,2 +0,0 @@
node_modules/
android/src/main/jniLibs/arm64-v8a/libark_circom_passport.so

View File

@@ -1,15 +0,0 @@
## License
Apache License, Version 2.0
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.

View File

@@ -1,61 +0,0 @@
# react-native-passport-reader
Adapted from [passport-reader](https://github.com/tananaev/passport-reader). Individual modifications are too many to enumerate, but essentially: the workflow code was adapted to the needs of a React Native module, and the scanning code was largely left as is.
## Getting started
```sh
$ npm install react-native-passport-reader --save
$ react-native link react-native-passport-reader
```
In your `android/app/build.gradle` add `packagingOptions`:
```
android {
...
packagingOptions {
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
}
}
```
In `AndroidManifest.xml` add:
```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />
```
If your app will not function without nfc capabilities, set `android:required` above to `true`
## Usage
```js
import PassportReader from 'react-native-passport-reader'
// { scan, cancel, isSupported }
async function scan () {
// 1. start a scan
// 2. press the back of your android phone against the passport
// 3. wait for the scan(...) Promise to get resolved/rejected
const {
firstName,
lastName,
gender,
issuer,
nationality,
photo
} = await PassportReader.scan({
// yes, you need to know a bunch of data up front
// this is data you can get from reading the MRZ zone of the passport
documentNumber: 'ofDocumentBeingScanned',
dateOfBirth: 'yyMMdd',
dateOfExpiry: 'yyMMdd'
})
const { base64, width, height } = photo
}
```

View File

@@ -1,48 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace "io.tradle.nfc"
// Use NDK that supports 16k page size
ndkVersion = "27.0.12077973"
compileSdkVersion 35
defaultConfig {
minSdkVersion 21
targetSdkVersion 35
versionCode 1
versionName "1.0"
multiDexEnabled = true
}
packagingOptions {
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
}
lintOptions {
warning 'InvalidPackage'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation 'com.google.code.gson:gson:2.8.9' // Check for the latest version
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'com.wdullaer:materialdatetimepicker:3.5.2'
implementation 'org.jmrtd:jmrtd:0.8.1'
implementation 'net.sf.scuba:scuba-sc-android:0.0.18'
implementation 'com.gemalto.jp2:jp2-android:1.0.3'
implementation 'com.github.mhshams:jnbis:1.1.0'
implementation 'commons-io:commons-io:2.8.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.facebook.react:react-native:+'
implementation "io.sentry:sentry-android:8.20.0"
}

View File

@@ -1,5 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />
</manifest>

View File

@@ -1,114 +0,0 @@
package io.tradle.nfc
import net.sf.scuba.smartcards.APDUEvent
import net.sf.scuba.smartcards.APDUListener
import net.sf.scuba.smartcards.CommandAPDU
import net.sf.scuba.smartcards.ResponseAPDU
import org.jmrtd.WrappedAPDUEvent
import android.util.Log
class APDULogger : APDUListener {
private var moduleReference: RNPassportReaderModule? = null
private val sessionContext = mutableMapOf<String, Any>()
fun setModuleReference(module: RNPassportReaderModule) {
moduleReference = module
}
fun setContext(key: String, value: Any) {
sessionContext[key] = value
}
fun clearContext() {
sessionContext.clear()
}
override fun exchangedAPDU(event: APDUEvent) {
try {
val entry = createLogEntry(event)
logToAnalytics(entry)
} catch (e: Exception) {
Log.e("APDULogger", "Error exchanging APDU", e)
}
}
private fun createLogEntry(event: APDUEvent): APDULogEntry {
val command = event.commandAPDU
val response = event.responseAPDU
val timestamp = System.currentTimeMillis()
val entry = APDULogEntry(
timestamp = timestamp,
commandHex = command.bytes.toHexString(),
responseHex = response.bytes.toHexString(),
statusWord = response.sw,
statusWordHex = "0x${response.sw.toString(16).uppercase().padStart(4, '0')}",
commandLength = command.bytes.size,
responseLength = response.bytes.size,
dataLength = response.data.size,
isWrapped = event is WrappedAPDUEvent,
plainCommandHex = if (event is WrappedAPDUEvent) event.plainTextCommandAPDU.bytes.toHexString() else null,
plainResponseHex = if (event is WrappedAPDUEvent) event.plainTextResponseAPDU.bytes.toHexString() else null,
plainCommandLength = if (event is WrappedAPDUEvent) event.plainTextCommandAPDU.bytes.size else null,
plainResponseLength = if (event is WrappedAPDUEvent) event.plainTextResponseAPDU.bytes.size else null,
plainDataLength = if (event is WrappedAPDUEvent) event.plainTextResponseAPDU.data.size else null,
context = sessionContext.toMap()
)
return entry
}
private fun ByteArray.toHexString(): String {
return joinToString("") { "%02X".format(it) }
}
private fun logToAnalytics(entry: APDULogEntry) {
try {
val params = mutableMapOf<String, Any>().apply {
put("timestamp", entry.timestamp)
put("command_hex", entry.commandHex)
put("response_hex", entry.responseHex)
put("status_word", entry.statusWord)
put("status_word_hex", entry.statusWordHex)
put("command_length", entry.commandLength)
put("response_length", entry.responseLength)
put("data_length", entry.dataLength)
put("is_wrapped", entry.isWrapped)
put("context", entry.context)
entry.plainCommandHex?.let { put("plain_command_hex", it) }
entry.plainResponseHex?.let { put("plain_response_hex", it) }
entry.plainCommandLength?.let { put("plain_command_length", it) }
entry.plainResponseLength?.let { put("plain_response_length", it) }
entry.plainDataLength?.let { put("plain_data_length", it) }
}
moduleReference?.logAnalyticsEvent("nfc_apdu_exchange", params)
} catch (e: Exception) {
Log.e("APDULogger", "Error logging to analytics", e)
}
}
}
data class APDULogEntry(
val timestamp: Long,
val commandHex: String,
val responseHex: String,
val statusWord: Int,
val statusWordHex: String,
val commandLength: Int,
val responseLength: Int,
val dataLength: Int,
val isWrapped: Boolean,
val plainCommandHex: String?,
val plainResponseHex: String?,
val plainCommandLength: Int?,
val plainResponseLength: Int?,
val plainDataLength: Int?,
val context: Map<String, Any>
)

View File

@@ -1,57 +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.
*/
package io.tradle.nfc
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.gemalto.jp2.JP2Decoder
import org.jnbis.WsqDecoder
import java.io.InputStream
object ImageUtil {
fun decodeImage(context: Context?, mimeType: String, inputStream: InputStream?): Bitmap {
return if (mimeType.equals("image/jp2", ignoreCase = true) || mimeType.equals(
"image/jpeg2000",
ignoreCase = true
)
) {
JP2Decoder(inputStream).decode()
} else if (mimeType.equals("image/x-wsq", ignoreCase = true)) {
val wsqDecoder = WsqDecoder()
val bitmap = wsqDecoder.decode(inputStream)
val byteData = bitmap.pixels
val intData = IntArray(byteData.size)
for (j in byteData.indices) {
intData[j] = 0xFF000000.toInt() or
(byteData[j].toInt() and 0xFF shl 16) or
(byteData[j].toInt() and 0xFF shl 8) or
(byteData[j].toInt() and 0xFF)
}
Bitmap.createBitmap(
intData,
0,
bitmap.width,
bitmap.width,
bitmap.height,
Bitmap.Config.ARGB_8888
)
} else {
BitmapFactory.decodeStream(inputStream)
}
}
}

View File

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

View File

@@ -1,30 +0,0 @@
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
import { NativeModules } from 'react-native'
const { RNPassportReader } = NativeModules
const DATE_REGEX = /^\d{6}$/
module.exports = {
...RNPassportReader,
scan,
reset: RNPassportReader.reset
}
function scan({ documentNumber, dateOfBirth, dateOfExpiry, canNumber, useCan, quality=1 }) {
assert(typeof documentNumber === 'string', 'expected string "documentNumber"')
assert(isDate(dateOfBirth), 'expected string "dateOfBirth" in format "yyMMdd"')
assert(isDate(dateOfExpiry), 'expected string "dateOfExpiry" in format "yyMMdd"')
return RNPassportReader.scan({ documentNumber, dateOfBirth, dateOfExpiry, quality, useCan, canNumber })
}
function assert (statement, err) {
if (!statement) {
throw new Error(err || 'Assertion failed')
}
}
function isDate (str) {
return typeof str === 'string' && DATE_REGEX.test(str)
}

View File

@@ -1,15 +0,0 @@
{
"name": "react-native-passport-reader",
"version": "1.0.3",
"description": "read the NFC chip in a passport",
"keywords": [
"react-native",
"react-component",
"nfc",
"android",
"scanner"
],
"license": "APLv2",
"author": "Mark Vayngrib <mark@tradle.io> (http://github.com/mvayngrib)",
"main": "index.android.js"
}

View File

@@ -13,6 +13,7 @@ module.exports = {
},
],
['@babel/plugin-transform-private-methods', { loose: true }],
'@babel/plugin-transform-export-namespace-from',
[
'module:react-native-dotenv',
{
@@ -25,4 +26,9 @@ module.exports = {
},
],
],
env: {
production: {
plugins: ['transform-remove-console'],
},
},
};

71
app/babel.config.test.cjs Normal file
View File

@@ -0,0 +1,71 @@
// 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.
// Babel config for Jest tests that excludes hermes-parser to avoid WebAssembly issues
// Based on React Native babel preset but with hermes parser plugin removed
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
'@babel/preset-typescript',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
],
plugins: [
// Module resolver for @ alias
[
'module-resolver',
{
root: ['./src'],
alias: { '@': './src' },
},
],
// Core React Native transforms (minimal set needed for tests)
['@babel/plugin-transform-class-properties', { loose: true }],
['@babel/plugin-transform-classes', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }],
['@babel/plugin-transform-private-property-in-object', { loose: true }],
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-export-default-from',
'@babel/plugin-transform-export-namespace-from',
'@babel/plugin-transform-unicode-regex',
['@babel/plugin-transform-destructuring', { useBuiltIns: true }],
'@babel/plugin-transform-spread',
[
'@babel/plugin-transform-object-rest-spread',
{ loose: true, useBuiltIns: true },
],
['@babel/plugin-transform-optional-chaining', { loose: true }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
['@babel/plugin-transform-logical-assignment-operators', { loose: true }],
// Flow type stripping to support React Native's Flow-based sources
['@babel/plugin-syntax-flow'],
['@babel/plugin-transform-flow-strip-types', { allowDeclareFields: true }],
// Environment variable support
[
'module:react-native-dotenv',
{
moduleName: '@env',
path: '.env',
blacklist: null,
whitelist: null,
safe: false,
allowUndefined: true,
},
],
],
};

View File

@@ -1,5 +1,4 @@
ENABLE_DEBUG_LOGS=
GITGUARDIAN_API_KEY=
GITLEAKS_LICENSE=
GOOGLE_SIGNIN_ANDROID_CLIENT_ID=
GRAFANA_LOKI_URL=

View File

@@ -28,3 +28,9 @@ export const IS_TEST_BUILD = process.env.IS_TEST_BUILD === 'true';
export const MIXPANEL_NFC_PROJECT_TOKEN = undefined;
export const SEGMENT_KEY = process.env.SEGMENT_KEY;
export const SENTRY_DSN = process.env.SENTRY_DSN;
export const TURNKEY_AUTH_PROXY_CONFIG_ID =
process.env.TURNKEY_AUTH_PROXY_CONFIG_ID;
export const TURNKEY_GOOGLE_CLIENT_ID = process.env.TURNKEY_GOOGLE_CLIENT_ID;
export const TURNKEY_ORGANIZATION_ID = process.env.TURNKEY_ORGANIZATION_ID;

View File

@@ -306,6 +306,14 @@ platform :android do
skip_upload = options[:skip_upload] == true || options[:skip_upload] == "true"
version_bump = options[:version_bump] || "build"
# Automatically skip uploads in local development (no local permissions for Play Store)
# Uploads must be done by CI/CD machines with proper authentication
if local_development && !skip_upload
skip_upload = true
UI.important("🏠 LOCAL DEVELOPMENT: Automatically skipping Play Store upload")
UI.important(" Uploads require CI/CD machine permissions and will be handled automatically")
end
if local_development
if ENV["ANDROID_KEYSTORE_PATH"].nil?
ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path)

View File

@@ -21,7 +21,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
[bundle exec] fastlane ios sync_version
```
Sync ios version
Sync ios version (DEPRECATED)
### ios internal_test
@@ -50,7 +50,7 @@ Deploy iOS app with automatic version management
[bundle exec] fastlane android sync_version
```
Sync android version
Sync android version (DEPRECATED)
### android internal_test

View File

@@ -19,7 +19,7 @@ import App from './App';
import { name as appName } from './app.json';
import tamaguiConfig from './tamagui.config';
import './src/utils/ethers';
import './src/utils/crypto/ethers';
import 'react-native-gesture-handler';
// Set global Buffer before any other imports

View File

@@ -19,23 +19,10 @@
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
// Request permission for notifications
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (error) {
NSLog(@"Failed to request notification authorization: %@", error.localizedDescription);
return;
}
if (granted) {
NSLog(@"Notification authorization granted");
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] registerForRemoteNotifications];
});
} else {
NSLog(@"Notification authorization denied by user");
}
}];
// NOTE: Notification permission request removed from app launch
// Permission is now requested only when user explicitly enables notifications
// (e.g., in Points screen or settings)
// The auto-request was causing unwanted permission prompts on first app launch
}
self.moduleName = @"OpenPassport";

View File

@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.7.3</string>
<string>2.9.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -30,6 +30,7 @@
<key>CFBundleURLSchemes</key>
<array>
<string>proofofpassport</string>
<string>com.warroom.proofofpassport</string>
</array>
</dict>
</array>
@@ -39,6 +40,18 @@
<false/>
<key>LSApplicationCategoryType</key>
<string></string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>argent</string>
<string>cbwallet</string>
<string>coinbase</string>
<string>metamask</string>
<string>rainbow</string>
<string>sms</string>
<string>trust</string>
<string>wc</string>
<string>whatsapp</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NFCReaderUsageDescription</key>
@@ -53,7 +66,7 @@
<key>NSCameraUsageDescription</key>
<string>Needed to scan the passport MRZ.</string>
<key>NSFaceIDUsageDescription</key>
<string>Needed to secure the secret</string>
<string>Personal information is only stored in the secure element of your device.</string>
<key>NSHumanReadableCopyright</key>
<string></string>
<key>NSLocationWhenInUseUsageDescription</key>
@@ -63,6 +76,7 @@
<key>UIAppFonts</key>
<array>
<string>Advercase-Regular.otf</string>
<string>DINOT-Bold.otf</string>
<string>DINOT-Medium.otf</string>
<string>IBMPlexMono-Regular.otf</string>
</array>

View File

@@ -15,6 +15,7 @@
<string>appclips:appclip.openpassport.app</string>
<string>applinks:proofofpassport-merkle-tree.xyz</string>
<string>applinks:redirect.self.xyz</string>
<string>webcredentials:redirect.self.xyz</string>
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>

View File

@@ -15,6 +15,9 @@
<string>appclips:appclip.openpassport.app</string>
<string>applinks:proofofpassport-merkle-tree.xyz</string>
<string>applinks:redirect.self.xyz</string>
<string>webcredentials:redirect.self.xyz</string>
<string>applinks:oauth-redirect.turnkey.com</string>
<string>webcredentials:oauth-redirect.turnkey.com</string>
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>

View File

@@ -33,7 +33,7 @@ def using_https_git_auth?
auth_data.include?("Logged in to github.com account") &&
auth_data.include?("Git operations protocol: https")
rescue => e
puts "gh auth status failed, assuming no HTTPS auth -- will try SSH"
# Avoid printing auth-related details in CI logs.
false
end
end
@@ -51,18 +51,16 @@ target "Self" do
# External fork - use public NFCPassportReader repository (placeholder)
# TODO: Replace with actual public NFCPassportReader repository URL
nfc_repo_url = "https://github.com/PLACEHOLDER/NFCPassportReader.git"
puts "📦 Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})"
elsif ENV["GITHUB_ACTIONS"] == "true" && ENV["SELFXYZ_INTERNAL_REPO_PAT"]
# Running in selfxyz GitHub Actions with PAT available - use private repo with token
nfc_repo_url = "https://#{ENV["SELFXYZ_INTERNAL_REPO_PAT"]}@github.com/selfxyz/NFCPassportReader.git"
puts "📦 Using private NFCPassportReader with PAT (selfxyz GitHub Actions)"
elsif ENV["GITHUB_ACTIONS"] == "true"
# CI: NEVER embed credentials in URLs. Rely on workflow-provided auth via:
# - ~/.netrc or a Git credential helper, and token masking in logs.
nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git"
elsif using_https_git_auth?
# Local development with HTTPS GitHub auth via gh - use HTTPS to private repo
nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git"
else
# Local development in selfxyz repo - use SSH to private repo
nfc_repo_url = "git@github.com:selfxyz/NFCPassportReader.git"
puts "📦 Using SSH for private NFCPassportReader (local selfxyz development)"
end
pod "NFCPassportReader", git: nfc_repo_url, commit: "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b"

View File

@@ -1465,7 +1465,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-app-auth (8.0.3):
- react-native-app-auth (8.1.0):
- AppAuth (>= 1.7.6)
- React-Core
- react-native-biometrics (3.0.1):
@@ -1512,36 +1512,34 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-compat (2.23.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-native-get-random-values (1.11.0):
- React-Core
- react-native-netinfo (11.4.1):
- React-Core
- react-native-nfc-manager (3.16.3):
- React-Core
- react-native-safe-area-context (5.6.1):
- 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-native-safe-area-context/common (= 5.6.1)
- react-native-safe-area-context/fabric (= 5.6.1)
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/common (5.6.1):
- react-native-passkey (3.3.1):
- DoubleConversion
- glog
- hermes-engine
@@ -1562,7 +1560,51 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/fabric (5.6.1):
- react-native-safe-area-context (5.6.2):
- 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-native-safe-area-context/common (= 5.6.2)
- react-native-safe-area-context/fabric (= 5.6.2)
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/common (5.6.2):
- 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-native-safe-area-context/fabric (5.6.2):
- DoubleConversion
- glog
- hermes-engine
@@ -1956,6 +1998,8 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNInAppBrowser (3.7.0):
- React-Core
- RNKeychain (10.0.0):
- DoubleConversion
- glog
@@ -1977,7 +2021,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNLocalize (3.5.3):
- RNLocalize (3.6.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2087,7 +2131,7 @@ PODS:
- ReactCommon/turbomodule/core
- Sentry/HybridSDK (= 8.53.2)
- Yoga
- RNSVG (15.12.1):
- RNSVG (15.15.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2107,9 +2151,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNSVG/common (= 15.12.1)
- RNSVG/common (= 15.15.0)
- Yoga
- RNSVG/common (15.12.1):
- RNSVG/common (15.15.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2194,9 +2238,11 @@ DEPENDENCIES:
- react-native-biometrics (from `../node_modules/react-native-biometrics`)
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- react-native-cloud-storage (from `../node_modules/react-native-cloud-storage`)
- "react-native-compat (from `../node_modules/@walletconnect/react-native-compat`)"
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-nfc-manager (from `../node_modules/react-native-nfc-manager`)
- react-native-passkey (from `../node_modules/react-native-passkey`)
- 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`)
@@ -2234,6 +2280,7 @@ DEPENDENCIES:
- "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)"
- "RNFBRemoteConfig (from `../node_modules/@react-native-firebase/remote-config`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`)
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNLocalize (from `../node_modules/react-native-localize`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
@@ -2360,12 +2407,16 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/blur"
react-native-cloud-storage:
:path: "../node_modules/react-native-cloud-storage"
react-native-compat:
:path: "../node_modules/@walletconnect/react-native-compat"
react-native-get-random-values:
:path: "../node_modules/react-native-get-random-values"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-nfc-manager:
:path: "../node_modules/react-native-nfc-manager"
react-native-passkey:
:path: "../node_modules/react-native-passkey"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
react-native-sqlite-storage:
@@ -2440,6 +2491,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-firebase/remote-config"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNInAppBrowser:
:path: "../node_modules/react-native-inappbrowser-reborn"
RNKeychain:
:path: "../node_modules/react-native-keychain"
RNLocalize:
@@ -2530,14 +2583,16 @@ SPEC CHECKSUMS:
React-logger: c4052eb941cca9a097ef01b59543a656dc088559
React-Mapbuffer: 9343a5c14536d4463c80f09a960653d754daae21
React-microtasksnativemodule: c7cafd8f4470cf8a4578ee605daa4c74d3278bf8
react-native-app-auth: eb42594042a25455119a8c57194b4fd25b9352f4
react-native-app-auth: e21c8ee920876b960e38c9381971bd189ebea06b
react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d
react-native-blur: 6334d934a9b5e67718b8f5725c44cc0a12946009
react-native-cloud-storage: 8d89f2bc574cf11068dfd90933905974087fb9e9
react-native-compat: 44e82a19b6130e3965d6c8ff37dbc1546d477f0f
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-nfc-manager: 66a00e5ddab9704efebe19d605b1b8afb0bb1bd7
react-native-safe-area-context: 90a89cb349c7f8168a707e6452288c2f665b9fd1
react-native-passkey: 8853c3c635164864da68a6dbbcec7148506c3bcf
react-native-safe-area-context: a7aad44fe544b55e2369a3086e16a01be60ce398
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
react-native-webview: 3f45e19f0ffc3701168768a6c37695e0f252410e
React-nativeconfig: 415626a63057638759bcc75e0a96e2e07771a479
@@ -2574,12 +2629,13 @@ SPEC CHECKSUMS:
RNFBMessaging: 92325b0d5619ac90ef023a23cfd16fd3b91d0a88
RNFBRemoteConfig: a569bacaa410acfcaba769370e53a787f80fd13b
RNGestureHandler: a63b531307e5b2e6ea21d053a1a7ad4cf9695c57
RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20
RNKeychain: 471ceef8c13f15a5534c3cd2674dbbd9d0680e52
RNLocalize: 7683e450496a5aea9a2dab3745bfefa7341d3f5e
RNLocalize: 4f5e4a46d2bccd04ccb96721e438dcb9de17c2e0
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
RNSVG: 0c1fc3e7b147949dc15644845e9124947ac8c9bb
RNSVG: 39476f26bbbe72ffe6194c6fc8f6acd588087957
segment-analytics-react-native: a0c29c75ede1989118b50cac96b9495ea5c91a1d
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
@@ -2588,6 +2644,6 @@ SPEC CHECKSUMS:
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
PODFILE CHECKSUM: b5f11f935be22fce84c5395aaa203b50427a79aa
PODFILE CHECKSUM: 0aa47f53692543349c43673cda7380fa23049eba
COCOAPODS: 1.16.2

View File

@@ -41,6 +41,7 @@
E9F9A99C2D57FE2900E1362E /* PassportOCRViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9F9A99A2D57FE2900E1362E /* PassportOCRViewManager.m */; };
E9F9A99D2D57FE2900E1362E /* PassportOCRViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F9A99B2D57FE2900E1362E /* PassportOCRViewManager.swift */; };
EBECCA4983EC6929A7722578 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E56E082698598B41447667BB /* PrivacyInfo.xcprivacy */; };
F3A8B2C9D4E5F6A7B8C9D0E1 /* DINOT-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F2 /* DINOT-Bold.otf */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -72,6 +73,7 @@
905B70062A72774000AFA232 /* PassportReader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PassportReader.m; sourceTree = "<group>"; };
905B70082A729CD400AFA232 /* OpenPassport.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = OpenPassport.entitlements; path = OpenPassport/OpenPassport.entitlements; sourceTree = "<group>"; };
9BF744D9A73A4BAC96EC569A /* DINOT-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "DINOT-Medium.otf"; path = "../src/assets/fonts/DINOT-Medium.otf"; sourceTree = "<group>"; };
A1B2C3D4E5F6A7B8C9D0E1F2 /* DINOT-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "DINOT-Bold.otf"; path = "../src/assets/fonts/DINOT-Bold.otf"; sourceTree = "<group>"; };
AE6147EB2DC95A8D00445C0F /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
B49D2B102E28AA7900946F64 /* IBMPlexMono-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "IBMPlexMono-Regular.otf"; path = "../src/assets/fonts/IBMPlexMono-Regular.otf"; sourceTree = SOURCE_ROOT; };
BF1044802DD53540009B3688 /* LiveMRZScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMRZScannerView.swift; sourceTree = "<group>"; };
@@ -155,8 +157,9 @@
isa = PBXGroup;
children = (
7E5C3CEF7EDA4871B3D0EBE1 /* Advercase-Regular.otf */,
B49D2B102E28AA7900946F64 /* IBMPlexMono-Regular.otf */,
A1B2C3D4E5F6A7B8C9D0E1F2 /* DINOT-Bold.otf */,
9BF744D9A73A4BAC96EC569A /* DINOT-Medium.otf */,
B49D2B102E28AA7900946F64 /* IBMPlexMono-Regular.otf */,
);
name = Resources;
sourceTree = "<group>";
@@ -270,8 +273,9 @@
AE6147EC2DC95A8D00445C0F /* GoogleService-Info.plist in Resources */,
EBECCA4983EC6929A7722578 /* PrivacyInfo.xcprivacy in Resources */,
DAC618BCA5874DD8AD74FFFC /* Advercase-Regular.otf in Resources */,
B49D2B112E28AA7900946F64 /* IBMPlexMono-Regular.otf in Resources */,
F3A8B2C9D4E5F6A7B8C9D0E1 /* DINOT-Bold.otf in Resources */,
D427791AA5714251A5EAF8AD /* DINOT-Medium.otf in Resources */,
B49D2B112E28AA7900946F64 /* IBMPlexMono-Regular.otf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -427,7 +431,7 @@
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassportDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 178;
CURRENT_PROJECT_VERSION = 189;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 5B29R5LYHQ;
ENABLE_BITCODE = NO;
@@ -542,7 +546,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
MARKETING_VERSION = 2.7.3;
MARKETING_VERSION = 2.9.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -568,7 +572,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements;
CURRENT_PROJECT_VERSION = 178;
CURRENT_PROJECT_VERSION = 189;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 5B29R5LYHQ;
FRAMEWORK_SEARCH_PATHS = (
@@ -682,7 +686,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
MARKETING_VERSION = 2.7.3;
MARKETING_VERSION = 2.9.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

View File

@@ -3,10 +3,20 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
module.exports = {
preset: 'react-native',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'cjs', 'json', 'node'],
moduleFileExtensions: [
'ios.js',
'android.js',
'native.js',
'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)/)',
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags|react-native-blur-effect)/)',
],
setupFiles: ['<rootDir>/jest.setup.js'],
testMatch: [
@@ -16,10 +26,12 @@ module.exports = {
testPathIgnorePatterns: [
'/node_modules/',
'/scripts/tests/', // Node.js native test runner tests
'/babel\\.config\\.test\\.cjs',
],
moduleNameMapper: {
'^@env$': '<rootDir>/tests/__setup__/@env.js',
'\\.svg$': '<rootDir>/tests/__setup__/svgMock.js',
'\\.(png|jpg|jpeg|gif|webp)$': '<rootDir>/tests/__setup__/imageMock.js',
'^@/(.*)$': '<rootDir>/src/$1',
'^@$': '<rootDir>/src',
'^@tests/(.*)$': '<rootDir>/tests/src/$1',
@@ -30,6 +42,8 @@ module.exports = {
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/index.cjs',
'^@selfxyz/mobile-sdk-alpha/components$':
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/components/index.cjs',
'^@selfxyz/mobile-sdk-alpha/hooks$':
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/hooks/index.cjs',
'^@selfxyz/mobile-sdk-alpha/onboarding/(.*)$':
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/flows/onboarding/$1.cjs',
'^@selfxyz/mobile-sdk-alpha/disclosing/(.*)$':
@@ -47,6 +61,9 @@ module.exports = {
'^@anon-aadhaar/core$':
'<rootDir>/../common/node_modules/@anon-aadhaar/core/dist/index.js',
},
transform: {
'\\.[jt]sx?$': ['babel-jest', { configFile: './babel.config.test.cjs' }],
},
globals: {
'ts-jest': {
tsconfig: 'tsconfig.test.json',

View File

@@ -5,6 +5,11 @@
/* global jest */
/** @jest-environment jsdom */
// Set up Buffer globally for tests that need it
const { Buffer } = require('buffer');
global.Buffer = Buffer;
// Mock React Native PixelRatio globally before anything else loads
const mockPixelRatio = {
get: jest.fn(() => 2),
@@ -16,21 +21,136 @@ const mockPixelRatio = {
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);
// Define NativeModules early so it's available for react-native mock
// This will be assigned to global.NativeModules later, but we define it here
// so the react-native mock can reference it
const NativeModules = {
PassportReader: {
configure: jest.fn(),
scanPassport: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
reset: jest.fn(),
},
ReactNativeBiometrics: {
isSensorAvailable: jest.fn().mockResolvedValue({
available: true,
biometryType: 'TouchID',
}),
createKeys: jest.fn().mockResolvedValue({ publicKey: 'mock-public-key' }),
deleteKeys: jest.fn().mockResolvedValue(true),
createSignature: jest
.fn()
.mockResolvedValue({ signature: 'mock-signature' }),
simplePrompt: jest.fn().mockResolvedValue({ success: true }),
},
NativeLoggerBridge: {
log: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
RNPassportReader: {
configure: jest.fn(),
scanPassport: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
reset: jest.fn(),
},
};
// Assign to global so it's available everywhere
global.NativeModules = NativeModules;
// Mock react-native comprehensively - single source of truth for all tests
// Note: NativeModules will be defined later and assigned to global.NativeModules
// This mock accesses it at runtime via global.NativeModules
jest.mock('react-native', () => {
// Create AppState mock with listener tracking
// Expose listeners array globally so tests can access it
const appStateListeners = [];
global.mockAppStateListeners = appStateListeners;
const mockAppState = {
currentState: 'active',
addEventListener: jest.fn((eventType, handler) => {
appStateListeners.push(handler);
return {
remove: () => {
const index = appStateListeners.indexOf(handler);
if (index >= 0) {
appStateListeners.splice(index, 1);
}
},
};
}),
};
return {
__esModule: true,
AppState: mockAppState,
Platform: {
OS: 'ios',
select: jest.fn(obj => obj.ios || obj.default),
Version: 14,
},
// NativeModules is defined above and assigned to global.NativeModules
// Use getter to access it at runtime (jest.mock is hoisted)
get NativeModules() {
return global.NativeModules || {};
},
NativeEventEmitter: jest.fn().mockImplementation(nativeModule => {
return {
addListener: jest.fn(),
removeListener: jest.fn(),
removeAllListeners: jest.fn(),
emit: jest.fn(),
};
}),
PixelRatio: mockPixelRatio,
Dimensions: {
get: jest.fn(() => ({
window: { width: 375, height: 667, scale: 2 },
screen: { width: 375, height: 667, scale: 2 },
})),
},
Linking: {
getInitialURL: jest.fn().mockResolvedValue(null),
addEventListener: jest.fn(() => ({ remove: jest.fn() })),
removeEventListener: jest.fn(),
openURL: jest.fn().mockResolvedValue(undefined),
canOpenURL: jest.fn().mockResolvedValue(true),
},
StyleSheet: {
create: jest.fn(styles => styles),
flatten: jest.fn(style => style),
hairlineWidth: 1,
absoluteFillObject: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
},
View: 'View',
Text: 'Text',
ScrollView: 'ScrollView',
TouchableOpacity: 'TouchableOpacity',
TouchableHighlight: 'TouchableHighlight',
Image: 'Image',
ActivityIndicator: 'ActivityIndicator',
SafeAreaView: 'SafeAreaView',
requireNativeComponent: jest.fn(name => {
// Return a mock component function for any native component
const MockNativeComponent = jest.fn(props => props.children || null);
MockNativeComponent.displayName = `Mock(${name})`;
return MockNativeComponent;
}),
};
});
require('react-native-gesture-handler/jestSetup');
// Mock NativeAnimatedHelper - using virtual mock during RN 0.76.9 prep phase
@@ -51,6 +171,23 @@ global.__fbBatchedBridgeConfig = {
// Set up global React Native test environment
global.__DEV__ = true;
// Set up global mock navigation ref for tests
global.mockNavigationRef = {
isReady: jest.fn(() => true),
getCurrentRoute: jest.fn(() => ({ name: 'Home' })),
navigate: jest.fn(),
goBack: jest.fn(),
canGoBack: jest.fn(() => true),
dispatch: jest.fn(),
getState: jest.fn(() => ({ routes: [{ name: 'Home' }], index: 0 })),
addListener: jest.fn(() => jest.fn()),
removeListener: jest.fn(),
};
// Load grouped mocks
require('./tests/__setup__/mocks/navigation');
require('./tests/__setup__/mocks/ui');
// Mock TurboModuleRegistry to provide required native modules for BOTH main app and mobile-sdk-alpha
jest.mock('react-native/Libraries/TurboModule/TurboModuleRegistry', () => ({
getEnforcing: jest.fn(name => {
@@ -84,6 +221,16 @@ jest.mock('react-native/Libraries/TurboModule/TurboModuleRegistry', () => ({
}),
};
}
if (name === 'RNDeviceInfo') {
return {
getConstants: () => ({
Dimensions: {
window: { width: 375, height: 667, scale: 2 },
screen: { width: 375, height: 667, scale: 2 },
},
}),
};
}
return {
getConstants: () => ({}),
};
@@ -115,25 +262,69 @@ jest.mock(
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,
// Return a simple object with all the mocks we need
// Avoid nested requireActual/requireMock to prevent OOM in CI
return {
__esModule: true,
PixelRatio,
default: {
...RN,
PixelRatio,
Platform: {
OS: 'ios',
select: jest.fn(obj => obj.ios || obj.default),
Version: 14,
},
Dimensions: {
get: jest.fn(() => ({
window: { width: 375, height: 667, scale: 2 },
screen: { width: 375, height: 667, scale: 2 },
})),
},
StyleSheet: {
create: jest.fn(styles => styles),
flatten: jest.fn(style => style),
hairlineWidth: 1,
absoluteFillObject: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
},
View: 'View',
Text: 'Text',
ScrollView: 'ScrollView',
TouchableOpacity: 'TouchableOpacity',
requireNativeComponent: jest.fn(name => {
const MockNativeComponent = jest.fn(props => props.children || null);
MockNativeComponent.displayName = `Mock(${name})`;
return MockNativeComponent;
}),
};
return mockedRN;
},
{ virtual: true },
);
// Mock @turnkey/react-native-wallet-kit to prevent loading of problematic dependencies
jest.mock(
'@turnkey/react-native-wallet-kit',
() => ({
AuthState: {
Authenticated: 'Authenticated',
Unauthenticated: 'Unauthenticated',
},
useTurnkey: jest.fn(() => ({
handleGoogleOauth: jest.fn(),
fetchWallets: jest.fn().mockResolvedValue([]),
exportWallet: jest.fn(),
importWallet: jest.fn(),
authState: 'Unauthenticated',
logout: jest.fn(),
})),
TurnkeyProvider: ({ children }) => children,
}),
{ 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',
@@ -239,47 +430,126 @@ jest.mock('react-native/src/private/specs/modules/NativeDeviceInfo', () => ({
})),
}));
// Mock NativeStatusBarManagerIOS for react-native-edge-to-edge SystemBars
jest.mock(
'react-native/src/private/specs/modules/NativeStatusBarManagerIOS',
() => ({
setStyle: jest.fn(),
setHidden: jest.fn(),
setNetworkActivityIndicatorVisible: jest.fn(),
}),
);
// Mock react-native-gesture-handler to prevent getConstants errors
jest.mock('react-native-gesture-handler', () => {
const RN = jest.requireActual('react-native');
// Avoid requiring React to prevent nested require memory issues
// Mock the components as simple pass-through functions
const MockScrollView = jest.fn(props => props.children || null);
const MockTouchableOpacity = jest.fn(props => props.children || null);
const MockTouchableHighlight = jest.fn(props => props.children || null);
const MockFlatList = jest.fn(props => null);
return {
...jest.requireActual('react-native-gesture-handler/jestSetup'),
// Provide gesture handler mock without requireActual to avoid OOM
GestureHandlerRootView: ({ children }) => children,
ScrollView: RN.ScrollView,
TouchableOpacity: RN.TouchableOpacity,
TouchableHighlight: RN.TouchableHighlight,
FlatList: RN.FlatList,
ScrollView: MockScrollView,
TouchableOpacity: MockTouchableOpacity,
TouchableHighlight: MockTouchableHighlight,
FlatList: MockFlatList,
Directions: {},
State: {},
Swipeable: jest.fn(() => null),
DrawerLayout: jest.fn(() => null),
PanGestureHandler: jest.fn(() => null),
TapGestureHandler: jest.fn(() => null),
LongPressGestureHandler: jest.fn(() => null),
};
});
// Mock react-native-safe-area-context
jest.mock('react-native-safe-area-context', () => {
const React = require('react');
const { View } = require('react-native');
// Avoid requiring React to prevent nested require memory issues
return {
__esModule: true,
SafeAreaProvider: ({ children }) =>
React.createElement(View, null, children),
SafeAreaView: ({ children }) => React.createElement(View, null, children),
SafeAreaProvider: jest.fn(({ children }) => children || null),
SafeAreaView: jest.fn(({ children }) => children || null),
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 {
constructor(nativeModule) {
// Accept any nativeModule argument (including null/undefined)
this.nativeModule = nativeModule;
}
function MockNativeEventEmitter(nativeModule) {
// Accept any nativeModule argument (including null/undefined)
this.nativeModule = nativeModule;
this.addListener = jest.fn();
this.removeListener = jest.fn();
this.removeAllListeners = jest.fn();
this.emit = jest.fn();
}
addListener = jest.fn();
removeListener = jest.fn();
removeAllListeners = jest.fn();
emit = jest.fn();
};
// The mock needs to be the constructor itself, not wrapped
MockNativeEventEmitter.default = MockNativeEventEmitter;
return MockNativeEventEmitter;
});
// Mock react-native-device-info to prevent NativeEventEmitter errors
jest.mock('react-native-device-info', () => ({
getUniqueId: jest.fn().mockResolvedValue('mock-device-id'),
getReadableVersion: jest.fn().mockReturnValue('1.0.0'),
getVersion: jest.fn().mockReturnValue('1.0.0'),
getBuildNumber: jest.fn().mockReturnValue('1'),
getModel: jest.fn().mockReturnValue('mock-model'),
getBrand: jest.fn().mockReturnValue('mock-brand'),
isTablet: jest.fn().mockReturnValue(false),
isLandscape: jest.fn().mockResolvedValue(false),
getSystemVersion: jest.fn().mockReturnValue('14.0'),
getSystemName: jest.fn().mockReturnValue('iOS'),
default: {
getUniqueId: jest.fn().mockResolvedValue('mock-device-id'),
getReadableVersion: jest.fn().mockReturnValue('1.0.0'),
getVersion: jest.fn().mockReturnValue('1.0.0'),
getBuildNumber: jest.fn().mockReturnValue('1'),
getModel: jest.fn().mockReturnValue('mock-model'),
getBrand: jest.fn().mockReturnValue('mock-brand'),
isTablet: jest.fn().mockReturnValue(false),
isLandscape: jest.fn().mockResolvedValue(false),
getSystemVersion: jest.fn().mockReturnValue('14.0'),
getSystemName: jest.fn().mockReturnValue('iOS'),
},
}));
// Mock react-native-device-info nested in @turnkey/react-native-wallet-kit
jest.mock(
'node_modules/@turnkey/react-native-wallet-kit/node_modules/react-native-device-info',
() => ({
getUniqueId: jest.fn().mockResolvedValue('mock-device-id'),
getReadableVersion: jest.fn().mockReturnValue('1.0.0'),
getVersion: jest.fn().mockReturnValue('1.0.0'),
getBuildNumber: jest.fn().mockReturnValue('1'),
getModel: jest.fn().mockReturnValue('mock-model'),
getBrand: jest.fn().mockReturnValue('mock-brand'),
isTablet: jest.fn().mockReturnValue(false),
isLandscape: jest.fn().mockResolvedValue(false),
getSystemVersion: jest.fn().mockReturnValue('14.0'),
getSystemName: jest.fn().mockReturnValue('iOS'),
default: {
getUniqueId: jest.fn().mockResolvedValue('mock-device-id'),
getReadableVersion: jest.fn().mockReturnValue('1.0.0'),
getVersion: jest.fn().mockReturnValue('1.0.0'),
getBuildNumber: jest.fn().mockReturnValue('1'),
getModel: jest.fn().mockReturnValue('mock-model'),
getBrand: jest.fn().mockReturnValue('mock-brand'),
isTablet: jest.fn().mockReturnValue(false),
isLandscape: jest.fn().mockResolvedValue(false),
getSystemVersion: jest.fn().mockReturnValue('14.0'),
getSystemName: jest.fn().mockReturnValue('iOS'),
},
}),
{ virtual: true },
);
// Mock problematic mobile-sdk-alpha components that use React Native StyleSheet
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
NFCScannerScreen: jest.fn(() => null),
@@ -382,6 +652,21 @@ jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
PROVING_FAILED: 'PROVING_FAILED',
// Add other events as needed
},
// Mock haptic functions
buttonTap: jest.fn(),
cancelTap: jest.fn(),
confirmTap: jest.fn(),
feedbackProgress: jest.fn(),
feedbackSuccess: jest.fn(),
feedbackUnsuccessful: jest.fn(),
impactLight: jest.fn(),
impactMedium: jest.fn(),
loadingScreenProgress: jest.fn(),
notificationError: jest.fn(),
notificationSuccess: jest.fn(),
notificationWarning: jest.fn(),
selectionChange: jest.fn(),
triggerFeedback: jest.fn(),
// Add other components and hooks as needed
}));
@@ -627,18 +912,11 @@ jest.mock('react-native-passport-reader', () => {
};
});
const { NativeModules } = require('react-native');
// NativeModules is already defined at the top of the file and assigned to global.NativeModules
// No need to redefine it here
NativeModules.PassportReader = {
configure: jest.fn(),
scanPassport: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
reset: jest.fn(),
};
// Mock @/utils/passportReader to properly expose the interface expected by tests
jest.mock('./src/utils/passportReader', () => {
// Mock @/integrations/nfc/passportReader to properly expose the interface expected by tests
jest.mock('./src/integrations/nfc/passportReader', () => {
const mockScanPassport = jest.fn();
// Mock the parameter count for scanPassport (iOS native method takes 9 parameters)
Object.defineProperty(mockScanPassport, 'length', { value: 9 });
@@ -774,183 +1052,164 @@ jest.mock('react-native-localize', () => ({
languageTag: 'en-US',
isRTL: false,
}),
default: {
getLocales: jest.fn().mockReturnValue([
{
countryCode: 'US',
languageTag: 'en-US',
languageCode: 'en',
isRTL: false,
},
]),
getCountry: jest.fn().mockReturnValue('US'),
getTimeZone: jest.fn().mockReturnValue('America/New_York'),
getCurrencies: jest.fn().mockReturnValue(['USD']),
getTemperatureUnit: jest.fn().mockReturnValue('celsius'),
getFirstWeekDay: jest.fn().mockReturnValue(0),
uses24HourClock: jest.fn().mockReturnValue(false),
usesMetricSystem: jest.fn().mockReturnValue(false),
findBestAvailableLanguage: jest.fn().mockReturnValue({
languageTag: 'en-US',
isRTL: false,
}),
},
}));
jest.mock('./src/utils/notifications/notificationService', () =>
// Ensure mobile-sdk-alpha's bundled react-native-localize dependency is mocked as well
jest.mock(
'../packages/mobile-sdk-alpha/node_modules/react-native-localize',
() => ({
getLocales: jest.fn().mockReturnValue([
{
countryCode: 'US',
languageTag: 'en-US',
languageCode: 'en',
isRTL: false,
},
]),
getCountry: jest.fn().mockReturnValue('US'),
getTimeZone: jest.fn().mockReturnValue('America/New_York'),
getCurrencies: jest.fn().mockReturnValue(['USD']),
getTemperatureUnit: jest.fn().mockReturnValue('celsius'),
getFirstWeekDay: jest.fn().mockReturnValue(0),
uses24HourClock: jest.fn().mockReturnValue(false),
usesMetricSystem: jest.fn().mockReturnValue(false),
findBestAvailableLanguage: jest.fn().mockReturnValue({
languageTag: 'en-US',
isRTL: false,
}),
default: {
getLocales: jest.fn().mockReturnValue([
{
countryCode: 'US',
languageTag: 'en-US',
languageCode: 'en',
isRTL: false,
},
]),
getCountry: jest.fn().mockReturnValue('US'),
getTimeZone: jest.fn().mockReturnValue('America/New_York'),
getCurrencies: jest.fn().mockReturnValue(['USD']),
getTemperatureUnit: jest.fn().mockReturnValue('celsius'),
getFirstWeekDay: jest.fn().mockReturnValue(0),
uses24HourClock: jest.fn().mockReturnValue(false),
usesMetricSystem: jest.fn().mockReturnValue(false),
findBestAvailableLanguage: jest.fn().mockReturnValue({
languageTag: 'en-US',
isRTL: false,
}),
},
}),
);
jest.mock('./src/services/notifications/notificationService', () =>
require('./tests/__setup__/notificationServiceMock.js'),
);
// Mock react-native-svg
jest.mock('react-native-svg', () => {
const React = require('react');
// Avoid requiring React to prevent nested require memory issues
// Mock SvgXml component that handles XML strings
const SvgXml = React.forwardRef(
({ xml, width, height, style, ...props }, ref) => {
return React.createElement('div', {
ref,
style: {
width: width || 'auto',
height: height || 'auto',
display: 'inline-block',
...style,
},
dangerouslySetInnerHTML: { __html: xml },
...props,
});
},
);
const SvgXml = jest.fn(() => null);
SvgXml.displayName = 'SvgXml';
return {
__esModule: true,
default: SvgXml,
SvgXml,
Svg: props => React.createElement('Svg', props, props.children),
Circle: props => React.createElement('Circle', props, props.children),
Path: props => React.createElement('Path', props, props.children),
G: props => React.createElement('G', props, props.children),
Rect: props => React.createElement('Rect', props, props.children),
Defs: props => React.createElement('Defs', props, props.children),
LinearGradient: props =>
React.createElement('LinearGradient', props, props.children),
Stop: props => React.createElement('Stop', props, props.children),
ClipPath: props => React.createElement('ClipPath', props, props.children),
Polygon: props => React.createElement('Polygon', props, props.children),
Polyline: props => React.createElement('Polyline', props, props.children),
Line: props => React.createElement('Line', props, props.children),
Text: props => React.createElement('Text', props, props.children),
TSpan: props => React.createElement('TSpan', props, props.children),
Svg: jest.fn(() => null),
Circle: jest.fn(() => null),
Path: jest.fn(() => null),
G: jest.fn(() => null),
Rect: jest.fn(() => null),
Defs: jest.fn(() => null),
LinearGradient: jest.fn(() => null),
Stop: jest.fn(() => null),
ClipPath: jest.fn(() => null),
Polygon: jest.fn(() => null),
Polyline: jest.fn(() => null),
Line: jest.fn(() => null),
Text: jest.fn(() => null),
TSpan: jest.fn(() => null),
};
});
// Mock React Navigation
jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native');
// Mock react-native-biometrics to prevent NativeModules errors
jest.mock('react-native-biometrics', () => {
class MockReactNativeBiometrics {
constructor(options) {
// Constructor accepts options but doesn't need to do anything
}
isSensorAvailable = jest.fn().mockResolvedValue({
available: true,
biometryType: 'TouchID',
});
createKeys = jest.fn().mockResolvedValue({ publicKey: 'mock-public-key' });
deleteKeys = jest.fn().mockResolvedValue(true);
createSignature = jest
.fn()
.mockResolvedValue({ signature: 'mock-signature' });
simplePrompt = jest.fn().mockResolvedValue({ success: true });
}
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(),
canGoBack: jest.fn(() => true),
dispatch: jest.fn(),
})),
createNavigationContainerRef: jest.fn(() => ({
current: null,
getCurrentRoute: jest.fn(),
})),
createStaticNavigation: jest.fn(() => ({ displayName: 'MockNavigation' })),
__esModule: true,
default: MockReactNativeBiometrics,
};
});
jest.mock('@react-navigation/native-stack', () => ({
createNativeStackNavigator: jest.fn(() => ({
displayName: 'MockStackNavigator',
})),
createNavigatorFactory: jest.fn(),
// Mock NativeAppState native module to prevent getCurrentAppState errors
jest.mock('react-native/Libraries/AppState/NativeAppState', () => ({
__esModule: true,
default: {
getConstants: jest.fn(() => ({ initialAppState: 'active' })),
getCurrentAppState: jest.fn(() => Promise.resolve({ app_state: 'active' })),
addListener: jest.fn(),
removeListeners: 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';
// Mock AppState to prevent getCurrentAppState errors
jest.mock('react-native/Libraries/AppState/AppState', () => {
// Use the global appStateListeners array so tests can access it
const appStateListeners = global.mockAppStateListeners || [];
return {
__esModule: true,
default: MockWebView,
WebView: MockWebView,
default: {
currentState: 'active',
addEventListener: jest.fn((eventType, handler) => {
appStateListeners.push(handler);
return {
remove: () => {
const index = appStateListeners.indexOf(handler);
if (index >= 0) {
appStateListeners.splice(index, 1);
}
},
};
}),
},
};
});
// Mock ExpandableBottomLayout to simple containers to avoid SDK internals in tests
jest.mock('@/layouts/ExpandableBottomLayout', () => {
const React = require('react');
const { View } = require('react-native');
const Layout = ({ children }) => React.createElement(View, null, children);
const TopSection = ({ children }) =>
React.createElement(View, null, children);
const BottomSection = ({ children }) =>
React.createElement(View, null, children);
const FullSection = ({ children }) =>
React.createElement(View, null, children);
return {
__esModule: true,
ExpandableBottomLayout: { Layout, TopSection, BottomSection, FullSection },
};
});
// Mock mobile-sdk-alpha components used by NavBar (Button, XStack)
jest.mock('@selfxyz/mobile-sdk-alpha/components', () => {
const React = require('react');
const { View, Text, TouchableOpacity } = require('react-native');
const Button = ({ children, onPress, icon, ...props }) =>
React.createElement(
TouchableOpacity,
{ onPress, ...props, testID: 'msdk-button' },
icon
? React.createElement(View, { testID: 'msdk-button-icon' }, icon)
: null,
children,
);
const XStack = ({ children, ...props }) =>
React.createElement(View, { ...props, testID: 'msdk-xstack' }, children);
return {
__esModule: true,
Button,
XStack,
// Provide minimal Text to satisfy potential usages
Text,
};
});
// Mock Tamagui lucide icons to simple components to avoid theme context
jest.mock('@tamagui/lucide-icons', () => {
const React = require('react');
const { View } = require('react-native');
const makeIcon = name => {
const Icon = ({ size, color, opacity }) =>
React.createElement(View, {
testID: `icon-${name}`,
size,
color,
opacity,
});
Icon.displayName = `MockIcon(${name})`;
return Icon;
};
return {
__esModule: true,
ExternalLink: makeIcon('external-link'),
X: makeIcon('x'),
};
});
// Mock WebViewFooter to avoid SDK rendering complexity
jest.mock('@/components/WebViewFooter', () => {
const React = require('react');
const { View } = require('react-native');
const WebViewFooter = () =>
React.createElement(View, { testID: 'webview-footer' });
return { __esModule: true, WebViewFooter };
});

View File

@@ -192,6 +192,73 @@ const config = {
// Handle problematic package exports and Node.js modules
// Fix @turnkey/encoding to use CommonJS instead of ESM
if (moduleName === '@turnkey/encoding') {
const filePath = path.resolve(
projectRoot,
'node_modules/@turnkey/encoding/dist/index.js',
);
return {
type: 'sourceFile',
filePath,
};
}
// Fix @turnkey/encoding submodules to use CommonJS
if (moduleName.startsWith('@turnkey/encoding/')) {
const subpath = moduleName.replace('@turnkey/encoding/', '');
const filePath = path.resolve(
projectRoot,
`node_modules/@turnkey/encoding/dist/${subpath}.js`,
);
return {
type: 'sourceFile',
filePath,
};
}
// Fix @turnkey/api-key-stamper to use CommonJS instead of ESM
if (moduleName === '@turnkey/api-key-stamper') {
const filePath = path.resolve(
projectRoot,
'node_modules/@turnkey/api-key-stamper/dist/index.js',
);
return {
type: 'sourceFile',
filePath,
};
}
// Fix @turnkey/api-key-stamper dynamic imports by resolving submodules statically
if (moduleName.startsWith('@turnkey/api-key-stamper/')) {
const subpath = moduleName.replace('@turnkey/api-key-stamper/', '');
const filePath = path.resolve(
projectRoot,
`node_modules/@turnkey/api-key-stamper/dist/${subpath}`,
);
return {
type: 'sourceFile',
filePath,
};
}
// Fix viem dynamic import resolution
if (moduleName === 'viem') {
try {
// Viem uses package exports, so we need to resolve to the actual file path
const viemPath = path.resolve(
projectRoot,
'node_modules/viem/_cjs/index.js',
);
return {
type: 'sourceFile',
filePath: viemPath,
};
} catch (error) {
console.warn('Failed to resolve viem:', error);
}
}
// Fix @tamagui/config v2-native export resolution
if (moduleName === '@tamagui/config/v2-native') {
try {

View File

@@ -1,6 +1,6 @@
{
"name": "@selfxyz/mobile-app",
"version": "2.7.3",
"version": "2.9.5",
"private": true,
"type": "module",
"scripts": {
@@ -29,11 +29,14 @@
"format": "yarn nice",
"ia": "yarn install-app",
"imports:fix": "node ./scripts/alias-imports.cjs",
"postinstall": "npx patch-package --patch-dir ../patches || true",
"install-app": "yarn install-app:setup && yarn clean:xcode-env-local",
"install-app:mobile-deploy": "yarn install && yarn build:deps && yarn clean:xcode-env-local",
"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",
"jest:clear": "node ./node_modules/jest/bin/jest.js --clearCache",
"jest:run": "node ./node_modules/jest/bin/jest.js",
"lint": "eslint . --cache --cache-location .eslintcache",
"lint:fix": "eslint --fix . --cache --cache-location .eslintcache",
"mobile-deploy": "node scripts/mobile-deploy-confirm.cjs both",
@@ -42,7 +45,7 @@
"mobile-local-deploy": "FORCE_UPLOAD_LOCAL_DEV=true node scripts/mobile-deploy-confirm.cjs both",
"mobile-local-deploy:android": "FORCE_UPLOAD_LOCAL_DEV=true node scripts/mobile-deploy-confirm.cjs android",
"mobile-local-deploy:ios": "FORCE_UPLOAD_LOCAL_DEV=true node scripts/mobile-deploy-confirm.cjs ios",
"nice": "sh -c 'if [ -z \"$SKIP_BUILD_DEPS\" ]; then yarn build:deps; fi; yarn imports:fix && yarn lint:fix && yarn fmt:fix'",
"nice": "sh -c 'if [ -z \"$SKIP_BUILD_DEPS\" ]; then yarn build:deps; fi; yarn imports:fix && yarn lint:fix'",
"reinstall": "yarn --top-level run reinstall-app",
"release": "./scripts/release.sh",
"release:major": "./scripts/release.sh major",
@@ -50,31 +53,36 @@
"release:patch": "./scripts/release.sh patch",
"setup": "yarn clean:build && yarn install && yarn build:deps && yarn setup:android-deps && cd ios && bundle install && bundle exec pod install --repo-update && cd .. && yarn clean:xcode-env-local",
"setup:android-deps": "node scripts/setup-private-modules.cjs",
"start": "watchman watch-del-all && react-native start",
"start:clean": "watchman watch-del-all && cd android && ./gradlew clean && cd .. && react-native start --reset-cache",
"start": "watchman watch-del-all && yarn watch:sdk & react-native start",
"start:clean": "watchman watch-del-all && cd android && ./gradlew clean && cd .. && yarn watch:sdk & react-native start --reset-cache",
"sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version",
"tag:release": "node scripts/tag.cjs release",
"tag:remove": "node scripts/tag.cjs remove",
"test": "yarn build:deps && jest --passWithNoTests && node --test scripts/tests/*.cjs",
"test": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
"test:build": "yarn build:deps && yarn types && node ./scripts/bundle-analyze-ci.cjs ios && yarn test",
"test:ci": "jest --passWithNoTests && node --test scripts/tests/*.cjs",
"test:coverage": "jest --coverage --passWithNoTests",
"test:coverage:ci": "jest --coverage --passWithNoTests --ci --coverageReporters=lcov --coverageReporters=text --coverageReporters=json",
"test:ci": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
"test:coverage": "yarn jest:run --coverage --passWithNoTests",
"test:coverage:ci": "yarn jest:run --coverage --passWithNoTests --ci --coverageReporters=lcov --coverageReporters=text --coverageReporters=json",
"test:e2e:android": "./scripts/mobile-ci-build-android.sh && maestro test tests/e2e/launch.android.flow.yaml",
"test:e2e:ios": "xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build && maestro test tests/e2e/launch.ios.flow.yaml",
"test:fastlane": "bundle exec ruby -Itest fastlane/test/helpers_test.rb",
"test:tree-shaking": "node ./scripts/test-tree-shaking.cjs",
"test:web-build": "jest tests/web-build-render.test.ts --testTimeout=180000",
"test:web-build": "yarn jest:run tests/web-build-render.test.ts --testTimeout=180000",
"types": "tsc --noEmit",
"watch:sdk": "yarn workspace @selfxyz/mobile-sdk-alpha watch",
"web": "vite",
"web:build": "yarn build:deps && vite build",
"web:preview": "vite preview"
},
"resolutions": {
"punycode": "npm:punycode.js@2.3.1"
"punycode": "npm:punycode.js@2.3.1",
"react-native-blur-effect": "1.1.3",
"react-native-webview": "13.16.0"
},
"overrides": {
"punycode": "npm:punycode.js@2.3.1"
"punycode": "npm:punycode.js@2.3.1",
"react-native-blur-effect": "1.1.3",
"react-native-webview": "13.16.0"
},
"dependencies": {
"@babel/runtime": "^7.28.3",
@@ -97,6 +105,7 @@
"@segment/analytics-react-native": "^2.21.2",
"@segment/sovran-react-native": "^1.1.3",
"@selfxyz/common": "workspace:^",
"@selfxyz/euclid": "^0.6.1",
"@selfxyz/mobile-sdk-alpha": "workspace:^",
"@sentry/react": "^9.32.0",
"@sentry/react-native": "7.0.1",
@@ -106,11 +115,19 @@
"@tamagui/config": "1.126.14",
"@tamagui/lucide-icons": "1.126.14",
"@tamagui/toast": "1.126.14",
"@turnkey/api-key-stamper": "^0.5.0",
"@turnkey/core": "1.7.0",
"@turnkey/encoding": "^0.6.0",
"@turnkey/react-native-wallet-kit": "1.1.5",
"@walletconnect/react-native-compat": "^2.23.0",
"@xstate/react": "^5.0.3",
"asn1js": "^3.0.6",
"axios": "^1.13.2",
"buffer": "^6.0.3",
"country-emoji": "^1.5.6",
"elliptic": "^6.6.1",
"ethers": "^6.11.0",
"expo-application": "^7.0.7",
"expo-modules-core": "^2.2.1",
"hash.js": "^1.1.7",
"js-sha1": "^0.7.0",
@@ -126,6 +143,7 @@
"react-native": "0.76.9",
"react-native-app-auth": "^8.0.3",
"react-native-biometrics": "^3.0.1",
"react-native-blur-effect": "^1.1.3",
"react-native-check-version": "^1.3.0",
"react-native-cloud-storage": "^2.2.2",
"react-native-device-info": "^14.0.4",
@@ -134,16 +152,19 @@
"react-native-gesture-handler": "2.19.0",
"react-native-get-random-values": "^1.11.0",
"react-native-haptic-feedback": "^2.3.3",
"react-native-inappbrowser-reborn": "^3.7.0",
"react-native-keychain": "^10.0.0",
"react-native-localize": "^3.5.2",
"react-native-logs": "^5.3.0",
"react-native-nfc-manager": "3.16.3",
"react-native-passkey": "^3.3.1",
"react-native-passport-reader": "1.0.3",
"react-native-safe-area-context": "5.6.1",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "4.15.3",
"react-native-sqlite-storage": "^6.0.1",
"react-native-svg": "15.12.1",
"react-native-svg": "^15.14.0",
"react-native-svg-web": "^1.0.9",
"react-native-url-polyfill": "^3.0.0",
"react-native-web": "^0.19.0",
"react-native-webview": "^13.16.0",
"react-qr-barcode-scanner": "^2.1.8",
@@ -155,8 +176,13 @@
},
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-syntax-flow": "^7.27.1",
"@babel/plugin-transform-classes": "^7.27.1",
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-flow-strip-types": "^7.27.1",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/preset-env": "^7.28.3",
"@babel/preset-react": "^7.27.1",
"@react-native-community/cli": "^16.0.3",
"@react-native/babel-preset": "0.76.9",
"@react-native/eslint-config": "0.76.9",
@@ -170,7 +196,7 @@
"@types/bn.js": "^5.2.0",
"@types/dompurify": "^3.2.0",
"@types/elliptic": "^6.4.18",
"@types/jest": "^29.5.14",
"@types/jest": "^30.0.0",
"@types/node": "^22.18.3",
"@types/node-forge": "^1.3.14",
"@types/path-browserify": "^1",
@@ -184,7 +210,7 @@
"@typescript-eslint/parser": "^8.39.0",
"@vitejs/plugin-react-swc": "^3.10.2",
"babel-plugin-module-resolver": "^5.0.2",
"buffer": "^6.0.3",
"babel-plugin-transform-remove-console": "^6.9.4",
"constants-browserify": "^1.0.0",
"dompurify": "^3.2.6",
"eslint": "^8.57.0",
@@ -193,12 +219,12 @@
"eslint-plugin-ft-flow": "^3.0.11",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.1",
"eslint-plugin-jest": "^29.1.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-sort-exports": "^0.9.1",
"hermes-eslint": "^0.19.1",
"jest": "^29.6.3",
"jest": "^30.2.0",
"path-browserify": "^1.0.1",
"prettier": "^3.5.3",
"react-native-svg-transformer": "^1.5.1",

View File

@@ -17,8 +17,8 @@ if (!platform || !['android', 'ios'].includes(platform)) {
// Bundle size thresholds in MB - easy to update!
const BUNDLE_THRESHOLDS_MB = {
// TODO: fix temporary bundle bump
ios: 42,
android: 42,
ios: 45,
android: 45,
};
function formatBytes(bytes) {

View File

@@ -0,0 +1,145 @@
// 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.
/**
* Check for nested require('react') and require('react-native') in test files
* These patterns cause out-of-memory errors in CI/CD pipelines
*
* Usage: node scripts/check-test-requires.cjs
* Exit code: 0 if no issues found, 1 if issues found
*/
const fs = require('fs');
const path = require('path');
const TESTS_DIR = path.join(__dirname, '..', 'tests');
const FORBIDDEN_PATTERNS = [
{
pattern: /require\(['"]react['"]\)/g,
name: "require('react')",
fix: 'Use \'import React from "react"\' at the top of the file instead',
},
{
pattern: /require\(['"]react-native['"]\)/g,
name: "require('react-native')",
fix: 'Use \'import { ... } from "react-native"\' at the top of the file instead',
},
];
/**
* Recursively find all test files in directory
*/
function findTestFiles(dir, files = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules and other common directories
if (
!['node_modules', '.git', 'coverage', 'dist', 'build'].includes(
entry.name,
)
) {
findTestFiles(fullPath, files);
}
} else if (
entry.isFile() &&
(entry.name.endsWith('.ts') ||
entry.name.endsWith('.tsx') ||
entry.name.endsWith('.js') ||
entry.name.endsWith('.jsx'))
) {
files.push(fullPath);
}
}
return files;
}
/**
* Check a file for forbidden require patterns
*/
function checkFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const issues = [];
for (const { pattern, name, fix } of FORBIDDEN_PATTERNS) {
const matches = content.matchAll(pattern);
for (const match of matches) {
const lines = content.substring(0, match.index).split('\n');
const lineNumber = lines.length;
const columnNumber = lines[lines.length - 1].length + 1;
issues.push({
file: path.relative(process.cwd(), filePath),
line: lineNumber,
column: columnNumber,
pattern: name,
fix: fix,
});
}
}
return issues;
}
/**
* Main execution
*/
function main() {
console.log('🔍 Checking for nested require() in test files...\n');
if (!fs.existsSync(TESTS_DIR)) {
console.error(`❌ Tests directory not found: ${TESTS_DIR}`);
process.exit(1);
}
const testFiles = findTestFiles(TESTS_DIR);
console.log(`Found ${testFiles.length} test files to check\n`);
let totalIssues = 0;
const issuesByFile = new Map();
for (const file of testFiles) {
const issues = checkFile(file);
if (issues.length > 0) {
issuesByFile.set(file, issues);
totalIssues += issues.length;
}
}
if (totalIssues === 0) {
console.log('✅ No nested require() patterns found in test files!');
process.exit(0);
}
// Report issues
console.error(
`❌ Found ${totalIssues} nested require() pattern(s) that cause OOM in CI:\n`,
);
for (const [file, issues] of issuesByFile.entries()) {
console.error(`\n${path.relative(process.cwd(), file)}:`);
for (const issue of issues) {
console.error(` Line ${issue.line}:${issue.column} - ${issue.pattern}`);
console.error(` Fix: ${issue.fix}`);
}
}
console.error(
'\n⚠ These patterns cause out-of-memory errors in CI/CD pipelines.',
);
console.error(
' Use ES6 imports at the top of files instead of require() calls.',
);
console.error(
' See .cursor/rules/test-memory-optimization.mdc for details.\n',
);
process.exit(1);
}
main();

View File

@@ -87,41 +87,62 @@ cd "$PROJECT_ROOT"
log "Working directory: $(pwd)"
# Clone android-passport-nfc-reader if it doesn't exist (for local development)
# Clone private Android modules if they don't exist (for local development)
# Note: In CI, this is usually handled by GitHub action, but we keep this as fallback
if [[ ! -d "app/android/android-passport-nfc-reader" ]]; then
log "Cloning android-passport-nfc-reader for build..."
clone_private_module() {
local repo_name=$1
local target_dir=$2
if [[ -d "$target_dir" ]]; then
if is_ci; then
log "📁 $repo_name exists (likely cloned by GitHub action)"
else
log "📁 $repo_name already exists - preserving existing directory"
fi
return 0
fi
log "Cloning $repo_name for build..."
cd app/android
# Extract just the directory name from the full path for git clone
local dir_name=$(basename "$target_dir")
# Use different clone methods based on environment
if is_ci && [[ -n "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]]; then
if is_ci && [[ -n "${SELFXYZ_APP_TOKEN:-}" ]]; then
# CI environment with GitHub App installation token
git clone "https://x-access-token:${SELFXYZ_APP_TOKEN}@github.com/selfxyz/${repo_name}.git" "$dir_name" || {
log "ERROR: Failed to clone $repo_name with GitHub App token"
exit 1
}
elif is_ci && [[ -n "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]]; then
# CI environment with PAT (fallback if action didn't run)
git clone "https://${SELFXYZ_INTERNAL_REPO_PAT}@github.com/selfxyz/android-passport-nfc-reader.git" || {
log "ERROR: Failed to clone android-passport-nfc-reader with PAT"
git clone "https://${SELFXYZ_INTERNAL_REPO_PAT}@github.com/selfxyz/${repo_name}.git" "$dir_name" || {
log "ERROR: Failed to clone $repo_name with PAT"
exit 1
}
elif [[ -n "${SSH_AUTH_SOCK:-}" ]] || [[ -f "${HOME}/.ssh/id_rsa" ]] || [[ -f "${HOME}/.ssh/id_ed25519" ]]; then
# Local development with SSH
git clone "git@github.com:selfxyz/android-passport-nfc-reader.git" || {
log "ERROR: Failed to clone android-passport-nfc-reader with SSH"
log "Please ensure you have SSH access to the repository or set SELFXYZ_INTERNAL_REPO_PAT"
git clone "git@github.com:selfxyz/${repo_name}.git" "$dir_name" || {
log "ERROR: Failed to clone $repo_name with SSH"
log "Please ensure you have SSH access to the repository or set SELFXYZ_APP_TOKEN/SELFXYZ_INTERNAL_REPO_PAT"
exit 1
}
else
log "ERROR: No authentication method available for cloning android-passport-nfc-reader"
log "ERROR: No authentication method available for cloning $repo_name"
log "Please either:"
log " - Set up SSH access (for local development)"
log " - Set SELFXYZ_INTERNAL_REPO_PAT environment variable (for CI)"
log " - Set SELFXYZ_APP_TOKEN or SELFXYZ_INTERNAL_REPO_PAT environment variable (for CI)"
exit 1
fi
cd ../../
log "✅ android-passport-nfc-reader cloned successfully"
elif is_ci; then
log "📁 android-passport-nfc-reader exists (likely cloned by GitHub action)"
else
log "📁 android-passport-nfc-reader already exists - preserving existing directory"
fi
log "$repo_name cloned successfully"
}
# Clone all required private modules
clone_private_module "android-passport-nfc-reader" "app/android/android-passport-nfc-reader"
clone_private_module "react-native-passport-reader" "app/android/react-native-passport-reader"
# Build and package the SDK with timeout (including dependencies)
log "Building SDK and dependencies..."
@@ -179,14 +200,15 @@ log "✅ Package files backed up successfully"
# Install SDK from tarball in app with timeout
log "Installing SDK as real files..."
if is_ci; then
# Temporarily unset PAT to skip private modules during SDK installation
env -u SELFXYZ_INTERNAL_REPO_PAT timeout 180 yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" || {
# Temporarily unset both auth tokens to skip private modules during SDK installation
# Both tokens must be unset to prevent setup-private-modules.cjs from attempting clones
env -u SELFXYZ_INTERNAL_REPO_PAT -u SELFXYZ_APP_TOKEN timeout 180 yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" || {
log "SDK installation timed out after 3 minutes"
exit 1
}
else
# Temporarily unset PAT to skip private modules during SDK installation
env -u SELFXYZ_INTERNAL_REPO_PAT yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH"
# Temporarily unset both auth tokens to skip private modules during SDK installation
env -u SELFXYZ_INTERNAL_REPO_PAT -u SELFXYZ_APP_TOKEN yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH"
fi
# Verify installation (check for AAR file in both local and hoisted locations)

View File

@@ -10,18 +10,28 @@ const path = require('path');
const SCRIPT_DIR = __dirname;
const APP_DIR = path.dirname(SCRIPT_DIR);
const ANDROID_DIR = path.join(APP_DIR, 'android');
const PRIVATE_MODULE_PATH = path.join(
ANDROID_DIR,
'android-passport-nfc-reader',
);
const GITHUB_ORG = 'selfxyz';
const REPO_NAME = 'android-passport-nfc-reader';
const BRANCH = 'main';
const PRIVATE_MODULES = [
{
repoName: 'android-passport-nfc-reader',
localPath: path.join(ANDROID_DIR, 'android-passport-nfc-reader'),
validationFiles: ['app/build.gradle', 'app/src/main/AndroidManifest.xml'],
},
{
repoName: 'react-native-passport-reader',
localPath: path.join(ANDROID_DIR, 'react-native-passport-reader'),
validationFiles: ['android/build.gradle'],
},
];
// Environment detection
const isCI = process.env.CI === 'true';
// CI is set by GitHub Actions, CircleCI, etc. Check for truthy value
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT;
const appToken = process.env.SELFXYZ_APP_TOKEN; // GitHub App installation token
const isDryRun = process.env.DRY_RUN === 'true';
// Platform detection for Android-specific modules
@@ -102,13 +112,13 @@ function sanitizeCommandForLogging(command) {
);
}
function removeExistingModule() {
if (fs.existsSync(PRIVATE_MODULE_PATH)) {
log(`Removing existing ${REPO_NAME}...`, 'cleanup');
function removeExistingModule(modulePath, repoName) {
if (fs.existsSync(modulePath)) {
log(`Removing existing ${repoName}...`, 'cleanup');
if (!isDryRun) {
// Force remove even if it's a git repo
fs.rmSync(PRIVATE_MODULE_PATH, {
fs.rmSync(modulePath, {
recursive: true,
force: true,
maxRetries: 3,
@@ -116,7 +126,7 @@ function removeExistingModule() {
});
}
log(`Removed existing ${REPO_NAME}`, 'success');
log(`Removed existing ${repoName}`, 'success');
}
}
// some of us connect to github via SSH, others via HTTPS with gh auth
@@ -136,18 +146,22 @@ function usingHTTPSGitAuth() {
}
}
function clonePrivateRepo() {
log(`Setting up ${REPO_NAME}...`, 'info');
function clonePrivateRepo(repoName, localPath) {
log(`Setting up ${repoName}...`, 'info');
let cloneUrl;
if (isCI && repoToken) {
if (isCI && appToken) {
// CI environment with GitHub App installation token
log('CI detected: Using SELFXYZ_APP_TOKEN for clone', 'info');
cloneUrl = `https://x-access-token:${appToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
} else if (isCI && repoToken) {
// CI environment with Personal Access Token
log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for clone', 'info');
cloneUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
cloneUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
} else if (isCI) {
log(
'CI environment detected but SELFXYZ_INTERNAL_REPO_PAT not available - skipping private module setup',
'CI environment detected but no token available - skipping private module setup',
'info',
);
log(
@@ -156,17 +170,18 @@ function clonePrivateRepo() {
);
return false; // Return false to indicate clone was skipped
} else if (usingHTTPSGitAuth()) {
cloneUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
cloneUrl = `https://github.com/${GITHUB_ORG}/${repoName}.git`;
} else {
// Local development with SSH
log('Local development: Using SSH for clone', 'info');
cloneUrl = `git@github.com:${GITHUB_ORG}/${REPO_NAME}.git`;
cloneUrl = `git@github.com:${GITHUB_ORG}/${repoName}.git`;
}
// Security: Use quiet mode for credentialed URLs to prevent token exposure
const isCredentialedUrl = isCI && repoToken;
const isCredentialedUrl = isCI && (appToken || repoToken);
const quietFlag = isCredentialedUrl ? '--quiet' : '';
const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" android-passport-nfc-reader`;
const targetDir = path.basename(localPath);
const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`;
try {
if (isCredentialedUrl) {
@@ -175,12 +190,12 @@ function clonePrivateRepo() {
} else {
runCommand(cloneCommand);
}
log(`Successfully cloned ${REPO_NAME}`, 'success');
log(`Successfully cloned ${repoName}`, 'success');
return true; // Return true to indicate successful clone
} catch (error) {
if (isCI) {
log(
'Clone failed in CI environment. Check SELFXYZ_INTERNAL_REPO_PAT permissions.',
'Clone failed in CI environment. Check SELFXYZ_APP_TOKEN or SELFXYZ_INTERNAL_REPO_PAT permissions.',
'error',
);
} else {
@@ -193,65 +208,99 @@ function clonePrivateRepo() {
}
}
function validateSetup() {
const expectedFiles = [
'app/build.gradle',
'app/src/main/AndroidManifest.xml',
];
for (const file of expectedFiles) {
const filePath = path.join(PRIVATE_MODULE_PATH, file);
function validateSetup(modulePath, validationFiles, repoName) {
for (const file of validationFiles) {
const filePath = path.join(modulePath, file);
if (!fs.existsSync(filePath)) {
throw new Error(`Expected file not found: ${file}`);
throw new Error(`Expected file not found in ${repoName}: ${file}`);
}
}
log('Private module validation passed', 'success');
log(`${repoName} validation passed`, 'success');
}
function setupPrivateModule(module) {
const { repoName, localPath, validationFiles } = module;
log(`Starting setup of ${repoName}...`, 'info');
// Remove existing module
removeExistingModule(localPath, repoName);
// Clone the private repository
const cloneSuccessful = clonePrivateRepo(repoName, localPath);
// If clone was skipped (e.g., in forked PRs), exit gracefully
if (cloneSuccessful === false) {
log(`${repoName} setup skipped - private module not available`, 'warning');
return false;
}
// Security: Remove credential-embedded remote URL after clone
if (isCI && (appToken || repoToken) && !isDryRun) {
scrubGitRemoteUrl(localPath, repoName);
}
// Validate the setup
if (!isDryRun) {
validateSetup(localPath, validationFiles, repoName);
}
log(`${repoName} setup complete!`, 'success');
return true;
}
function setupAndroidPassportReader() {
log(`Starting setup of ${REPO_NAME}...`, 'info');
// Ensure android directory exists
if (!fs.existsSync(ANDROID_DIR)) {
throw new Error(`Android directory not found: ${ANDROID_DIR}`);
}
// Remove existing module
removeExistingModule();
log(
`Starting setup of ${PRIVATE_MODULES.length} private module(s)...`,
'info',
);
// Clone the private repository
const cloneSuccessful = clonePrivateRepo();
// If clone was skipped (e.g., in forked PRs), exit gracefully
if (cloneSuccessful === false) {
log(`${REPO_NAME} setup skipped - private module not available`, 'warning');
return;
let successCount = 0;
for (const module of PRIVATE_MODULES) {
try {
const success = setupPrivateModule(module);
if (success) {
successCount++;
}
} catch (error) {
log(`Failed to setup ${module.repoName}: ${error.message}`, 'error');
throw error;
}
}
// Security: Remove credential-embedded remote URL after clone
if (isCI && repoToken && !isDryRun) {
scrubGitRemoteUrl();
if (successCount === PRIVATE_MODULES.length) {
log('All private modules setup complete!', 'success');
} else if (successCount > 0) {
log(
`Setup complete: ${successCount}/${PRIVATE_MODULES.length} modules cloned`,
'warning',
);
} else {
log(
'No private modules were cloned - this is expected for forked PRs',
'info',
);
}
// Validate the setup
if (!isDryRun) {
validateSetup();
}
log(`${REPO_NAME} setup complete!`, 'success');
}
function scrubGitRemoteUrl() {
function scrubGitRemoteUrl(modulePath, repoName) {
try {
const cleanUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
const scrubCommand = `cd "${PRIVATE_MODULE_PATH}" && git remote set-url origin "${cleanUrl}"`;
const cleanUrl = `https://github.com/${GITHUB_ORG}/${repoName}.git`;
const scrubCommand = `cd "${modulePath}" && git remote set-url origin "${cleanUrl}"`;
log('Scrubbing credential from git remote URL...', 'info');
log(`Scrubbing credential from git remote URL for ${repoName}...`, 'info');
runCommand(scrubCommand, { stdio: 'pipe' });
log('Git remote URL cleaned', 'success');
log(`Git remote URL cleaned for ${repoName}`, 'success');
} catch (error) {
log(`Warning: Failed to scrub git remote URL: ${error.message}`, 'warning');
log(
`Warning: Failed to scrub git remote URL for ${repoName}: ${error.message}`,
'warning',
);
// Non-fatal error - continue execution
}
}
@@ -274,5 +323,5 @@ if (require.main === module) {
module.exports = {
setupAndroidPassportReader,
removeExistingModule,
PRIVATE_MODULE_PATH,
PRIVATE_MODULES,
};

View File

@@ -179,14 +179,14 @@ describe('alias-imports transform', () => {
const appRoot = tempRoot;
const srcDir = join(appRoot, 'src');
const testsSrcDir = join(appRoot, 'tests', 'src');
const fileHaptic = join(srcDir, 'utils', 'haptic.ts');
const fileHaptic = join(srcDir, 'integrations', 'haptics.ts');
const deepSpecDir = join(testsSrcDir, 'deep');
const deepSpecFile = join(deepSpecDir, 'spec.ts');
writeFileEnsured(fileHaptic, 'export const h = 1;\n');
writeFileEnsured(
deepSpecFile,
"import { h } from '../../../src/utils/haptic';\nexport const v = h;\n",
"import { h } from '../../../src/integrations/haptics';\nexport const v = h;\n",
);
const project = new Project({
@@ -203,21 +203,24 @@ describe('alias-imports transform', () => {
const specFile = project.getSourceFileOrThrow(deepSpecFile);
const imports = specFile.getImportDeclarations();
assert.strictEqual(imports.length, 1);
assert.strictEqual(imports[0].getModuleSpecifierValue(), '@/utils/haptic');
assert.strictEqual(
imports[0].getModuleSpecifierValue(),
'@/integrations/haptics',
);
});
it("transforms deep relative require '../../../src/...' to @src alias from tests", () => {
const appRoot = tempRoot;
const srcDir = join(appRoot, 'src');
const testsSrcDir = join(appRoot, 'tests', 'src');
const fileHaptic = join(srcDir, 'utils', 'haptic.ts');
const fileHaptic = join(srcDir, 'integrations', 'haptics.ts');
const deepSpecDir = join(testsSrcDir, 'deep');
const deepSpecFile = join(deepSpecDir, 'req.ts');
writeFileEnsured(fileHaptic, 'module.exports = { h: 1 };\n');
writeFileEnsured(
deepSpecFile,
"const h = require('../../../src/utils/haptic');\nexport const v = h;\n",
"const h = require('../../../src/integrations/haptics');\nexport const v = h;\n",
);
const project = new Project({
@@ -232,7 +235,7 @@ describe('alias-imports transform', () => {
transformProjectToAliasImports(project, appRoot);
const specFile = project.getSourceFileOrThrow(deepSpecFile);
assert.ok(specFile.getText().includes("require('@/utils/haptic')"));
assert.ok(specFile.getText().includes("require('@/integrations/haptics')"));
});
it('aliases export star re-exports with ../ from sibling directory', () => {

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8.88281C0 8.58594 0.121094 8.32422 0.363281 8.09766L8.12109 0.351562C8.24609 0.226562 8.37109 0.136719 8.49609 0.0820312C8.62891 0.0273437 8.76562 0 8.90625 0C9.19531 0 9.4375 0.0976562 9.63281 0.292969C9.83594 0.480469 9.9375 0.71875 9.9375 1.00781C9.9375 1.14844 9.91016 1.28516 9.85547 1.41797C9.80859 1.54297 9.73828 1.65234 9.64453 1.74609L7.01953 4.41797L2.37891 8.66016L2.13281 8.07422L5.90625 7.83984H20.7305C21.0352 7.83984 21.2812 7.9375 21.4688 8.13281C21.6641 8.32812 21.7617 8.57812 21.7617 8.88281C21.7617 9.1875 21.6641 9.4375 21.4688 9.63281C21.2812 9.82812 21.0352 9.92578 20.7305 9.92578H5.90625L2.13281 9.69141L2.37891 9.11719L7.01953 13.3477L9.64453 16.0195C9.73828 16.1133 9.80859 16.2266 9.85547 16.3594C9.91016 16.4844 9.9375 16.6172 9.9375 16.7578C9.9375 17.0469 9.83594 17.2852 9.63281 17.4727C9.4375 17.668 9.19531 17.7656 8.90625 17.7656C8.625 17.7656 8.37109 17.6562 8.14453 17.4375L0.363281 9.66797C0.121094 9.44141 0 9.17969 0 8.88281Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="22" height="24" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.04004 18.7129C1.41699 18.7129 0.919271 18.5446 0.546875 18.208C0.181641 17.8714 -0.000976562 17.4274 -0.000976562 16.876C-0.000976562 16.4678 0.0921224 16.0846 0.27832 15.7266C0.464518 15.3613 0.708008 15.0212 1.00879 14.7061C1.30957 14.3838 1.62826 14.0723 1.96484 13.7715C2.24414 13.5352 2.4554 13.2129 2.59863 12.8047C2.74902 12.3965 2.86361 11.9346 2.94238 11.4189C3.02116 10.9033 3.09993 10.3626 3.17871 9.79688C3.26465 8.57943 3.46875 7.48014 3.79102 6.49902C4.11328 5.51074 4.56803 4.67643 5.15527 3.99609C5.74967 3.30859 6.49089 2.80729 7.37891 2.49219C7.62956 1.76888 8.05208 1.17448 8.64648 0.708984C9.24089 0.236328 9.93555 0 10.7305 0C11.3607 0 11.9193 0.14681 12.4062 0.44043C11.9622 0.905924 11.6077 1.4502 11.3428 2.07324C11.085 2.68913 10.9561 3.34798 10.9561 4.0498C10.9561 5.00944 11.1924 5.8903 11.665 6.69238C12.1449 7.4873 12.7822 8.12467 13.5771 8.60449C14.3792 9.08431 15.2637 9.32422 16.2305 9.32422C16.5885 9.32422 16.9359 9.28841 17.2725 9.2168C17.6162 9.13802 17.9421 9.02702 18.25 8.88379C18.3001 9.20605 18.3431 9.53906 18.3789 9.88281C18.4147 10.2194 18.4469 10.5596 18.4756 10.9033C18.5186 11.2829 18.5794 11.6553 18.6582 12.0205C18.737 12.3857 18.8408 12.7223 18.9697 13.0303C19.1058 13.3382 19.2812 13.5853 19.4961 13.7715C19.8327 14.0723 20.1514 14.3838 20.4521 14.7061C20.7529 15.0212 20.9964 15.3613 21.1826 15.7266C21.376 16.0846 21.4727 16.4678 21.4727 16.876C21.4727 17.4274 21.2865 17.8714 20.9141 18.208C20.5417 18.5446 20.0439 18.7129 19.4209 18.7129H2.04004ZM10.7412 23.3428C10.068 23.3428 9.46289 23.1995 8.92578 22.9131C8.39583 22.6338 7.96973 22.2614 7.64746 21.7959C7.3252 21.3304 7.14258 20.8219 7.09961 20.2705H14.3828C14.3398 20.8219 14.1572 21.3304 13.835 21.7959C13.5127 22.2614 13.083 22.6338 12.5459 22.9131C12.016 23.1995 11.4144 23.3428 10.7412 23.3428ZM16.2412 7.73438C15.568 7.73438 14.9521 7.56608 14.3936 7.22949C13.835 6.8929 13.3874 6.44531 13.0508 5.88672C12.7214 5.32812 12.5566 4.71582 12.5566 4.0498C12.5566 3.37663 12.7214 2.76074 13.0508 2.20215C13.3874 1.64355 13.835 1.19954 14.3936 0.870117C14.9521 0.533529 15.568 0.365234 16.2412 0.365234C16.9072 0.365234 17.5195 0.533529 18.0781 0.870117C18.6367 1.19954 19.0843 1.64355 19.4209 2.20215C19.7575 2.76074 19.9258 3.37663 19.9258 4.0498C19.9258 4.71582 19.7575 5.32812 19.4209 5.88672C19.0843 6.44531 18.6367 6.8929 18.0781 7.22949C17.5195 7.56608 16.9072 7.73438 16.2412 7.73438Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 845 B

After

Width:  |  Height:  |  Size: 845 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.71289 20.2451C0.589844 19.1318 0.0234375 18.0088 0.0136719 16.876C0.00390625 15.7334 0.555664 14.6006 1.66895 13.4775L13.4902 1.6709C14.6035 0.557617 15.7266 0.00585937 16.8594 0.015625C18.002 0.0253906 19.1348 0.591797 20.2578 1.71484L32.0059 13.4629C33.1289 14.5859 33.6904 15.7188 33.6904 16.8613C33.7002 18.0039 33.1484 19.127 32.0352 20.2305L20.2285 32.0518C19.1152 33.1553 17.9873 33.7021 16.8447 33.6924C15.7119 33.6924 14.584 33.1309 13.4609 32.0078L1.71289 20.2451ZM15.2334 24.083C15.5068 24.083 15.7559 24.0195 15.9805 23.8926C16.2148 23.7656 16.4199 23.5752 16.5957 23.3213L23.4512 12.6279C23.5488 12.4717 23.6367 12.3057 23.7148 12.1299C23.793 11.9443 23.832 11.7686 23.832 11.6025C23.832 11.2217 23.6855 10.9141 23.3926 10.6797C23.1094 10.4453 22.7871 10.3281 22.4258 10.3281C21.9473 10.3281 21.5518 10.582 21.2393 11.0898L15.1748 20.7871L12.3623 17.2129C12.167 16.9688 11.9766 16.7979 11.791 16.7002C11.6055 16.6025 11.3955 16.5537 11.1611 16.5537C10.79 16.5537 10.4727 16.6904 10.209 16.9639C9.95508 17.2275 9.82812 17.5449 9.82812 17.916C9.82812 18.1016 9.8623 18.2822 9.93066 18.458C9.99902 18.6338 10.0967 18.8047 10.2236 18.9707L13.8125 23.3359C14.0273 23.5996 14.2471 23.79 14.4717 23.9072C14.6963 24.0244 14.9502 24.083 15.2334 24.083Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 4V8L10.5 9.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 444 B

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="38" height="26" viewBox="0 0 38 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.2539 26.002H9.00977C7.73047 26.002 6.54395 25.7822 5.4502 25.3428C4.36621 24.9131 3.41406 24.3271 2.59375 23.585C1.77344 22.833 1.13379 21.9639 0.674805 20.9775C0.225586 19.9912 0.000976562 18.9414 0.000976562 17.8281C0.000976562 16.6074 0.220703 15.4844 0.660156 14.459C1.09961 13.4238 1.72461 12.5645 2.53516 11.8809C3.3457 11.1973 4.30762 10.7578 5.4209 10.5625C5.45996 9.53711 5.70898 8.60938 6.16797 7.7793C6.62695 6.93945 7.22266 6.24609 7.95508 5.69922C8.69727 5.14258 9.52246 4.77148 10.4307 4.58594C11.3389 4.39063 12.252 4.41992 13.1699 4.67383C13.7852 3.78516 14.5176 2.98926 15.3672 2.28613C16.2168 1.58301 17.1787 1.02637 18.2529 0.616211C19.3369 0.206055 20.5332 0.000976562 21.8418 0.000976562C23.375 0.000976562 24.8008 0.279297 26.1191 0.835938C27.4375 1.39258 28.5898 2.18359 29.5762 3.20898C30.5723 4.23438 31.3438 5.44043 31.8906 6.82715C32.4473 8.21387 32.7256 9.7373 32.7256 11.3975C33.6924 11.7979 34.5273 12.3545 35.2305 13.0674C35.9336 13.7803 36.4707 14.6006 36.8418 15.5283C37.2129 16.4561 37.3984 17.4375 37.3984 18.4727C37.3984 19.5176 37.1885 20.4941 36.7686 21.4023C36.3584 22.3105 35.7822 23.1113 35.04 23.8047C34.2979 24.4883 33.4287 25.0254 32.4326 25.416C31.4463 25.8066 30.3867 26.002 29.2539 26.002ZM17.6377 19.9961C18.1748 19.9961 18.5947 19.7568 18.8975 19.2783L24.6396 10.0498C24.7178 9.92285 24.791 9.78125 24.8594 9.625C24.9375 9.45898 24.9766 9.28809 24.9766 9.1123C24.9766 8.76074 24.8447 8.46777 24.5811 8.2334C24.3174 7.99902 24.0049 7.88184 23.6436 7.88184C23.1553 7.88184 22.7695 8.12109 22.4863 8.59961L17.5791 16.7588L15.0889 13.5654C14.8057 13.165 14.4346 12.9648 13.9756 12.9648C13.624 12.9648 13.3164 13.0918 13.0527 13.3457C12.7988 13.5898 12.6719 13.9023 12.6719 14.2832C12.6719 14.6055 12.7939 14.9229 13.0381 15.2354L16.3193 19.3076C16.5049 19.542 16.7051 19.7178 16.9199 19.835C17.1348 19.9424 17.374 19.9961 17.6377 19.9961Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.79297 18.6377C2.56836 18.6377 1.63021 18.3154 0.978516 17.6709C0.326823 17.0192 0.000976562 16.0846 0.000976562 14.8672V3.77051C0.000976562 2.55306 0.326823 1.62207 0.978516 0.977539C1.63021 0.325846 2.56836 0 3.79297 0H14.8467C16.0641 0 16.9987 0.325846 17.6504 0.977539C18.3021 1.62207 18.6279 2.55306 18.6279 3.77051V6.41309H15.8564V4.11426C15.8564 3.65592 15.7419 3.31934 15.5127 3.10449C15.2907 2.88249 14.9648 2.77148 14.5352 2.77148H4.09375C3.66406 2.77148 3.33464 2.88249 3.10547 3.10449C2.88346 3.31934 2.77246 3.65592 2.77246 4.11426V14.5234C2.77246 14.9818 2.88346 15.3219 3.10547 15.5439C3.33464 15.7588 3.66406 15.8662 4.09375 15.8662H6.92969V18.6377H3.79297ZM9.38965 23.9766C8.15788 23.9766 7.21615 23.6507 6.56445 22.999C5.91276 22.3545 5.58691 21.4235 5.58691 20.2061V9.10938C5.58691 7.89193 5.91276 6.96094 6.56445 6.31641C7.21615 5.66471 8.15788 5.33887 9.38965 5.33887H20.4326C21.6572 5.33887 22.5918 5.66471 23.2363 6.31641C23.888 6.9681 24.2139 7.89909 24.2139 9.10938V20.2061C24.2139 21.4235 23.888 22.3545 23.2363 22.999C22.5918 23.6507 21.6572 23.9766 20.4326 23.9766H9.38965ZM9.69043 21.2051H20.1211C20.5508 21.2051 20.8766 21.0941 21.0986 20.8721C21.3278 20.6572 21.4424 20.3206 21.4424 19.8623V9.45312C21.4424 8.99479 21.3278 8.6582 21.0986 8.44336C20.8766 8.22852 20.5508 8.12109 20.1211 8.12109H9.69043C9.25358 8.12109 8.92057 8.22852 8.69141 8.44336C8.4694 8.6582 8.3584 8.99479 8.3584 9.45312V19.8623C8.3584 20.3206 8.4694 20.6572 8.69141 20.8721C8.92057 21.0941 9.25358 21.2051 9.69043 21.2051Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36">
<path fill="currentColor" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/>
</svg>

After

Width:  |  Height:  |  Size: 774 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

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