Open Source Job State Transport SDK for React Native
Real-time job tracking with Live Activity support for React Native apps. Perfect for AI video generation, image processing, and long-running async tasks.
- ✅ Real-time updates via Polling
- ✅ iOS Live Activity - Lock Screen & Dynamic Island (iOS 16.2+)
- ✅ Android Ongoing Notification - Persistent foreground notification
- ✅ Multi-job support - Track up to 5 concurrent Live Activities
- ✅ React hooks for easy integration
- ✅ TypeScript support
- ✅ Parent-child jobs tracking
- ✅ ETA countdown with confidence scoring
- ✅ Provisional Push (iOS 12+) - no permission prompt
- ✅ Standalone mode - Use with any backend (Firebase, Supabase, custom)
- ✅ Error codes - Standardized error handling with
SeennErrorCode - ✅ Debug mode - Detailed logging for development
- ✅ Open source (MIT License)
npm install @seenn/react-native
# or
yarn add @seenn/react-native
# or
pnpm add @seenn/react-nativeimport { Seenn } from '@seenn/react-native';
const seenn = new Seenn({
baseUrl: 'https://api.seenn.io', // Seenn Cloud
// OR: 'https://api.yourapp.com' for self-hosted
apiKey: 'pk_your_key',
debug: true, // Enable logging
});import { useEffect } from 'react';
function App() {
useEffect(() => {
// Connect and start polling for updates
seenn.connect('user_123');
// Cleanup on unmount
return () => {
seenn.disconnect();
};
}, []);
return <YourApp />;
}import { useSeennJob } from '@seenn/react-native';
function VideoGenerationScreen({ jobId }) {
const job = useSeennJob(seenn, jobId);
if (!job) return <Text>Loading...</Text>;
return (
<View>
<Text>{job.title}</Text>
<ProgressBar value={job.progress || 0} />
{job.eta && <Text>ETA: {job.eta}s</Text>}
{job.stage && (
<Text>
Stage: {job.stage.label} ({job.stage.index}/{job.stage.total})
</Text>
)}
</View>
);
}Configure polling interval for your needs:
const seenn = new Seenn({
baseUrl: 'https://api.yourcompany.com',
apiKey: 'pk_your_key', // or any token for self-hosted
pollInterval: 3000, // Poll every 3 seconds (default: 5000)
});
await seenn.connect(userId);
// Subscribe to specific jobs for polling
seenn.subscribeJobForPolling('job_123');
seenn.subscribeJobsForPolling(['job_456', 'job_789']);
// Jobs auto-unsubscribe when completed/failed/cancelledTrack a specific job.
import { useSeennJob } from '@seenn/react-native';
function JobTracker({ jobId }) {
const job = useSeennJob(seenn, jobId);
if (!job) return <Text>Job not found</Text>;
return (
<View>
<Text>Status: {job.status}</Text>
<Text>Progress: {job.progress}%</Text>
</View>
);
}Track job with lifecycle callbacks.
import { useSeennJobProgress } from '@seenn/react-native';
function VideoGenerator({ jobId }) {
const job = useSeennJobProgress(seenn, jobId, {
onProgress: (job) => {
console.log(`Progress: ${job.progress}%`);
},
onComplete: (job) => {
Alert.alert('Done!', 'Video is ready');
// Navigate to result screen
},
onFailed: (job) => {
Alert.alert('Error', job.error?.message);
},
});
return <ProgressView job={job} />;
}Get all tracked jobs.
import { useSeennJobs } from '@seenn/react-native';
function JobList() {
const jobs = useSeennJobs(seenn);
return (
<FlatList
data={Array.from(jobs.values())}
keyExtractor={(job) => job.jobId}
renderItem={({ item }) => <JobCard job={item} />}
/>
);
}Monitor connection state.
import { useSeennConnectionState } from '@seenn/react-native';
function ConnectionIndicator() {
const state = useSeennConnectionState(seenn);
return (
<View style={[styles.indicator, { backgroundColor: getColor(state) }]}>
<Text>{state}</Text>
</View>
);
}
function getColor(state) {
switch (state) {
case 'connected':
return 'green';
case 'connecting':
case 'reconnecting':
return 'orange';
default:
return 'red';
}
}Filter jobs by status.
import { useSeennJobsByStatus } from '@seenn/react-native';
function RunningJobs() {
const runningJobs = useSeennJobsByStatus(seenn, 'running');
return <Text>{runningJobs.length} jobs running</Text>;
}Show job progress on the Lock Screen and Dynamic Island (iOS 16.2+).
Why iOS 16.2? While Live Activities were introduced in iOS 16.1, the push token API (
pushType: .token) andActivityContentstruct required for remote backend updates were added in iOS 16.2. Seenn's core feature is updating Live Activities from your backend via APNs push.
1. Add Widget Extension to your Xcode project:
- Open your iOS project in Xcode
- File → New → Target → Widget Extension
- Name it
SeennWidgetExtension - Copy files from
node_modules/@seenn/react-native/templates/SeennWidgetExtension/
2. Add NSSupportsLiveActivities to Info.plist:
<key>NSSupportsLiveActivities</key>
<true/>3. Run pod install:
cd ios && pod installimport { useSeennJob, useLiveActivity } from '@seenn/react-native';
function JobScreen({ jobId }) {
const job = useSeennJob(seenn, jobId);
// Auto-sync job state with Live Activity
const { isActive, isSupported } = useLiveActivity(job, {
autoStart: true, // Start when job begins running
autoEnd: true, // End when job completes/fails
dismissAfter: 300, // Keep on screen 5 min after completion
});
return (
<View>
<Text>{job?.title}</Text>
<Text>Progress: {job?.progress}%</Text>
{isSupported && <Text>Live Activity: {isActive ? 'On' : 'Off'}</Text>}
</View>
);
}import { LiveActivity } from '@seenn/react-native';
// Check support
const supported = await LiveActivity.isSupported();
// Start activity
const result = await LiveActivity.start({
jobId: 'job_123',
title: 'Generating video...',
jobType: 'video-generation',
initialProgress: 0,
});
// Update progress
await LiveActivity.update({
jobId: 'job_123',
progress: 50,
status: 'running',
message: 'Encoding frames...',
stageName: 'Encoding',
stageIndex: 2,
stageTotal: 3,
});
// End activity
await LiveActivity.end({
jobId: 'job_123',
finalStatus: 'completed',
message: 'Video ready!',
resultUrl: 'https://example.com/video.mp4',
dismissAfter: 300,
});
// Get active activities
const activeIds = await LiveActivity.getActiveIds();
// ['job_123', 'job_456']
// Cancel all
await LiveActivity.cancelAll();All Live Activity operations return a result with error codes for programmatic handling:
import { LiveActivity, SeennErrorCode, SDK_VERSION } from '@seenn/react-native';
console.log('SDK Version:', SDK_VERSION); // '0.9.10'
const result = await LiveActivity.start({
jobId: 'job_123',
title: 'Processing...',
});
if (!result.success) {
switch (result.code) {
case SeennErrorCode.PLATFORM_NOT_SUPPORTED:
console.log('Not on iOS');
break;
case SeennErrorCode.INVALID_JOB_ID:
console.log('Invalid job ID');
break;
case SeennErrorCode.NATIVE_MODULE_NOT_FOUND:
console.log('Native setup incomplete');
break;
default:
console.log(`Error [${result.code}]: ${result.error}`);
}
}iOS allows up to 5 concurrent Live Activities per app:
// Start multiple activities
await LiveActivity.start({ jobId: 'job_1', title: 'Video 1', ... });
await LiveActivity.start({ jobId: 'job_2', title: 'Video 2', ... });
await LiveActivity.start({ jobId: 'job_3', title: 'Image Pack', ... });
// Each updates independently
await LiveActivity.update({ jobId: 'job_1', progress: 50, ... });
await LiveActivity.update({ jobId: 'job_2', progress: 75, ... });
// Check which are active
const activeIds = await LiveActivity.getActiveIds();
console.log(activeIds); // ['job_1', 'job_2', 'job_3']import { LiveActivity } from '@seenn/react-native';
// Listen for push tokens
const unsubscribe = LiveActivity.onPushToken((event) => {
console.log(`Token for ${event.jobId}: ${event.token}`);
// Send to your backend for APNs push updates
sendTokenToBackend(event.jobId, event.token);
});
// Later: unsubscribe()Request push notifications without showing a permission prompt:
import { LiveActivity } from '@seenn/react-native';
// Check current status
const status = await LiveActivity.getPushAuthorizationStatus();
console.log(status.status); // 'provisional', 'authorized', etc.
console.log(status.isProvisional); // true if quiet notifications
// Request provisional push (no prompt!)
const granted = await LiveActivity.requestProvisionalPushAuthorization();
if (granted) {
console.log('Provisional push enabled');
}
// Later: upgrade to full push when ready
if (status.canRequestFullAuthorization) {
await LiveActivity.upgradeToStandardPush(); // Shows prompt
}Note: Provisional notifications appear silently in Notification Center only. Users can "Keep" or "Turn Off" from their first notification.
import { useSeennPush } from '@seenn/react-native';
function App() {
const { token, authorizationStatus, requestProvisional } = useSeennPush({
debug: true, // Enable detailed logging in __DEV__
autoRefresh: true, // Auto-refresh token on mount
onTokenReceived: async (token) => {
await api.registerDevice({ userId, deviceToken: token });
},
onAuthorizationStatus: (status) => {
console.log('Auth status:', status);
},
onError: (error) => {
console.error('Push error:', error);
},
});
return <YourApp />;
}For Expo projects, Live Activity works via expo-live-activity (Software Mansion).
Note: Requires Expo Dev Client. Not compatible with Expo Go.
# If you already have @seenn/react-native installed:
npx expo install expo-live-activity
# Fresh installation (both packages):
npx expo install expo-live-activity @seenn/react-native{
"expo": {
"plugins": ["expo-live-activity"]
}
}npx expo prebuild --cleanimport { useSeennJob, useExpoLiveActivity } from '@seenn/react-native';
function JobScreen({ jobId }) {
const job = useSeennJob(seenn, jobId);
// Auto-sync with Expo Live Activity
const { isActive, isSupported } = useExpoLiveActivity(job, {
autoStart: true,
autoEnd: true,
colors: {
backgroundColor: '#1c1c1e',
progressTint: '#3b82f6',
},
deepLinkUrl: 'myapp://jobs/job_123',
});
return (
<View>
<Text>{job?.title}</Text>
<Text>Progress: {job?.progress}%</Text>
</View>
);
}import { ExpoLiveActivity } from '@seenn/react-native';
// Check if available
const isAvailable = ExpoLiveActivity.isAvailable();
// Start activity
const activityId = await ExpoLiveActivity.start(job, {
backgroundColor: '#1c1c1e',
progressViewTint: '#3b82f6',
});
// Update activity
await ExpoLiveActivity.update(activityId, job);
// Stop activity
await ExpoLiveActivity.stop(activityId, job);| Feature | Native Module | Expo (expo-live-activity) |
|---|---|---|
| Setup | Manual Xcode | npx expo prebuild |
| Custom UI | Full SwiftUI | Config-based |
| Expo Go | No | No |
| Dev Client | No | Yes |
| Multi-job | Yes (5 max) | Yes (5 max) |
If you're not using React hooks, you can subscribe manually:
// Subscribe to a job
const unsubscribe = seenn.subscribeToJob('job_123', (job) => {
console.log(`Job updated:`, job);
});
// Unsubscribe when done
unsubscribe();
// Subscribe to connection state
const unsubscribeState = seenn.subscribeToConnectionState((state) => {
console.log(`Connection: ${state}`);
});
// Synchronous access
const currentJob = seenn.getJob('job_123');
const allJobs = seenn.getAllJobs();
const state = seenn.getConnectionState();const seenn = new Seenn({
// Required
baseUrl: 'https://api.seenn.io',
// Optional
apiKey: 'pk_your_key', // API key (pk_* for Seenn Cloud)
pollInterval: 5000, // Polling interval in ms (default: 5000)
basePath: '/v1', // API base path (default: '/v1')
debug: false, // Enable debug logging
});Seenn Cloud (Recommended):
const seenn = new Seenn({
baseUrl: 'https://api.seenn.io',
apiKey: 'pk_your_key',
});Your Own Backend (Self-Hosted):
const seenn = new Seenn({
baseUrl: 'https://api.yourapp.com',
apiKey: 'your_jwt_or_api_key', // Any token format works
basePath: '/api/seenn', // Custom path if needed
});Requirements for self-hosted:
- Implement job endpoints (
GET /jobs/:id,GET /jobs?userId=...) - Return jobs in Seenn format
- Handle job state management (any database)
See Self-Hosted Guide for details.
import React, { useEffect, useState } from 'react';
import { View, Text, Button, ActivityIndicator } from 'react-native';
import { Seenn, useSeennJobProgress } from '@seenn/react-native';
const seenn = new Seenn({
baseUrl: 'https://api.yourapp.com',
apiKey: 'your_jwt_token',
});
function VideoGeneratorScreen() {
const [jobId, setJobId] = useState<string | null>(null);
useEffect(() => {
seenn.connect('user_123');
return () => seenn.disconnect();
}, []);
const startGeneration = async () => {
const response = await fetch('https://api.yourapp.com/v1/videos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: 'A cat playing piano',
duration: 5,
}),
});
const { jobId } = await response.json();
setJobId(jobId);
};
if (!jobId) {
return <Button title="Generate Video" onPress={startGeneration} />;
}
return <VideoProgress jobId={jobId} />;
}
function VideoProgress({ jobId }: { jobId: string }) {
const job = useSeennJobProgress(seenn, jobId, {
onComplete: (job) => {
console.log('Video ready:', job.result?.url);
},
onFailed: (job) => {
console.error('Failed:', job.error?.message);
},
});
if (!job) return <ActivityIndicator />;
return (
<View>
<Text>{job.title}</Text>
<Text>Status: {job.status}</Text>
{job.progress !== undefined && <Text>Progress: {job.progress}%</Text>}
{job.stage && (
<Text>
{job.stage.label} ({job.stage.index}/{job.stage.total})
</Text>
)}
{job.eta && <Text>ETA: {job.eta}s</Text>}
{job.queue && <Text>Queue: {job.queue.position}/{job.queue.total}</Text>}
{job.status === 'completed' && job.result && (
<Text>Video URL: {job.result.url}</Text>
)}
{job.status === 'failed' && job.error && (
<Text style={{ color: 'red' }}>Error: {job.error.message}</Text>
)}
</View>
);
}
export default VideoGeneratorScreen;interface SeennJob {
jobId: string;
userId: string;
appId: string;
status: 'queued' | 'running' | 'completed' | 'failed';
title: string;
progress?: number; // 0-100
stage?: StageInfo;
eta?: number; // seconds
queue?: QueueInfo;
result?: JobResult;
error?: JobError;
// Parent-child jobs
parentJobId?: string;
childProgressMode?: 'average' | 'weighted' | 'sequential';
children?: ChildJob[];
childrenCompleted?: number;
childrenTotal?: number;
createdAt: string;
updatedAt: string;
}
interface StageInfo {
id: string;
label: string;
index: number; // Current stage
total: number; // Total stages
}
interface QueueInfo {
position: number;
total: number;
estimatedWaitSeconds?: number;
}
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';Yes! The SDK is open source (MIT). You can:
- Use Seenn Cloud (recommended, easy setup)
- Self-host with your own backend (guide)
Yes! iOS Live Activity is fully supported. See the Live Activity Setup section.
MIT © Seenn
Contributions welcome!
git clone https://github.com/seenn-io/react-native
cd react-native
npm install
npm run dev