Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
320cd46
P-1525 React Native SDK
yosriady Jan 29, 2026
239fc7c
Fix TypeScript build errors
yosriady Jan 29, 2026
d2fe5aa
Fix cleanup race condition and reset session state
yosriady Jan 29, 2026
2760ad2
Fix race conditions and consent key collision
yosriady Jan 29, 2026
be619e2
Fix event deduplication and wagmi config tracking
yosriady Jan 29, 2026
610d344
Fix multiple bugs identified in code review
yosriady Jan 29, 2026
fbb0953
Fix multiple bugs from code review
yosriady Jan 29, 2026
29ee10e
Simplify consent key and document deduplication behavior
yosriady Jan 29, 2026
08b75a5
Improve security and fix chain tracking issues
yosriady Jan 29, 2026
235af1d
Fix recursive call in status change processing
yosriady Jan 29, 2026
721af74
Improve useFormo hook and cleanup error handling
yosriady Jan 29, 2026
389ea2b
Fix code review issues: options ref, flush mutex, cleanup leak
yosriady Jan 29, 2026
f4f90a7
Fix Date object corruption in toSnakeCase
yosriady Jan 30, 2026
ae27956
Add error handling for flush() in enqueue threshold check
yosriady Jan 30, 2026
b30eef2
Fix cleanup() to flush all queued events
yosriady Jan 30, 2026
27c6836
Fix storage fallback caching and chainId validation consistency
yosriady Jan 30, 2026
5e04c24
push
yosriady Jan 30, 2026
f783740
Add rdns to connect event from wagmi integration
yosriady Jan 30, 2026
abb689f
Add chainId validation to handleSignatureMutation
yosriady Jan 30, 2026
b3ea77f
Make chainId required for signature() method
yosriady Jan 30, 2026
8262340
fix build warnings
yosriady Feb 3, 2026
5c8c95c
Add enhanced mobile context properties and UTM tracking
yosriady Feb 3, 2026
21d8f4b
Use full SHA-256 hash for message_id to match web SDK format
yosriady Feb 4, 2026
151e8a3
Add unit tests
yosriady Feb 4, 2026
3ef6db7
Fix type interface, cleanup safety, and provider race condition
yosriady Feb 4, 2026
b5e3ea2
Add CI workflow for running tests and build
yosriady Feb 4, 2026
461249c
update lockfile
yosriady Feb 4, 2026
cd3668a
Fix CI workflow pnpm version conflict and duplicate runs
yosriady Feb 4, 2026
89bdafc
Fix CI workflow and TypeScript errors in tests
yosriady Feb 4, 2026
e8025f3
Add Expo Go compatibility for device info
yosriady Feb 4, 2026
bbb5088
Fix CI lockfile issue and add missing test mocks
yosriady Feb 4, 2026
786f73f
Use --no-frozen-lockfile in CI to handle package.json updates
yosriady Feb 4, 2026
a67ff3e
Fix Jest configuration and useFormo warning spam
yosriady Feb 4, 2026
2e5d788
Use --frozen-lockfile in CI for reproducible builds
yosriady Feb 4, 2026
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
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
node_modules

# Build output
lib/

# IDE
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Test coverage
coverage/

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Environment
.env
.env.local
.env.*.local
303 changes: 302 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,302 @@
# sdk-react-native
# @formo/react-native-analytics

Formo Analytics SDK for React Native - Track wallet events and user analytics in mobile dApps.

## Installation

```bash
npm install @formo/react-native-analytics @react-native-async-storage/async-storage

# or with yarn
yarn add @formo/react-native-analytics @react-native-async-storage/async-storage

# or with pnpm
pnpm add @formo/react-native-analytics @react-native-async-storage/async-storage
```

### iOS Setup

```bash
cd ios && pod install
```

## Quick Start

### 1. Wrap your app with the provider

```tsx
import AsyncStorage from '@react-native-async-storage/async-storage';
import { FormoAnalyticsProvider } from '@formo/react-native-analytics';

function App() {
return (
<FormoAnalyticsProvider
writeKey="your-write-key"
asyncStorage={AsyncStorage}
options={{
app: {
name: 'MyApp',
version: '1.0.0',
},
}}
>
<YourApp />
</FormoAnalyticsProvider>
);
}
```

### 2. Use the hook in your components

```tsx
import { useFormo } from '@formo/react-native-analytics';
import { useEffect } from 'react';

function HomeScreen() {
const formo = useFormo();

useEffect(() => {
// Track screen view
formo.screen('Home');
}, []);

const handleSignUp = () => {
// Track custom event
formo.track('Sign Up Button Pressed', {
screen: 'Home',
});
};

return <Button onPress={handleSignUp}>Sign Up</Button>;
}
```

## Wagmi Integration

For dApps using Wagmi for wallet connections, you can enable automatic wallet event tracking:

```tsx
import { QueryClient } from '@tanstack/react-query';
import { createConfig } from 'wagmi';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { FormoAnalyticsProvider } from '@formo/react-native-analytics';

const queryClient = new QueryClient();
const wagmiConfig = createConfig({
// your wagmi config
});

function App() {
return (
<FormoAnalyticsProvider
writeKey="your-write-key"
asyncStorage={AsyncStorage}
options={{
wagmi: {
config: wagmiConfig,
queryClient: queryClient,
},
}}
>
<YourApp />
</FormoAnalyticsProvider>
);
}
```

This automatically tracks:
- Wallet connections and disconnections
- Chain changes
- Signature requests (with QueryClient)
- Transaction events (with QueryClient)

## API Reference

### FormoAnalyticsProvider Props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `writeKey` | `string` | Yes | Your Formo write key |
| `asyncStorage` | `AsyncStorageInterface` | Yes | AsyncStorage instance |
| `options` | `Options` | No | Configuration options |
| `disabled` | `boolean` | No | Disable analytics |

### Options

```typescript
interface Options {
// Wagmi integration
wagmi?: {
config: any; // Wagmi config from createConfig()
queryClient?: any; // TanStack QueryClient for mutation tracking
};

// App information
app?: {
name?: string;
version?: string;
build?: string;
bundleId?: string;
};

// Event batching
flushAt?: number; // Batch size (default: 20)
flushInterval?: number; // Flush interval in ms (default: 30000)
retryCount?: number; // Retry count (default: 3)
maxQueueSize?: number; // Max queue size in bytes (default: 500KB)

// Autocapture control
autocapture?: boolean | {
connect?: boolean;
disconnect?: boolean;
signature?: boolean;
transaction?: boolean;
chain?: boolean;
};

// Tracking control
tracking?: boolean | {
excludeChains?: number[];
};

// Logging
logger?: {
enabled?: boolean;
levels?: ('debug' | 'info' | 'warn' | 'error' | 'log')[];
};

// Custom API host
apiHost?: string;

// Ready callback
ready?: (formo: IFormoAnalytics) => void;
}
```

### useFormo Hook Methods

#### `screen(name, properties?, context?, callback?)`
Track a screen view.

```typescript
formo.screen('Profile', { userId: '123' });
```

#### `track(event, properties?, context?, callback?)`
Track a custom event.

```typescript
formo.track('Purchase Completed', {
revenue: 99.99,
currency: 'USD',
productId: 'nft-001',
});
```

#### `identify(params, properties?, context?, callback?)`
Identify a user by their wallet address.

```typescript
formo.identify({
address: '0x1234...',
userId: 'user-123',
providerName: 'MetaMask',
rdns: 'io.metamask',
});
```

#### `connect(params, properties?, context?, callback?)`
Track wallet connection.

```typescript
formo.connect({
chainId: 1,
address: '0x1234...',
});
```

#### `disconnect(params?, properties?, context?, callback?)`
Track wallet disconnection.

```typescript
formo.disconnect({
chainId: 1,
address: '0x1234...',
});
```

#### `chain(params, properties?, context?, callback?)`
Track chain change.

```typescript
formo.chain({
chainId: 137,
address: '0x1234...',
});
```

#### `signature(params, properties?, context?, callback?)`
Track signature event.

```typescript
import { SignatureStatus } from '@formo/react-native-analytics';

formo.signature({
status: SignatureStatus.CONFIRMED,
chainId: 1,
address: '0x1234...',
message: 'Sign this message',
signatureHash: '0xabcd...',
});
```

#### `transaction(params, properties?, context?, callback?)`
Track transaction event.

```typescript
import { TransactionStatus } from '@formo/react-native-analytics';

formo.transaction({
status: TransactionStatus.BROADCASTED,
chainId: 1,
address: '0x1234...',
to: '0x5678...',
value: '1000000000000000000',
transactionHash: '0xdef...',
});
```

#### Consent Management

```typescript
// Opt out of tracking (GDPR compliance)
formo.optOutTracking();

// Check opt-out status
const isOptedOut = formo.hasOptedOutTracking();

// Opt back in
formo.optInTracking();
```

## Event Types

The SDK automatically enriches events with mobile-specific context:

- Device information (OS, version, model)
- Screen dimensions and density
- Locale and timezone
- App information (if provided)
- Anonymous ID (persistent across sessions)

## Offline Support

Events are queued locally and sent when the device has network connectivity. Events are automatically flushed when:

- The batch size is reached (default: 20 events)
- The flush interval passes (default: 30 seconds)
- The app goes to background

## License

MIT
3 changes: 3 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
};
34 changes: 34 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'react-native',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testMatch: ['**/__tests__/**/*.test.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|@react-native-community|react-native-device-info)/)',
],
moduleNameMapper: {
'^@formo/react-native-analytics$': '<rootDir>/src/index',
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'node',
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/index.ts',
'!src/types/**/*',
],
coverageThreshold: {
global: {
branches: 50,
functions: 50,
lines: 50,
statements: 50,
},
},
clearMocks: true,
resetMocks: true,
restoreMocks: true,
};
Loading