notiblast_sdk 0.8.0
notiblast_sdk: ^0.8.0 copied to clipboard
Flutter push-notification SDK for the NotiBlast backend: FCM device registration, tray display, inline reply, and chat-style conversation notifications.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:notiblast_sdk/notiblast_sdk.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// print() (not debugPrint) so SDK logs reach Console.app in Release builds.
// ignore: avoid_print
NotiBlast.setLogHandler((record) => print(record));
// Done before runApp, not behind a button: a reply to a closed app (iOS)
// launches it in the background without a UI, and the reply only reaches
// onReply once setReplyHandler() has run. Same rule as canReply in
// ios/NotificationService/NotificationService.swift.
await NotiBlast.initAppId('smoke-test-app-id');
await NotiBlast.Notifications.setReplyHandler(
onReply,
canReply: canReply,
actionTitle: 'Balas',
placeholder: 'Tulis pesan',
);
runApp(const MyApp());
}
/// Receives the text a user typed into a notification's reply field.
///
/// Must be a top-level function with this annotation. On Android it runs in
/// a background isolate, so it can't touch the page below — check the device
/// log (`flutter logs` / logcat) for the line it prints.
@pragma('vm:entry-point')
Future<void> onReply(NotiBlastReplyEvent event) async {
debugPrint(
'[NotiBlast example] reply: text="${event.text}" '
'data=${event.notification.data}',
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'NotiBlast SDK Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SmokeTestPage(),
);
}
}
/// Decides which notifications get the reply action: here, only group chats
/// (`group_key` starting with `groupChat-`).
///
/// Same rules as [onReply]: top-level, annotated, and it also runs in a
/// background isolate — so it only reads the notification.
@pragma('vm:entry-point')
bool canReply(NotiBlastNotification notification) =>
notification.groupId?.startsWith('groupChat-') ?? false;
/// Decides which notifications are shown as a conversation (Android): here,
/// only group chats (`group_key` starting with `groupChat-`); everything else
/// stays a regular notification.
///
/// Same rules as [onReply]. The sender fields fall back to the title when the
/// payload has no `sender_id` / `sender_name`, so the dev push script works
/// as is.
@pragma('vm:entry-point')
NotiBlastConversation? toConversation(NotiBlastNotification notification) {
final room = notification.groupId;
if (room == null || !room.startsWith('groupChat-')) return null;
final data = notification.data;
final senderName =
data['sender_name'] as String? ?? notification.title ?? 'Someone';
return NotiBlastConversation(
conversationId: room,
conversationTitle: data['room_name'] as String? ?? 'Group chat',
senderId: data['sender_id'] as String? ?? senderName,
senderName: senderName,
senderAvatarUrl: data['sender_avatar'] as String?,
// Consumer picks the conversation icon; the dev push script sends `icon`.
conversationAvatarUrl:
data['room_avatar'] as String? ?? data['icon'] as String?,
text: notification.body ?? '',
isGroup: true,
);
}
/// Manual smoke-test page for the NotiBlast SDK core flow — not a product
/// screen. Listeners are registered in [initState] (before any button is
/// pressed) and removed in [dispose]; buttons call the SDK in order
/// (initAppId -> requestPermission -> login -> logout / optOut / optIn) —
/// device registration and notifications both start at login(), which
/// registers the device if permission was granted in step 2. Results and
/// listener events are appended to the log below.
class SmokeTestPage extends StatefulWidget {
const SmokeTestPage({super.key});
@override
State<SmokeTestPage> createState() => _SmokeTestPageState();
}
class _SmokeTestPageState extends State<SmokeTestPage> {
final List<String> _log = [];
@override
void initState() {
super.initState();
NotiBlast.Notifications.addClickListener(_onClick);
NotiBlast.Notifications.addForegroundListener(_onForeground);
NotiBlast.Notifications.addRegistrationErrorListener(_onRegistrationError);
}
@override
void dispose() {
NotiBlast.Notifications.removeClickListener(_onClick);
NotiBlast.Notifications.removeForegroundListener(_onForeground);
NotiBlast.Notifications.removeRegistrationErrorListener(
_onRegistrationError,
);
super.dispose();
}
void _onClick(NotiBlastClickEvent event) {
final n = event.notification;
_appendLog(
'click: ${n.title} / group=${n.groupId} '
'/ image=${n.imageUrl} / clickAction=${n.clickAction} '
'/ additionalData=${n.additionalData} / data=${n.data}',
);
}
void _onForeground(NotiBlastNotification notification) {
_appendLog(
'foreground notification: ${notification.title} '
'/ group=${notification.groupId} / image=${notification.imageUrl}',
);
// The SDK no longer auto-shows foreground notifications — call this to
// display it in the tray.
notification.showNotification();
}
void _onRegistrationError(DeviceRegistrationException error) {
_appendLog('registration error: $error');
}
void _appendLog(String message) {
setState(() => _log.insert(0, message));
}
Future<void> _initAppId() async {
try {
await NotiBlast.initAppId('smoke-test-app-id');
_appendLog('initAppId: ok');
} catch (e) {
_appendLog('initAppId failed: $e');
}
}
// main() already runs initAppId() + setReplyHandler (groupChat- only).
// initAppId() clears the stored reply settings, so after pressing
// "1. initAppId" press one of these again (before "3. login").
Future<void> _setReplyHandlerAll() async {
try {
await NotiBlast.Notifications.setReplyHandler(onReply);
_appendLog('setReplyHandler (all notifications): ok');
} catch (e) {
_appendLog('setReplyHandler failed: $e');
}
}
Future<void> _setReplyHandlerGroupChatOnly() async {
try {
await NotiBlast.Notifications.setReplyHandler(
onReply,
canReply: canReply,
actionTitle: 'Balas',
placeholder: 'Tulis pesan',
);
_appendLog('setReplyHandler (groupChat- only): ok');
} catch (e) {
_appendLog('setReplyHandler failed: $e');
}
}
// Also cleared by initAppId(): press after "1. initAppId", before "3. login".
Future<void> _setConversationHandler() async {
try {
await NotiBlast.Notifications.setConversationHandler(
toConversation,
style: const NotiBlastConversationStyle(
maxMessages: 5,
selfName: 'Saya',
),
);
_appendLog('setConversationHandler (groupChat- only): ok');
} catch (e) {
_appendLog('setConversationHandler failed: $e');
}
}
// Same room as the group_key in scripts/send_push.dart.
Future<void> _clearConversation() async {
try {
await NotiBlast.Notifications.clearConversation(
'groupChat-691d86229cec5cdf3009ba21',
);
_appendLog('clearConversation: ok');
} catch (e) {
_appendLog('clearConversation failed: $e');
}
}
Future<void> _requestPermission() async {
try {
final granted = await NotiBlast.Notifications.requestPermission();
_appendLog('requestPermission: $granted');
} catch (e) {
_appendLog('requestPermission failed: $e');
}
}
Future<void> _login() async {
try {
await NotiBlast.login('smoke-test-user-id');
_appendLog('login: ok');
} catch (e) {
_appendLog('login failed: $e');
}
}
Future<void> _logout() async {
try {
await NotiBlast.logout();
_appendLog('logout: ok');
} catch (e) {
_appendLog('logout failed: $e');
}
}
Future<void> _optOut() async {
try {
await NotiBlast.Notifications.optOut();
_appendLog('optOut: ok');
} catch (e) {
_appendLog('optOut failed: $e');
}
}
Future<void> _optIn() async {
try {
await NotiBlast.Notifications.optIn();
_appendLog('optIn: ok');
} catch (e) {
_appendLog('optIn failed: $e');
}
}
Future<void> _subscribeTopic() async {
try {
await NotiBlast.Notifications.subscribeTopic('test-topic');
_appendLog('subscribeTopic("test-topic"): ok');
} catch (e) {
_appendLog('subscribeTopic failed: $e');
}
}
Future<void> _unsubscribeTopic() async {
try {
await NotiBlast.Notifications.unsubscribeTopic('test-topic');
_appendLog('unsubscribeTopic("test-topic"): ok');
} catch (e) {
_appendLog('unsubscribeTopic failed: $e');
}
}
/// Shows a grouped notification that disappears after 2 seconds (Android).
/// Same room as scripts/send_push.dart: `toConversation` would match it, but
/// a temporary notification is always plain, and the room's conversation
/// and summary should stay untouched when it goes away.
Future<void> _showTemporary() async {
try {
await const NotiBlastNotification(
title: 'Reply not sent',
body: 'Disappears in 2 seconds',
data: {'replyFailed': true},
groupId: 'groupChat-691d86229cec5cdf3009ba21',
).showNotification(timeoutAfter: const Duration(seconds: 2));
_appendLog('showNotification(timeoutAfter: 2s): ok');
} catch (e) {
_appendLog('showNotification(timeoutAfter) failed: $e');
}
}
Future<void> _clearAll() async {
try {
await NotiBlast.Notifications.clearAll();
_appendLog('clearAll: ok');
} catch (e) {
_appendLog('clearAll failed: $e');
}
}
Future<void> _removeGroupedNotifications() async {
try {
await NotiBlast.Notifications.removeGroupedNotifications('demo-group');
_appendLog('removeGroupedNotifications("demo-group"): ok');
} catch (e) {
_appendLog('removeGroupedNotifications failed: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('NotiBlast SDK Smoke Test')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(
onPressed: _initAppId,
child: const Text('1. initAppId'),
),
ElevatedButton(
onPressed: _requestPermission,
child: const Text('2. requestPermission'),
),
ElevatedButton(
onPressed: _login,
child: const Text('3. login'),
),
ElevatedButton(
onPressed: _setReplyHandlerAll,
child: const Text('reply: all'),
),
ElevatedButton(
onPressed: _setReplyHandlerGroupChatOnly,
child: const Text('reply: groupChat-'),
),
ElevatedButton(
onPressed: _setConversationHandler,
child: const Text('conversation: groupChat-'),
),
ElevatedButton(
onPressed: _clearConversation,
child: const Text('clearConversation'),
),
ElevatedButton(onPressed: _logout, child: const Text('logout')),
ElevatedButton(onPressed: _optOut, child: const Text('optOut')),
ElevatedButton(onPressed: _optIn, child: const Text('optIn')),
ElevatedButton(
onPressed: _subscribeTopic,
child: const Text('subscribeTopic'),
),
ElevatedButton(
onPressed: _unsubscribeTopic,
child: const Text('unsubscribeTopic'),
),
ElevatedButton(
onPressed: _showTemporary,
child: const Text('temporary (2s)'),
),
ElevatedButton(
onPressed: _clearAll,
child: const Text('clearAll'),
),
ElevatedButton(
onPressed: _removeGroupedNotifications,
child: const Text('removeGroupedNotifications'),
),
],
),
const Divider(height: 32),
Expanded(
child: ListView.builder(
itemCount: _log.length,
itemBuilder: (context, index) => Text(_log[index]),
),
),
],
),
),
);
}
}