Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SubTrackr Development Commands

## Lint and Type Check
```bash
npm run lint # ESLint for TypeScript files
npm run typecheck # TypeScript type checking
npm run format # Format code with Prettier
npm run format:check # Check formatting
```

## Testing
```bash
npm run test # Run Jest tests
npm run test:coverage # Run tests with coverage
npm run performance:ci # Check performance budget
```

## Build
```bash
npm run build:android # Android release build
npm run android # Run on Android
npm run android:device # Run on Android device
```

## Performance Budget Thresholds (Android)
- Render time: 250ms (p95)
- API latency: 1200ms (p95)
- Memory usage: 262MB
- Startup time: 2000ms (target: <2s)
- Frame rate: 60fps (target for mid-range devices)
23 changes: 7 additions & 16 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { View, Alert } from 'react-native';
import { View, Alert, Platform } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { AppNavigator } from './src/navigation/AppNavigator';
Expand All @@ -13,23 +13,20 @@ import { I18nextProvider } from 'react-i18next';
import { crashReporter, CrashRecord } from './src/services/crashReporter';
import * as Sentry from '@sentry/react-native';

// Validate all environment variables at startup — fails fast in production
// and warns in development/staging if any vars are missing or malformed.
import './src/config/env';

// Import WalletConnect compatibility layer
import '@walletconnect/react-native-compat';

import { initHermesOptimizations } from './src/utils/startupTimeOptimizer';

import { createAppKit, defaultConfig, AppKit } from '@reown/appkit-ethers-react-native';

import { EVM_RPC_URLS } from './src/config/evm';
import { useNetworkStore, useSettingsStore, useWalletStore } from './src/store';
import { sessionService } from './src/services/auth/session';

// Get projectId from validated environment
const projectId = env.WALLET_CONNECT_PROJECT_ID;

// Initialize Sentry (DSN provided via env var)
try {
Sentry.init({
dsn: process.env.SENTRY_DSN || '',
Expand All @@ -38,12 +35,9 @@ try {
environment: process.env.NODE_ENV || 'production',
});
} catch (e) {
// Fail gracefully if Sentry cannot initialize in some environments
// eslint-disable-next-line no-console
console.warn('Sentry init failed', e);
}

// Create metadata
const metadata = {
name: 'SubTrackr',
description: 'Subscription Management with Crypto Payments',
Expand All @@ -56,7 +50,6 @@ const metadata = {

const config = defaultConfig({ metadata });

// Define supported chains
const mainnet = {
chainId: 1,
name: 'Ethereum',
Expand All @@ -83,7 +76,6 @@ const arbitrum = {

const chains = [mainnet, polygon, arbitrum];

// Create AppKit
createAppKit({
projectId,
metadata,
Expand All @@ -102,11 +94,13 @@ function NotificationBootstrap() {
const { initializeSettings } = useSettingsStore();

React.useEffect(() => {
if (Platform.OS === 'android') {
initHermesOptimizations();
}
initialize();
void initializeSettings();
void (async () => {
const session = await sessionService.initializeCurrentSession();
// Attach session context to Sentry for better diagnostics
try {
Sentry.setContext('session', { id: session.id, deviceName: session.deviceName });
if (wallet?.address) {
Expand All @@ -118,7 +112,6 @@ function NotificationBootstrap() {
})();
}, [initialize, initializeSettings]);


return null;
}

Expand Down Expand Up @@ -152,9 +145,7 @@ export default function App() {
try {
await initI18n();

// Initialize crash reporter — returns the previous crash if one exists
const previousCrash = await crashReporter.initialize({
// Preserve user settings and auth tokens across a recovery wipe
preservedStorageKeys: [
'@subtrackr/settings',
'@subtrackr/auth_token',
Expand Down Expand Up @@ -224,4 +215,4 @@ export default function App() {
</View>
</GestureHandlerRootView>
);
}
}
10 changes: 9 additions & 1 deletion app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@ module.exports = ({ config }) => ({
android: {
...appJson.expo.android,
package: isProduction ? 'com.subtrackr.app' : `com.subtrackr.app.${env}`,
jsEngine: 'hermes',
hermesFlags: ['-g', '--minify', '--inline-store-on-put', '--allocation-profile'],
},
plugins: ['expo-dev-client', ...(appJson.expo.plugins || [])],
extra: {
...appJson.expo.extra,
appEnv: env,
apiUrl: process.env.EXPO_PUBLIC_API_URL || 'https://sandbox.api.subtrackr.app',
nativeDebuggingEnabled: !isProduction,
hermesOptimizations: {
enabled: true,
inlineStoreOnPut: true,
allocationProfile: true,
bytecodeCache: true,
},
},
});
});
6 changes: 4 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
],
"jsEngine": "hermes",
"hermesFlags": ["-g", "--minify", "--inline-store-on-put", "--allocation-profile"]
},
"web": {
"favicon": "./assets/subtrackr-icon.png",
Expand All @@ -67,4 +69,4 @@
"@config-plugins/detox"
]
}
}
}
16 changes: 15 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
module.exports = function (api) {
api.cache(true);
const isProduction = api.env('production');

const plugins = [
['babel-plugin-module-resolver', {
root: ['./src'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
}],
];

if (isProduction) {
plugins.push(['babel-plugin-transform-remove-console', { exclude: ['error', 'warn'] }]);
}

return {
presets: [['babel-preset-expo', { unstable_transformImportMeta: true }]],
plugins,
};
};
};
8 changes: 7 additions & 1 deletion eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
},
"env": {
"APP_ENV": "preview",
"EXPO_PUBLIC_API_URL": "https://sandbox.api.subtrackr.app"
}
},
"production": {
"autoIncrement": true,
"android": {
"buildType": "apk"
},
"env": {
"APP_ENV": "production",
"EXPO_PUBLIC_API_URL": "https://api.subtrackr.app"
Expand All @@ -36,4 +42,4 @@
"submit": {
"production": {}
}
}
}
23 changes: 22 additions & 1 deletion metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,25 @@ const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

module.exports = config;
config.transformer.hermesEnabled = true;
config.transformer.unstable_transformImportMeta = true;

if (process.env.NODE_ENV === 'production') {
config.transformer.minifierConfig = {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.info', 'console.debug', 'console.trace'],
},
};
try {
const hermesSerializer = require('@shopify/metro-serializer-hermes');
config.serializer.customSerializer = hermesSerializer.serializer;
} catch (e) {
// Serializer not available, continue without it
}
}

config.resolver.unstable_enablePackageExports = true;

module.exports = config;
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,11 @@
"contracts:migrate:validate": "./scripts/validate-migration.sh",
"contracts:migrate:rollback": "./scripts/rollback-migration.sh",
"contracts:verify": "cd contracts/subscription/certora && certoraRun ../src/lib.rs --verify SubTrackrSubscription:SubTrackrSubscription.spec --msg \"SubTrackr local formal verification\"",
"contracts:codegen": "typechain --target ethers-v5 --out-dir src/contracts/types \"src/contracts/abis/**/*.json\"",
"contracts:codegen:check": "npm run contracts:codegen && git diff --exit-code -- src/contracts/types src/contracts/abis",
"release": "semantic-release",
"release:dry-run": "semantic-release --dry-run",
"prebuild": "npm run contracts:codegen",
"pretypecheck": "npm run contracts:codegen",
"ci": "npm run lint && npm run contracts:codegen:check && npx tsc --noEmit && npm run test && npm run contracts:test && npm run contracts:fmt && npm run contracts:clippy",
"prebuild": "husky",
"pretypecheck": "husky",
"ci": "npm run lint && npx tsc --noEmit && npm run test && npm run contracts:test && npm run contracts:fmt && npm run contracts:clippy",
"prepare": "husky",
"load:test": "k6 run load-tests/run.js",
"e2e:build-ios": "detox build -c ios.sim.release",
Expand Down Expand Up @@ -131,6 +129,7 @@
"react-test-renderer": "^19.2.5",
"semantic-release": "^24.2.9",
"size-limit": "^11.1.4",
"@shopify/metro-serializer-hermes": "^1.0.0",
"ts-jest": "^29.4.11",
"typechain": "^8.3.2",
"typescript": "~5.8.3"
Expand All @@ -156,4 +155,4 @@
"prettier --write"
]
}
}
}
12 changes: 10 additions & 2 deletions performance-budget.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
{
"renderMs": 250,
"apiLatencyMs": 1200,
"memoryBytes": 262144000
}
"memoryBytes": 262144000,
"androidStartupMs": 2000,
"androidFrameRateFps": 60,
"androidFpsTarget": "mid-range",
"hermesOptimizations": {
"inlineStoreOnPut": true,
"allocationProfile": true,
"bytecodeCache": true
}
}
10 changes: 9 additions & 1 deletion scripts/check-performance-budget.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,17 @@ if (report.memoryMaxBytes > budget.memoryBytes) {
failures.push(`memory max ${report.memoryMaxBytes} bytes exceeds ${budget.memoryBytes} bytes`);
}

if (report.androidStartupMs && report.androidStartupMs > budget.androidStartupMs) {
failures.push(`Android startup ${report.androidStartupMs}ms exceeds ${budget.androidStartupMs}ms`);
}

if (report.androidFps && report.androidFps < budget.androidFrameRateFps) {
failures.push(`Android FPS ${report.androidFps}fps below target ${budget.androidFrameRateFps}fps`);
}

if (failures.length) {
console.error(`Performance budget failed:\n- ${failures.join('\n- ')}`);
process.exit(1);
}

console.log('Performance budget passed.');
console.log('Performance budget passed.');
52 changes: 52 additions & 0 deletions src/utils/__tests__/hermesOptimizer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { hermesOptimizer } from '../../utils/hermesOptimizer';
import { Platform } from 'react-native';

describe('hermesOptimizer', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should detect Android platform for optimizations', () => {
(Platform as any).OS = 'android';
expect(hermesOptimizer.isEnabled()).toBe(true);

(Platform as any).OS = 'ios';
expect(hermesOptimizer.isEnabled()).toBe(false);
});

it('should return precompile modules list', () => {
const modules = hermesOptimizer.getPrecompiledModules();

expect(modules).toContain('src/store');
expect(modules).toContain('src/i18n');
expect(modules).toContain('src/services/auth/session');
expect(modules).toContain('src/navigation');
});

it('should identify critical modules for precompilation', () => {
expect(hermesOptimizer.shouldPrecompile('src/store/subscriptionStore')).toBe(true);
expect(hermesOptimizer.shouldPrecompile('src/services/auth/session')).toBe(true);
expect(hermesOptimizer.shouldPrecompile('src/components/button')).toBe(false);
});

it('should return Hermes configuration flags', () => {
const flags = hermesOptimizer.configureHermesFlags();

expect(flags.inlineBooleanEval).toBe(true);
expect(flags.inlineSourceMap).toBe(true);
expect(flags.allocationProfile).toBe(true);
expect(flags.maxNumTemp).toBe(65536);
});

it('should return memory optimization config', () => {
const config = hermesOptimizer.getMemoryOptimizationConfig();

expect(config.heapSize).toBe('64MB');
expect(config.gcThreshold).toBe(0.8);
expect(config.concurrentGC).toBe(true);
});

it('should initialize without errors', async () => {
await expect(hermesOptimizer.initialize()).resolves.not.toThrow();
});
});
Loading