seenn_flutter
Flutter SDK for Seenn - Real-time job progress tracking with Live Activity (iOS), and Ongoing Notification (Android) support.
Features
- Real-time Updates - Polling with automatic reconnection
- iOS Live Activity - Lock Screen and Dynamic Island progress
- Android Ongoing Notification - Persistent notification with progress bar
- Provisional Push - iOS 12+ quiet notifications without permission prompt
- ETA Countdown - Smart time estimates with confidence scores
- Parent-Child Jobs - Hierarchical job relationships
- Reactive Streams - RxDart-powered state management
- Error Codes - Standardized error handling with
SeennErrorCode - Input Validation - Client-side validation before native calls
Installation
dependencies:
seenn_flutter: ^0.8.4
Quick Start
import 'package:seenn_flutter/seenn_flutter.dart';
// Initialize
final seenn = Seenn(SeennConfig(
publicKey: 'pk_live_xxx',
baseUrl: 'https://api.seenn.io',
));
// Connect for a user
await seenn.connect(userId: 'user_123');
// Subscribe to a job
final tracker = seenn.jobs.subscribe('job_abc');
tracker.onProgress.listen((update) {
print('Progress: ${update.progress}%');
print('ETA: ${update.etaFormatted}');
});
tracker.onComplete.listen((job) {
print('Job completed!');
});
iOS Live Activity
// Start Live Activity
await seenn.liveActivity.startActivity(
jobId: 'job_abc',
title: 'Processing video...',
);
// Auto-sync with job updates
seenn.jobs.stream('job_abc').listen((job) {
if (job != null) {
seenn.liveActivity.updateActivity(
jobId: job.id,
progress: job.progress,
message: job.message,
);
}
});
iOS Setup
Add to ios/Runner/Info.plist:
<key>NSSupportsLiveActivities</key>
<true/>
Provisional Push (iOS 12+)
Request push notifications without showing a permission prompt:
import 'package:seenn_flutter/seenn_flutter.dart';
// Check current status
final status = await LiveActivity.getPushAuthorizationStatus();
print(status.status); // PushAuthorizationStatus.provisional
print(status.isProvisional); // true if quiet notifications
// Request provisional push (no prompt!)
final granted = await LiveActivity.requestProvisionalPushAuthorization();
if (granted) {
print('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.
Error Handling
All Live Activity operations return results with error codes for programmatic handling:
import 'package:seenn_flutter/seenn_flutter.dart';
print('SDK Version: $sdkVersion'); // '0.8.4'
final result = await LiveActivity.start(
jobId: 'job_123',
title: 'Processing...',
jobType: 'video',
);
if (!result.success) {
switch (result.code) {
case SeennErrorCode.platformNotSupported:
print('Not on iOS');
break;
case SeennErrorCode.invalidJobId:
print('Invalid job ID');
break;
case SeennErrorCode.bridgeNotRegistered:
print('Native setup incomplete');
break;
default:
print('Error [${result.code}]: ${result.error}');
}
}
Android Ongoing Notification
// Start notification
await seenn.ongoingNotification.startNotification(
jobId: 'job_abc',
title: 'Processing',
message: 'Starting...',
);
// Update progress
await seenn.ongoingNotification.updateNotification(
jobId: 'job_abc',
progress: 50,
message: 'Halfway there...',
);
// End notification
await seenn.ongoingNotification.endNotification(
jobId: 'job_abc',
title: 'Complete',
message: 'Your video is ready!',
);
Cross-Platform Notifications
Use JobNotificationService for unified iOS + Android handling:
// Automatically uses Live Activity on iOS, Ongoing Notification on Android
await seenn.jobNotification.startNotification(
jobId: 'job_abc',
title: 'Processing',
message: 'Starting...',
);
// Sync with job updates
await seenn.jobNotification.syncWithJob(job);
ETA Countdown
// Get countdown stream
final countdown = etaCountdownStream(
job: job,
intervalMs: 1000,
);
countdown.listen((state) {
print('Remaining: ${state.formatted}'); // "2:34"
print('Past due: ${state.isPastDue}');
print('Confidence: ${state.confidence}'); // 0.0 - 1.0
});
Parent-Child Jobs
// Get parent jobs
final parents = seenn.jobs.parents;
// Get children of a parent
final children = seenn.jobs.childrenOf('parent_job_id');
// Stream child progress
tracker.onChildProgress.listen((update) {
print('${update.completed}/${update.total} complete');
print('${update.failed} failed');
});
Job Filtering
// By status
final active = seenn.jobs.active;
final completed = seenn.jobs.byStatus(JobStatus.completed);
// Reactive streams
seenn.jobs.active$.listen((jobs) {
print('Active jobs: ${jobs.length}');
});
Connection Management
// Check connection state
seenn.connection$.listen((state) {
print('Connected: ${state.isConnected}');
});
// Manual reconnect
await seenn.reconnect();
// Disconnect
await seenn.disconnect();
Rich Push Notifications (iOS)
Display images (avatars, thumbnails) in push notifications. Requires a Notification Service Extension.
Setup
-
Create extension in Xcode:
- Open
ios/Runner.xcworkspace - File → New → Target → Notification Service Extension
- Name:
NotificationServiceExtension - Language: Swift
- Open
-
Replace the generated code in
NotificationServiceExtension/NotificationService.swift:
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let bestAttemptContent = bestAttemptContent else {
contentHandler(request.content)
return
}
// Look for image URL in payload
let userInfo = request.content.userInfo
let imageUrlString = userInfo["senderAvatar"] as? String
?? userInfo["imageUrl"] as? String
?? userInfo["image"] as? String
guard let urlString = imageUrlString,
let imageUrl = URL(string: urlString) else {
contentHandler(bestAttemptContent)
return
}
// Download and attach image
downloadImage(from: imageUrl) { attachment in
if let attachment = attachment {
bestAttemptContent.attachments = [attachment]
}
contentHandler(bestAttemptContent)
}
}
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler,
let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
private func downloadImage(
from url: URL,
completion: @escaping (UNNotificationAttachment?) -> Void
) {
let task = URLSession.shared.downloadTask(with: url) { localUrl, response, error in
guard let localUrl = localUrl, error == nil else {
completion(nil)
return
}
let fileManager = FileManager.default
let tempDir = fileManager.temporaryDirectory
let fileName = url.lastPathComponent
let destUrl = tempDir.appendingPathComponent(fileName)
try? fileManager.removeItem(at: destUrl)
do {
try fileManager.moveItem(at: localUrl, to: destUrl)
let attachment = try UNNotificationAttachment(
identifier: "image",
url: destUrl,
options: nil
)
completion(attachment)
} catch {
completion(nil)
}
}
task.resume()
}
}
- Build and run your app
Payload Format
Include mutable-content: 1 and an image URL:
{
"aps": {
"alert": { "title": "Message", "body": "Hello!" },
"mutable-content": 1
},
"senderAvatar": "https://example.com/avatar.jpg"
}
Supported fields: senderAvatar, imageUrl, image
Documentation
Requirements
- Flutter >= 3.10.0
- Dart >= 3.0.0
- iOS 16.2+ (for Live Activity with push updates)
- Android API 21+ (for Ongoing Notification)
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.
License
MIT License - see LICENSE for details.
Libraries
- seenn_flutter
- Seenn Flutter SDK Real-time job progress tracking with Polling, Live Activity (iOS), and Ongoing Notification (Android) support.