firebase_notification_helper 0.0.3
firebase_notification_helper: ^0.0.3 copied to clipboard
A lightweight helper for Firebase Messaging, local notifications, and sending FCM push notifications via Legacy API. Best for internal tools and testing environments.
import 'package:flutter/material.dart';
import 'package:firebase_notification_helper/firebase_notification_helper.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FirebaseNotificationHelper.initialize(
config: const NotificationHelperConfig(
debug: true,
// Android notification channel
channelId: 'high_importance_channel',
channelName: 'High Importance Notifications',
channelDescription: 'Used for important notifications.',
// Firebase Cloud Function config
cloudFunctionName: 'sendNotification',
cloudFunctionRegion: 'us-central1',
requestPermissionOnInit: true,
enableForegroundLocalNotification: true,
handleInitialMessage: true,
emitInitialToken: true,
),
onNotificationTap: (payload) {
debugPrint('Notification tapped: $payload');
},
onTokenChanged: (token) {
debugPrint('FCM Token Changed: $token');
},
);
runApp(const FcmTokenSenderApp());
}
class FcmTokenSenderApp extends StatelessWidget {
const FcmTokenSenderApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FCM Sender Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFFFF7A00),
),
home: const FcmSenderHome(),
);
}
}
class FcmSenderHome extends StatefulWidget {
const FcmSenderHome({super.key});
@override
State<FcmSenderHome> createState() => _FcmSenderHomeState();
}
class _FcmSenderHomeState extends State<FcmSenderHome> {
final TextEditingController _tokenCtrl = TextEditingController();
final TextEditingController _topicCtrl = TextEditingController(
text: 'all_users',
);
final TextEditingController _titleCtrl = TextEditingController(
text: 'Test Notification',
);
final TextEditingController _bodyCtrl = TextEditingController(
text: 'This is a remote notification from firebase_notification_helper.',
);
final TextEditingController _screenCtrl = TextEditingController(
text: 'home',
);
bool _isSending = false;
bool _sendToTopic = false;
String? _myToken;
String? _result;
@override
void initState() {
super.initState();
_loadToken();
}
Future<void> _loadToken() async {
try {
final token = await FirebaseNotificationHelper.getToken();
if (!mounted) return;
setState(() {
_myToken = token;
_tokenCtrl.text = token ?? '';
});
debugPrint('My FCM Token: $token');
} catch (e) {
if (!mounted) return;
setState(() {
_result = 'Token fetch error: $e';
});
}
}
@override
void dispose() {
_tokenCtrl.dispose();
_topicCtrl.dispose();
_titleCtrl.dispose();
_bodyCtrl.dispose();
_screenCtrl.dispose();
super.dispose();
}
Future<void> _copyMyTokenToTarget() async {
final token = _myToken;
if (token == null || token.trim().isEmpty) {
setState(() {
_result = 'Token not found. Please refresh token.';
});
return;
}
setState(() {
_tokenCtrl.text = token;
_result = 'Own device token copied to target token field.';
});
}
Future<void> _subscribeToTopic() async {
final topic = _topicCtrl.text.trim();
if (topic.isEmpty) {
setState(() {
_result = 'Topic name is required.';
});
return;
}
try {
await FirebaseNotificationHelper.subscribeToTopic(topic);
if (!mounted) return;
setState(() {
_result = 'Subscribed to topic: $topic';
});
} catch (e) {
if (!mounted) return;
setState(() {
_result = 'Topic subscribe error: $e';
});
}
}
Future<void> _unsubscribeFromTopic() async {
final topic = _topicCtrl.text.trim();
if (topic.isEmpty) {
setState(() {
_result = 'Topic name is required.';
});
return;
}
try {
await FirebaseNotificationHelper.unsubscribeFromTopic(topic);
if (!mounted) return;
setState(() {
_result = 'Unsubscribed from topic: $topic';
});
} catch (e) {
if (!mounted) return;
setState(() {
_result = 'Topic unsubscribe error: $e';
});
}
}
Future<void> _showLocalNotification() async {
final title = _titleCtrl.text.trim().isEmpty
? 'Local Notification'
: _titleCtrl.text.trim();
final body = _bodyCtrl.text.trim().isEmpty
? 'This is a local test notification.'
: _bodyCtrl.text.trim();
await FirebaseNotificationHelper.showLocalNotification(
title: title,
body: body,
payload: {
'type': 'local',
'screen': _screenCtrl.text.trim(),
},
);
}
Future<void> _sendRemoteNotification() async {
final token = _tokenCtrl.text.trim();
final topic = _topicCtrl.text.trim();
final title = _titleCtrl.text.trim().isEmpty
? 'Notification'
: _titleCtrl.text.trim();
final body = _bodyCtrl.text.trim().isEmpty
? 'This is a remote notification.'
: _bodyCtrl.text.trim();
if (_sendToTopic) {
if (topic.isEmpty) {
setState(() {
_result = 'Topic name is required.';
});
return;
}
} else {
if (token.isEmpty) {
setState(() {
_result = 'Target FCM token is required.';
});
return;
}
}
setState(() {
_isSending = true;
_result = null;
});
try {
final result = await FirebaseNotificationHelper.sendRemoteNotification(
target: _sendToTopic ? FcmTarget.topic(topic) : FcmTarget.token(token),
title: title,
body: body,
data: {
'sender': 'firebase_notification_helper_example',
'screen': _screenCtrl.text.trim().isEmpty
? 'home'
: _screenCtrl.text.trim(),
'time': DateTime.now().toIso8601String(),
},
);
if (!mounted) return;
setState(() {
_result = result.rawBody;
});
} catch (e) {
if (!mounted) return;
setState(() {
_result = 'Error: $e';
});
} finally {
if (!mounted) return;
setState(() {
_isSending = false;
});
}
}
@override
Widget build(BuildContext context) {
final pad = MediaQuery.of(context).padding;
return Scaffold(
appBar: AppBar(
title: const Text('FCM Sender Example'),
actions: [
IconButton(
onPressed: _loadToken,
icon: const Icon(Icons.refresh),
tooltip: 'Refresh Token',
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _showLocalNotification,
icon: const Icon(Icons.notifications),
label: const Text('Local'),
),
body: Padding(
padding: EdgeInsets.fromLTRB(
16,
16 + pad.top,
16,
16 + pad.bottom,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SectionCard(
title: 'My FCM Token',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
_myToken?.isNotEmpty == true
? _myToken!
: 'Token loading...',
style: const TextStyle(fontSize: 12),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _copyMyTokenToTarget,
icon: const Icon(Icons.copy),
label: const Text('Use My Token As Target'),
),
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'Send Target',
child: Column(
children: [
SwitchListTile(
contentPadding: EdgeInsets.zero,
value: _sendToTopic,
title: const Text('Send to Topic'),
subtitle: Text(
_sendToTopic
? 'Notification will be sent to topic subscribers.'
: 'Notification will be sent to a single device token.',
),
onChanged: (value) {
setState(() {
_sendToTopic = value;
});
},
),
const SizedBox(height: 8),
if (_sendToTopic) ...[
TextField(
controller: _topicCtrl,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Topic Name',
hintText: 'all_users',
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _subscribeToTopic,
icon: const Icon(Icons.add_alert),
label: const Text('Subscribe'),
),
),
const SizedBox(width: 10),
Expanded(
child: OutlinedButton.icon(
onPressed: _unsubscribeFromTopic,
icon: const Icon(Icons.notifications_off),
label: const Text('Unsubscribe'),
),
),
],
),
] else ...[
TextField(
controller: _tokenCtrl,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Target Device FCM Token',
hintText: 'Paste target device FCM token here',
),
maxLines: 4,
),
],
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'Notification Content',
child: Column(
children: [
TextField(
controller: _titleCtrl,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Notification Title',
),
),
const SizedBox(height: 12),
TextField(
controller: _bodyCtrl,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Notification Body',
),
maxLines: 3,
),
const SizedBox(height: 12),
TextField(
controller: _screenCtrl,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Data: screen',
hintText: 'home / chat / order_details',
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _isSending ? null : _sendRemoteNotification,
icon: _isSending
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.send),
label: Text(
_isSending
? 'Sending...'
: _sendToTopic
? 'Send Remote Notification To Topic'
: 'Send Remote Notification To Token',
),
),
),
const SizedBox(height: 20),
_SectionCard(
title: 'Response',
child: Container(
constraints: const BoxConstraints(
minHeight: 80,
maxHeight: 260,
),
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: SingleChildScrollView(
child: SelectableText(
_result ?? 'No response yet.',
style: const TextStyle(fontSize: 12),
),
),
),
),
const SizedBox(height: 80),
],
),
),
),
);
}
}
class _SectionCard extends StatelessWidget {
const _SectionCard({
required this.title,
required this.child,
});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
color: Theme.of(context).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(
color: Theme.of(context).dividerColor.withOpacity(0.3),
),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 12),
child,
],
),
),
);
}
}