React Native SDK
@seenn/react-native — TypeScript-first SDK with React hooks, iOS Live Activity and Android Ongoing Notification support
#Installation
npm install @seenn/react-native
# or
yarn add @seenn/react-native
#iOS Setup (Live Activity)
Live Activity requires native code setup due to iOS module isolation. Follow these steps:
Step 1: Install and link the native module:
cd ios && pod install
Step 2: Add NSSupportsLiveActivities to your Info.plist:
<key>NSSupportsLiveActivities</key>
<true/>
Step 3: Copy bridge files from the SDK templates to your Xcode project:
node_modules/@seenn/react-native/templates/SeennLiveActivityBridge/SeennLiveActivityBridgeImpl.swiftnode_modules/@seenn/react-native/templates/SeennWidgetExtension/SeennJobAttributes.swift
Step 4: Register the bridge in your AppDelegate.swift:
import SeennReactNative
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Register Live Activity bridge
if #available(iOS 16.2, *) {
SeennLiveActivityRegistry.shared.register(SeennLiveActivityBridgeImpl.shared)
}
return true
}
}
Step 5: Create a Widget Extension for the Lock Screen UI. Copy the template files from node_modules/@seenn/react-native/templates/SeennWidgetExtension/.
#Initialization
Initialize the SDK once at app startup (inside your root App component or provider).
import { Seenn } from '@seenn/react-native';
import { useEffect } from 'react';
export default function App() {
useEffect(() => {
// Initialize once on mount
Seenn.init({
apiKey: 'pk_live_xxx', // Your public API key
userId: 'user_123', // Your user ID
});
return () => {
// Cleanup on unmount
Seenn.dispose();
};
}, []);
return <AppContent />;
}
#Configuration Options
Seenn.init({
apiKey: 'pk_live_xxx',
userId: 'user_123',
config: {
apiUrl: 'https://api.seenn.io',
pollInterval: 5000, // 5 seconds
debug: __DEV__, // Enable debug logs in development
},
});
#Real-time Updates
The SDK uses polling to receive job updates from your backend:
| Aspect | Details |
|---|---|
| How it works | SDK fetches job state via HTTP at intervals (default 5s) |
| Backend requirement | GET /v1/jobs/:id endpoint only |
| Latency | Poll interval (configurable, default 5s) |
| Best for | All backends: Firebase, Supabase, custom, Seenn Cloud |
#Polling Configuration
Configure polling interval and subscribe to job updates:
import { Seenn } from '@seenn/react-native';
// Initialize with polling mode
Seenn.init({
apiKey: 'your-api-key',
userId: 'user_123',
config: {
baseUrl: 'https://api.yourcompany.com',
basePath: '/v1',
mode: 'polling', // ← Explicit polling mode
pollInterval: 5000, // 5 seconds (default)
},
});
// Subscribe to job updates (starts polling)
seenn.subscribeJobForPolling('job_123');
// Or subscribe to multiple jobs
seenn.subscribeJobsForPolling(['job_456', 'job_789']);
// Unsubscribe when done
seenn.unsubscribeJobFromPolling('job_123');
GET /v1/jobs/:id endpoint needed.
See API Reference.
#useSeennJob()
React hook to subscribe to real-time updates for a specific job. Automatically manages subscription lifecycle.
import { useSeennJob } from '@seenn/react-native';
import { View, Text, ActivityIndicator } from 'react-native';
function JobProgress({ jobId }: { jobId: string }) {
const { job, isLoading, error } = useSeennJob(jobId);
if (isLoading) {
return <ActivityIndicator />;
}
if (error) {
return <Text>Error: {error.message}</Text>;
}
return (
<View>
<Text>{job.title}</Text>
<Text>Progress: {job.progress}%</Text>
{job.message && <Text>{job.message}</Text>}
{job.stage && (
<Text>
Stage: {job.stage.label} ({job.stage.index}/{job.stage.total})
</Text>
)}
{job.eta && (
<Text>ETA: {job.eta}s</Text>
)}
{job.status === 'completed' && (
<Button title="Download" onPress={() => openUrl(job.resultUrl)} />
)}
</View>
);
}
#Return Values
| Property | Type | Description |
|---|---|---|
job |
SeennJob | null |
Current job state (null while loading) |
isLoading |
boolean |
True while fetching initial job data |
error |
Error | null |
Error if job fetch/subscription fails |
#useSeennJobs()
React hook to list all jobs for the current user with optional filtering.
import { useSeennJobs } from '@seenn/react-native';
import { FlatList, Text } from 'react-native';
function JobsList() {
const { jobs, isLoading, refetch } = useSeennJobs({
status: ['running', 'queued'],
limit: 20,
});
if (isLoading) {
return <ActivityIndicator />;
}
return (
<FlatList
data={jobs}
keyExtractor={(item) => item.jobId}
onRefresh={refetch}
refreshing={isLoading}
renderItem={({ item }) => (
<View>
<Text>{item.title}</Text>
<Text>{item.progress}% - {item.status}</Text>
</View>
)}
/>
);
}
#Options
| Property | Type | Description |
|---|---|---|
status |
JobStatus[]? |
Filter by status (e.g., ['running', 'queued']) |
limit |
number? |
Max number of jobs to return (default: 50) |
#Reliability Methods
The SDK provides methods for manual connection recovery and state management (v0.2.5+).
#reconnect()
Force reconnect. Useful for recovering from connection issues or when the app returns to foreground.
import { useEffect } from 'react';
import { AppState } from 'react-native';
// Reconnect when app comes to foreground
useEffect(() => {
const subscription = AppState.addEventListener('change', (state) => {
if (state === 'active') {
seenn.reconnect();
}
});
return () => subscription.remove();
}, []);
#updateJob()
Manually update job state. Useful for optimistic updates or when you receive job data from other sources.
// Manually update job state from external source
const fetchJobManually = async (jobId: string) => {
const response = await fetch(`https://api.seenn.io/v1/jobs/${jobId}`, {
headers: { 'Authorization': `Bearer ${publicKey}` }
});
const job = await response.json();
seenn.updateJob(job);
};
// Connection state based polling
const connectionState = useSeennConnectionState(seenn);
useEffect(() => {
if (connectionState === 'disconnected') {
const interval = setInterval(() => {
fetchJobManually(activeJobId);
}, 30000); // Poll every 30s
return () => clearInterval(interval);
}
}, [connectionState]);
#Job Events
The SDK handles these job events automatically:
| Event | Description |
|---|---|
job.sync |
State reconciliation - sent for all active jobs on connect/reconnect |
job.started |
Job has started processing |
job.progress |
Job progress updated |
job.completed |
Job completed successfully |
job.failed |
Job failed with error |
job.cancelled |
Job was cancelled |
connection.idle |
Server closing due to 5 minutes of inactivity (auto-reconnect handles this) |
job.sync events for all active jobs. This ensures the client has accurate state even after network interruptions.
#Live Activity (iOS)
The SDK provides native iOS Live Activity support (v0.2.0+), showing job progress on Dynamic Island and Lock Screen. Supports up to 5 concurrent Live Activities per app.
#Setup
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 install
#useLiveActivity() Hook (Recommended)
Auto-sync job state with Live Activity — the easiest way to integrate:
import { useSeennJob, useLiveActivity } from '@seenn/react-native';
function JobScreen({ jobId }: { jobId: string }) {
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>
);
}
#Manual Control API
For full control over Live Activity lifecycle:
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();
#Multi-Job Support
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']
#Push Token for Background Updates
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()
Auto-sync with Polling
When using useLiveActivity() hook with autoStart: true, Live Activity updates are automatically synced via polling. You don't need to manually call update() — the SDK handles it for you!
#CTA Button (v0.6.0+)
Add a tappable button to your Live Activity when a job completes or fails. The button opens a deep link in your app:
import { useLiveActivity } from '@seenn/react-native';
const { isActive } = useLiveActivity(job, {
autoStart: true,
autoEnd: true,
// CTA button on completion
completionCTA: {
text: 'See Your Photos ✨',
deepLink: (job) => `myapp://jobs/${job.jobId}/results`,
},
// Or different buttons for completed vs failed
ctaButtons: {
completed: {
text: 'View Results',
deepLink: (job) => `myapp://results/${job.jobId}`,
},
failed: {
text: 'Try Again',
deepLink: (job) => `myapp://retry/${job.jobId}`,
},
},
});
Or use the manual API with custom styling:
await LiveActivity.end({
jobId: 'job_123',
finalStatus: 'completed',
message: 'Your Photos are Ready!',
ctaButton: {
text: 'See Your Photos ✨',
deepLink: 'myapp://jobs/job_123/results',
style: 'primary', // 'primary' | 'secondary' | 'outline'
backgroundColor: '#FFFFFF',
textColor: '#000000',
cornerRadius: 20,
},
});
#Widget Setup CLI (v0.6.0+)
Use the @seenn/setup-widget CLI to create a customizable Widget Extension:
# Install the CLI
npm install @seenn/setup-widget --save-dev
# Initialize widget configuration
npx @seenn/setup-widget init
# After editing seenn.config.js, regenerate Swift files
npx @seenn/setup-widget update
This creates a seenn.config.js file for theme customization:
// seenn.config.js
module.exports = {
theme: {
preset: 'gradient-sunset', // or 'default', 'gradient-ocean', etc.
colors: {
progressBar: '#FFFFFF',
},
layout: {
showIcon: true,
showEta: true,
showStage: true,
},
cta: {
style: { backgroundColor: '#FFFFFF', textColor: '#000000' },
},
},
jobTypes: {
'video-generation': { icon: 'video.fill', color: '#9B59B6' },
'image-generation': { icon: 'photo.fill', color: '#3498DB' },
},
};
Available theme presets:
| Preset | Description |
|---|---|
default | Dark background with blue progress bar |
gradient-sunset | Orange to yellow gradient |
gradient-ocean | Purple to blue gradient |
gradient-purple | Purple to pink gradient |
minimal-dark | Black background, white text |
minimal-light | White background, black text |
#Push Enhancements (Media & Communication)
iOS supports two types of enhanced push notifications with images:
| Type | Image Position | iOS Version | Use Case |
|---|---|---|---|
| Media Attachment | RIGHT side (thumbnail) | iOS 10+ | Product images, album art, photos |
| Communication Notification | LEFT side (avatar) | iOS 15+ | Messages, DMs (iMessage/WhatsApp style) |
Media Attachment (thumbnail RIGHT): ┌────────────────────────────────────┬────┐ │ Remix Complete! │ 🖼️ │ ← imageUrl │ Summer Vibes is ready to play │ │ └────────────────────────────────────┴────┘ Communication Notification (avatar LEFT): ┌────────────────────────────────────────┐ │ 👤 DJ Mix │ ← senderAvatar │ Check out this remix I made! │ └────────────────────────────────────────┘
#Setup
Both features require a Notification Service Extension. The @seenn/setup-widget CLI creates everything automatically:
# Creates Widget Extension + Notification Service Extension
npx @seenn/setup-widget init
# Skip if you only need Live Activities
npx @seenn/setup-widget init --skip-rich-push
After running the CLI, complete these steps in Xcode:
- Open your
.xcworkspacein Xcode - File → New → Target → Notification Service Extension
- Name:
NotificationServiceExtension - Run
npx @seenn/setup-widget linkto configure - Link Intents framework: Select NotificationServiceExtension target → Build Phases → Link Binary With Libraries → + →
Intents.framework
NotificationService.swift automatically routes notifications — media attachments and communication notifications work out of the box.
#Media Attachment (Thumbnail on RIGHT)
Display an image thumbnail on the right side of the notification. Works on iOS 10+. Can be added to any push type (alert, silent, or communication).
// Backend: Alert notification with media attachment
await job.complete({
push: {
alert: {
title: 'Remix Complete!',
body: 'Summer Vibes is ready to play',
},
// Media attachment - thumbnail on RIGHT side
media: {
imageUrl: 'https://example.com/album-art.jpg',
},
},
});
// Also works with silent/passive push!
await job.setProgress(50, {
push: {
silent: true,
media: { imageUrl: 'https://example.com/preview.jpg' },
},
});
#Communication Notification (Avatar on LEFT)
Display a sender avatar on the left side, like iMessage or WhatsApp. Requires iOS 15+ and uses INSendMessageIntent under the hood.
// Backend: Send communication notification
await job.complete({
push: {
// Communication notification - avatar on LEFT side
communication: {
senderName: 'DJ Mix',
senderAvatar: 'https://example.com/avatar.jpg',
conversationId: 'conv-123', // Optional: for grouping
},
// Optional: break through Focus mode
timeSensitive: true,
},
});
Extra Setup for Communication Notifications
Communication notifications require one additional step — add this to your main app's Info.plist:
<!-- Add to ios/YourApp/Info.plist -->
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
#Combined: Avatar + Thumbnail
You can use both communication and media together — avatar on LEFT, thumbnail on RIGHT:
// Backend: Communication + Media (avatar LEFT, thumbnail RIGHT)
await job.complete({
push: {
communication: {
senderName: 'Sarah',
senderAvatar: 'https://example.com/sarah.jpg',
},
media: {
imageUrl: 'https://example.com/shared-photo.jpg',
},
},
});
Combined: Avatar LEFT + Thumbnail RIGHT ┌────────────────────────────────────┬────┐ │ 👤 Sarah │ 🖼️ │ │ Check out this photo! │ │ └────────────────────────────────────┴────┘ ↑ senderAvatar ↑ imageUrl
#API Reference
| Field | Type | Description |
|---|---|---|
| push.media (Media Attachment) | ||
imageUrl | string | URL of image to show as thumbnail on RIGHT |
| push.communication (Communication Notification) | ||
senderName | string | Sender name displayed as title |
senderAvatar | string? | URL of avatar image to show on LEFT |
conversationId | string? | Identifier for grouping messages |
| push (Top-level options) | ||
timeSensitive | boolean? | Break through Focus mode (iOS 15+) |
#Supported Image Formats
| Format | Extension | Notes |
|---|---|---|
| JPEG | .jpg, .jpeg | Best for photos |
| PNG | .png | Supports transparency |
| GIF | .gif | Static only (no animation) |
| WebP | .webp | iOS 14+ |
mutable-content: 1 in the APNs payload. Without it, iOS won't invoke the Notification Service Extension.
#Provisional Push (iOS 12+)
Request push notification permission without showing a prompt. Users see "quiet" notifications in Notification Center only — no sounds or banners.
#Usage
import { LiveActivity } from '@seenn/react-native';
// Check current authorization 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 shown!
const granted = await LiveActivity.requestProvisionalPushAuthorization();
if (granted) {
console.log('Provisional push enabled');
}
// Later: upgrade to full push (shows prompt)
if (status.canRequestFullAuthorization) {
const upgraded = await LiveActivity.upgradeToStandardPush();
if (upgraded) {
console.log('Full push access granted');
}
}
#Authorization Status Values
| Status | Description |
|---|---|
notDetermined | Permission never requested |
denied | User denied permission |
authorized | Full push access (alerts, sounds, badges) |
provisional | Quiet notifications only (Notification Center) |
ephemeral | App Clips only (iOS 14+) |
#Device Push Token (v0.9.0+)
When you request provisional or standard push authorization, the SDK automatically registers for remote notifications and provides the device push token:
import { LiveActivity } from '@seenn/react-native';
// Listen for both Live Activity and device push tokens
const unsubscribe = LiveActivity.onPushToken((event) => {
if (event.type === 'liveActivity') {
// Live Activity token - for updating a specific Live Activity via APNs
console.log(`Live Activity token for ${event.jobId}: ${event.token}`);
myBackend.registerLiveActivityToken(event.jobId, event.token);
} else if (event.type === 'device') {
// Device token - for regular push notifications
console.log(`Device push token: ${event.token}`);
myBackend.registerDevicePushToken(event.token);
}
});
// Request provisional push - token will be emitted via onPushToken
await LiveActivity.requestProvisionalPushAuthorization();
// Don't forget to unsubscribe when done
unsubscribe();
| Token Type | Description | jobId |
|---|---|---|
liveActivity | APNs token for updating a specific Live Activity | Present |
device | APNs token for regular push notifications | Absent |
#Expo Live Activity
For Expo projects, Live Activity works via expo-live-activity (Software Mansion).
#Installation (Expo)
# 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
Add to app.json:
{
"expo": {
"plugins": ["expo-live-activity"]
}
}
Then prebuild:
npx expo prebuild --clean
#useExpoLiveActivity() Hook
import { useSeennJob, useExpoLiveActivity } from '@seenn/react-native';
function JobScreen({ jobId }: { jobId: string }) {
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>
);
}
#Manual Control (Expo)
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);
#Expo vs Native Module
| 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) |
expo-live-activity package has significant limitations compared to the Native Module approach:
- ❌ CTA Button — Not supported (API doesn't expose it)
- ❌ Gradient backgrounds — Only solid colors via config
- ❌ Custom SwiftUI layout — Config-based UI only
- ⚠️ Deep link — Works on activity tap, not via CTA button
Recommendation: If you need CTA buttons (tappable button on completion) or advanced customization, use the Native Module approach with @seenn/setup-widget CLI.
#Ongoing Notification (Android)
The SDK provides native Android Ongoing Notification support (v0.3.0+), showing job progress as a persistent notification in the notification drawer. This is the Android equivalent of iOS Live Activities.
#Setup
No additional setup required! The SDK automatically:
- Creates a notification channel for job progress
- Requests notification permission on Android 13+
- Shows ongoing (non-dismissable) notifications during progress
#useJobNotification() Hook (Cross-Platform)
Works on both iOS and Android — the recommended way to integrate:
import { useSeennJob, useJobNotification } from '@seenn/react-native';
function JobScreen({ jobId }: { jobId: string }) {
const job = useSeennJob(seenn, jobId);
// Auto-sync job state with notification (iOS + Android)
const { isActive, isSupported, platform } = useJobNotification(job);
return (
<View>
<Text>{job?.title}</Text>
<Text>Progress: {job?.progress}%</Text>
{isSupported && <Text>Notification: {isActive ? 'Active' : 'Off'} ({platform})</Text>}
</View>
);
}
#Manual Control API (Android)
For Android-specific control:
import { OngoingNotification } from '@seenn/react-native';
// Check support (Android 8.0+)
const supported = await OngoingNotification.isSupported();
// Check permission (Android 13+)
const hasPermission = await OngoingNotification.hasPermission();
// Start notification
const result = await OngoingNotification.start({
jobId: 'job_123',
title: 'Generating video...',
initialProgress: 0,
});
// Update progress
await OngoingNotification.update({
jobId: 'job_123',
progress: 50,
status: 'running',
message: 'Encoding frames...',
stageName: 'Encoding',
stageIndex: 2,
stageTotal: 3,
});
// End notification
await OngoingNotification.end({
jobId: 'job_123',
finalStatus: 'completed',
message: 'Video ready!',
dismissAfter: 5000, // 5 seconds (milliseconds)
});
// Cancel all notifications
await OngoingNotification.cancelAll();
#Unified JobNotification API
Single API that works on both platforms:
import { JobNotification } from '@seenn/react-native';
// Works on iOS (Live Activity) and Android (Ongoing Notification)
await JobNotification.start({ jobId, title, initialProgress: 0 });
await JobNotification.update({ jobId, progress: 50, status: 'running' });
await JobNotification.end({ jobId, finalStatus: 'completed' });
// Check current platform
console.log(JobNotification.platform); // 'ios' | 'android' | 'other'
#Platform Comparison
| Feature | iOS (Live Activity) | Android (Ongoing Notification) |
|---|---|---|
| Min Version | iOS 16.2+ | Android 8.0+ (API 26) |
| Location | Dynamic Island + Lock Screen | Notification Drawer |
| Concurrent Jobs | 5 max | No practical limit |
| Dismissable | No (during progress) | No (ongoing flag) |
| Permission | Settings toggle | POST_NOTIFICATIONS (13+) |
| Push Updates | APNs token | Not needed (polling) |
#Parent-Child Jobs
Track batch processing jobs with automatic progress aggregation. Perfect for AI image packs, multi-stage pipelines, and bulk operations.
#Basic Batch UI
function BatchJobProgress({ jobId }: { jobId: string }) {
const { job } = useSeennJob(jobId);
return (
<View>
<Text style={styles.title}>{job.title}</Text>
{/* Progress bar */}
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${job.progress}%` }]} />
</View>
{/* Batch counter: "3/5 images completed" */}
{job.childrenTotal && (
<Text style={styles.counter}>
{job.childrenCompleted}/{job.childrenTotal} images completed
</Text>
)}
{/* Individual child status */}
{job.children?.map((child) => (
<View key={child.childId} style={styles.childRow}>
<StatusIcon status={child.status} />
<Text>{child.title}</Text>
{child.status === 'running' && <Text>{child.progress}%</Text>}
</View>
))}
</View>
);
}
#Live Activity for Batch Jobs (iOS)
Show batch progress in Dynamic Island and Lock Screen:
import { Seenn, useSeennJob } from '@seenn/react-native';
function BatchWithLiveActivity({ parentJobId }: { parentJobId: string }) {
const { job } = useSeennJob(parentJobId);
// Start Live Activity when job begins
useEffect(() => {
const startActivity = async () => {
if (job?.status === 'running') {
await Seenn.liveActivity.start({
jobId: parentJobId,
title: job.title,
// Pass batch info for custom UI
metadata: {
childrenTotal: job.childrenTotal,
childrenCompleted: job.childrenCompleted,
},
});
}
};
startActivity();
}, [job?.status]);
// Live Activity auto-updates via polling!
// When job.childrenCompleted changes, widget refreshes
return (
<View>
<Text>{job.childrenCompleted}/{job.childrenTotal} completed</Text>
</View>
);
}
useSeennJob(), the Live Activity UI automatically updates as children complete. No manual update() calls needed!
#Parent-Child Hooks
Specialized hooks for parent-child job tracking with built-in helpers.
#useParentJob()
Subscribe to a parent job with additional batch helpers.
import { useParentJob } from '@seenn/react-native';
function BatchProgress({ seenn, batchJobId }) {
const {
job, // The parent job
isParent, // true if this is a parent job
childStats, // { total, completed, failed, running, pending }
progress, // 0-100
allChildrenDone, // true when all children finished
allChildrenSuccess, // true when all completed successfully
hasFailedChildren, // true if any child failed
} = useParentJob(seenn, batchJobId);
if (!job) return <Text>Loading...</Text>;
return (
<View>
<Text>{job.title}</Text>
<ProgressBar progress={progress} />
{childStats && (
<Text>{childStats.completed}/{childStats.total} completed</Text>
)}
{hasFailedChildren && (
<Text style={{ color: 'red' }}>{childStats.failed} failed</Text>
)}
{allChildrenSuccess && <Text>All done!</Text>}
</View>
);
}
#useChildJob()
Subscribe to a child job with parent context.
import { useChildJob } from '@seenn/react-native';
function ChildJobCard({ seenn, childJobId }) {
const {
job, // The child job
isChild, // true if this is a child job
parentJobId, // Parent job ID
childIndex, // 0-based index within parent
isComplete, // true if completed
isFailed, // true if failed
} = useChildJob(seenn, childJobId);
if (!job) return null;
return (
<View style={styles.card}>
<Text>#{childIndex + 1}: {job.title}</Text>
<ProgressBar progress={job.progress} />
{isComplete && <Icon name="check" color="green" />}
{isFailed && <Icon name="x" color="red" />}
</View>
);
}
#useJobsByIds()
Subscribe to multiple specific jobs by ID. Useful for displaying all children of a parent.
import { useJobsByIds } from '@seenn/react-native';
function ChildrenList({ seenn, childJobIds }) {
// Subscribe to all children by their IDs
const childJobs = useJobsByIds(seenn, childJobIds);
return (
<View>
{childJobs.map((job) => (
<View key={job.jobId} style={styles.row}>
<StatusIcon status={job.status} />
<Text>{job.title}</Text>
<Text>{job.progress}%</Text>
</View>
))}
</View>
);
}
#TypeScript Types
All types are fully typed with TypeScript:
interface SeennJob {
jobId: string;
userId: string;
status: JobStatus; // 'pending' | 'queued' | 'running' | 'completed' | 'failed'
title: string;
progress: number; // 0-100
message?: string;
stage?: StageInfo;
queue?: QueueInfo;
eta?: number; // Seconds to completion
resultUrl?: string;
errorMessage?: string;
createdAt: string;
updatedAt: string;
// Parent-child fields
parentJobId?: string;
childProgressMode?: 'average' | 'weighted' | 'sequential';
children?: ChildJob[];
childrenCompleted?: number;
childrenTotal?: number;
}
interface StageInfo {
id: string;
label: string;
index: number;
total: number;
}
interface QueueInfo {
position: number;
total: number;
estimatedWaitSeconds?: number;
}
#ETA Countdown
The SDK provides smart ETA (Estimated Time of Arrival) support. The backend calculates ETA based on historical job data, and the SDK provides hooks for smooth countdown display.
#useEtaCountdown() Hook
React hook for smooth ETA countdown. Updates every second for smooth UI, syncs with server ETA on each progress update.
import { useSeennJob, useEtaCountdown } from '@seenn/react-native';
function JobProgress({ seenn, jobId }) {
const job = useSeennJob(seenn, jobId);
const eta = useEtaCountdown(job);
return (
<View>
<ProgressBar progress={job?.progress || 0} />
{eta.remaining !== null && (
<Text>~{eta.formatted} remaining</Text>
)}
{eta.isPastDue && (
<Text style={{ color: '#f59e0b' }}>
Taking longer than expected...
</Text>
)}
{eta.confidence !== null && eta.confidence < 0.5 && (
<Text style={{ opacity: 0.6 }}>
(estimate may vary)
</Text>
)}
</View>
);
}
workflowId (or jobType as fallback). After 5+ completed jobs, ETA is calculated from historical data. Before that, the estimatedDuration from job creation is used. ETA is recalculated on every progress update based on current rate.
#ETA Fields on SeennJob
| Field | Type | Description |
|---|---|---|
estimatedCompletionAt |
string? |
ISO 8601 timestamp for estimated completion |
workflowId |
string? |
ETA tracking key (defaults to jobType) |
etaConfidence |
number? |
Confidence score (0.0 - 1.0), higher = more reliable |
etaBasedOn |
number? |
Number of historical jobs used for calculation |
startedAt |
string? |
When the job started running (for elapsed time display) |
#Custom Backend
Using your own backend? Configure the SDK to point to your API:
Seenn.init({
apiKey: 'pk_live_xxx',
userId: 'user_123',
config: {
// Point to your own backend
apiUrl: 'https://api.yourcompany.com',
pollInterval: 5000, // 5 seconds
},
});
#Standalone Mode (BYO Backend)
Already have your own backend with job state management and APNs push? Use standalone mode to control Live Activities without connecting to any Seenn server. No Seenn.init() required.
- Self-Hosted / Polling Mode: You implement Seenn's REST API spec (
GET /v1/jobs/:id). The SDK polls your backend and manages state internally. UseSeenn.init()with your custombaseUrl. - Standalone Mode: You manage everything yourself — your own database, your own state management, your own APNs push. The SDK is just a thin native bridge to iOS Live Activity. No
Seenn.init()needed.
LiveActivity API works standalone out of the box. Import it and use it directly — no initialization needed.
#Basic Usage
import { LiveActivity } from '@seenn/react-native';
// No Seenn.init() needed!
// Start a Live Activity when job begins
await LiveActivity.start({
jobId: 'job_123',
title: 'Processing your video...',
jobType: 'video-processing',
initialProgress: 0,
});
// Update from your own state management
await LiveActivity.update({
jobId: 'job_123',
progress: 50,
status: 'running',
message: 'Encoding frames...',
});
// End with CTA button
await LiveActivity.end({
jobId: 'job_123',
finalStatus: 'completed',
message: 'Video ready!',
ctaButton: {
text: 'Watch Now',
deepLink: 'myapp://videos/job_123',
},
});
#Push Token Handling
When your backend handles APNs push for Live Activity updates, you need to capture and send the push token:
import { LiveActivity } from '@seenn/react-native';
import { useEffect } from 'react';
function App() {
useEffect(() => {
// Listen for Live Activity push tokens
const unsubscribe = LiveActivity.onPushToken((event) => {
// Send token to your backend
myApi.registerLiveActivityToken({
jobId: event.jobId,
token: event.token,
});
});
return () => unsubscribe();
}, []);
return <YourApp />;
}
#APNs Payload Format
Your backend sends APNs push updates directly to iOS. The payload must match the Widget Extension's ContentState:
// Progress update
{
"aps": {
"timestamp": 1706500000,
"event": "update",
"content-state": {
"progress": 75,
"status": "running",
"message": "Processing 3/4...",
"stageName": "Encoding",
"stageIndex": 3,
"stageTotal": 4
}
}
}
// Completion with CTA button
{
"aps": {
"timestamp": 1706500120,
"event": "end",
"dismissal-date": 1706500420,
"content-state": {
"progress": 100,
"status": "completed",
"message": "Your video is ready!",
"ctaButtonText": "Watch Now",
"ctaDeepLink": "myapp://videos/123"
}
}
}
ContentState Fields
| Field | Type | Description |
|---|---|---|
progress |
Int | Progress 0-100 |
status |
String | pending | running | completed | failed |
message |
String? | Status message to display |
stageName |
String? | Current stage name |
stageIndex |
Int? | Current stage index (1-based) |
stageTotal |
Int? | Total number of stages |
estimatedEndTime |
Int? | ETA as Unix timestamp (seconds) |
resultUrl |
String? | Result URL for completed jobs |
errorMessage |
String? | Error message for failed jobs |
ctaButtonText |
String? | CTA button label |
ctaDeepLink |
String? | Deep link URL for CTA button |
#LiveActivity API Reference
| Method | Description |
|---|---|
LiveActivity.start(params) |
Start a new Live Activity |
LiveActivity.update(params) |
Update an existing Live Activity |
LiveActivity.end(params) |
End a Live Activity with optional CTA |
LiveActivity.isSupported() |
Check if Live Activities are supported |
LiveActivity.areActivitiesEnabled() |
Check if user has enabled Live Activities |
LiveActivity.isActive(jobId) |
Check if a specific activity is active |
LiveActivity.getActiveIds() |
Get all active Live Activity job IDs |
LiveActivity.cancel(jobId) |
Cancel a specific activity immediately |
LiveActivity.cancelAll() |
Cancel all active activities |
LiveActivity.onPushToken(callback) |
Subscribe to push token events |
npx @seenn/setup-widget init to generate the required files. See Widget Setup CLI.
#Error Handling
The SDK provides typed error handling:
import { useSeennJob, SeennError } from '@seenn/react-native';
function JobProgress({ jobId }: { jobId: string }) {
const { job, error } = useSeennJob(jobId);
if (error) {
if (error instanceof SeennError) {
switch (error.code) {
case 'JOB_NOT_FOUND':
return <Text>Job not found</Text>;
case 'UNAUTHORIZED':
// Refresh user token
refreshToken();
break;
case 'RATE_LIMITED':
return <Text>Too many requests, please wait</Text>;
}
}
return <Text>Error: {error.message}</Text>;
}
// Render job...
}
#Error Codes
| Code | Description | Action |
|---|---|---|
JOB_NOT_FOUND |
Job doesn't exist | Check jobId, show error message |
UNAUTHORIZED |
Invalid/expired token | Refresh user token |
RATE_LIMITED |
Too many requests | Retry after delay |
CONNECTION_ERROR |
Connection failed | Auto-reconnect in progress |
#SDK Error Codes (v0.9.10+)
All Live Activity and JobNotification operations return a result with standardized 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}`);
}
}
| Code | Description |
|---|---|
PLATFORM_NOT_SUPPORTED |
Operation not supported on this platform (e.g., LiveActivity on Android) |
NATIVE_MODULE_NOT_FOUND |
Native module not found - native setup incomplete |
BRIDGE_NOT_REGISTERED |
Live Activity bridge not registered in AppDelegate |
ACTIVITY_NOT_FOUND |
Live Activity not found for the given jobId |
INVALID_JOB_ID |
Invalid or empty jobId |
INVALID_PROGRESS |
Progress value out of range (must be 0-100) |
INVALID_TITLE |
Invalid or empty title |
INVALID_STATUS |
Invalid status value |
UNKNOWN_ERROR |
Unknown error occurred |
#useSeennPush Hook (v0.9.9+)
Convenience hook for managing push notifications with auto-refresh and debug mode:
import { useSeennPush } from '@seenn/react-native';
function App() {
const {
token,
authorizationStatus,
isLoading,
requestProvisional,
requestStandard,
refreshToken,
} = 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);
},
});
useEffect(() => {
if (authorizationStatus === 'notDetermined') {
requestProvisional();
}
}, [authorizationStatus]);
return <YourApp />;
}
#Advanced Usage
#Update User Token
// When token expires, update it
await Seenn.setUserToken(newToken);
#Manual Reconnect
// Force reconnect
await Seenn.reconnect();
#Cleanup
// Call when user logs out
await Seenn.dispose();