firebase_fcm 0.0.2 copy "firebase_fcm: ^0.0.2" to clipboard
firebase_fcm: ^0.0.2 copied to clipboard

A Flutter package to construct and dispatch Firebase Cloud Messaging (FCM) v1 notifications from Dart using Service Account OAuth2 credentials.

example/lib/main.dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:firebase_fcm/firebase_fcm.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firebase FCM Sender',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.dark,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFFFF9100), // Firebase Accent Orange
          brightness: Brightness.dark,
        ),
        scaffoldBackgroundColor: const Color(0xFF121214),
        cardTheme: CardThemeData(
          color: const Color(0xFF1E1E22),
          elevation: 4,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
        ),
      ),
      home: const NotificationSenderScreen(),
    );
  }
}

class NotificationSenderScreen extends StatefulWidget {
  const NotificationSenderScreen({super.key});

  @override
  State<NotificationSenderScreen> createState() => _NotificationSenderScreenState();
}

class _NotificationSenderScreenState extends State<NotificationSenderScreen> {
  final _formKey = GlobalKey<FormState>();

  // Controllers
  final _serviceAccountController = TextEditingController();
  final _titleController = TextEditingController();
  final _bodyController = TextEditingController();
  final _tokenController = TextEditingController();
  final _topicController = TextEditingController();

  bool _sendToTopic = false;
  bool _isDebug = true;
  bool _isLoading = false;

  // Key-value pairs for custom data payload
  final List<MapEntry<TextEditingController, TextEditingController>> _customData = [];

  // Logs or Status message
  String _statusMessage = 'Ready to send';
  Color _statusColor = Colors.grey;

  @override
  void initState() {
    super.initState();
    // Default values for standard notification properties
    _titleController.text = 'New message received!';
    _bodyController.text = 'You have a message waiting in your inbox.';
    _topicController.text = 'F2chat';
    _tokenController.text = '';
    
    // Add default custom data key-values
    _addDataRow('type', 'chat');
    _addDataRow('chatType', 'seller');
  }

  void _addDataRow([String key = '', String val = '']) {
    setState(() {
      _customData.add(MapEntry(
        TextEditingController(text: key),
        TextEditingController(text: val),
      ));
    });
  }

  void _removeDataRow(int index) {
    setState(() {
      _customData[index].key.dispose();
      _customData[index].value.dispose();
      _customData.removeAt(index);
    });
  }

  void _loadSampleServiceAccount() {
    const sample = r'''{
  "type": "service_account",
  "project_id": "fir-notificaiton-5c0fd",
  "private_key_id": "123456789abcdef",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh...\n-----END PRIVATE KEY-----\n",
  "client_email": "firebase-adminsdk-xxxxx@fir-notificaiton-5c0fd.iam.gserviceaccount.com",
  "client_id": "12345678901234567890",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-xxxxx%40fir-notificaiton-5c0fd.iam.gserviceaccount.com"
}''';
    setState(() {
      _serviceAccountController.text = sample;
    });
  }

  Future<void> _sendNotification() async {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    if (_serviceAccountController.text.trim().isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Please enter service account JSON'),
          backgroundColor: Colors.redAccent,
        ),
      );
      return;
    }

    // Parse Service Account JSON
    Map<String, dynamic> serviceAccount;
    try {
      String rawJson = _serviceAccountController.text.trim();
      // Automatically sanitize and remove trailing commas before parsing
      rawJson = rawJson.replaceAllMapped(RegExp(r',\s*([}\]])'), (Match m) => m[1]!);
      serviceAccount = jsonDecode(rawJson);
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Invalid Service Account JSON format: $e'),
          backgroundColor: Colors.redAccent,
        ),
      );
      return;
    }

    // Build data map from user input rows
    final Map<String, String> dataMap = {};
    for (var entry in _customData) {
      final key = entry.key.text.trim();
      final val = entry.value.text.trim();
      if (key.isNotEmpty && val.isNotEmpty) {
        dataMap[key] = val;
      }
    }

    setState(() {
      _isLoading = true;
      _statusMessage = 'Sending notification... Check terminal debugPrint output.';
      _statusColor = Colors.amber;
    });

    try {
      await sendFcmNotification(
        title: _titleController.text.trim(),
        body: _bodyController.text.trim(),
        token: _sendToTopic ? null : _tokenController.text.trim(),
        topicPath: _sendToTopic ? _topicController.text.trim() : null,
        sendToTopic: _sendToTopic,
        serviceAccountJson: serviceAccount,
        data: dataMap.isNotEmpty ? dataMap : null,
        isDebug: _isDebug,
      );

      if (!mounted) return;

      setState(() {
        _statusMessage = 'Success! Notification request completed. Check IDE debug output.';
        _statusColor = Colors.greenAccent;
      });

      showDialog(
        context: context,
        builder: (ctx) => AlertDialog(
          title: const Row(
            children: [
              Icon(Icons.check_circle, color: Colors.greenAccent),
              SizedBox(width: 8),
              Text('Request Dispatched'),
            ],
          ),
          content: const Text(
            'The FCM send call was executed successfully. If debug mode is enabled, full response logs are printed in the terminal.',
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(ctx),
              child: const Text('OK'),
            ),
          ],
        ),
      );
    } catch (e) {
      if (!mounted) return;

      setState(() {
        _statusMessage = 'Error: $e';
        _statusColor = Colors.redAccent;
      });

      showDialog(
        context: context,
        builder: (ctx) => AlertDialog(
          title: const Row(
            children: [
              Icon(Icons.error_outline, color: Colors.redAccent),
              SizedBox(width: 8),
              Text('Execution Failed'),
            ],
          ),
          content: Text(
            'Failed to execute FCM request. Ensure your Service Account JSON credentials and network connection are correct.\n\nDetails:\n$e',
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(ctx),
              child: const Text('Dismiss'),
            ),
          ],
        ),
      );
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  @override
  void dispose() {
    _serviceAccountController.dispose();
    _titleController.dispose();
    _bodyController.dispose();
    _tokenController.dispose();
    _topicController.dispose();
    for (var entry in _customData) {
      entry.key.dispose();
      entry.value.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'Firebase FCM Portal',
          style: TextStyle(fontWeight: FontWeight.w800, letterSpacing: 0.5),
        ),
        elevation: 0,
        backgroundColor: Colors.transparent,
        flexibleSpace: Container(
          decoration: const BoxDecoration(
            gradient: LinearGradient(
              colors: [Color(0xFFE65100), Color(0xFFFF8F00)],
              begin: Alignment.topLeft,
              end: Alignment.bottomRight,
            ),
          ),
        ),
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
          child: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // Header Details
                const Text(
                  'FCM Send Console',
                  style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 4),
                const Text(
                  'Test FCM V1 notification push with OAuth2 authorization using a service account.',
                  style: TextStyle(color: Colors.grey, fontSize: 13),
                ),
                const SizedBox(height: 20),

                // CARD 1: Service Account Credentials
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            const Icon(Icons.vpn_key_rounded, color: Color(0xFFFF9100)),
                            const SizedBox(width: 8),
                            const Expanded(
                              child: Text(
                                '1. Service Account Credentials',
                                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                              ),
                            ),
                            TextButton.icon(
                              onPressed: _loadSampleServiceAccount,
                              icon: const Icon(Icons.content_paste_go, size: 16),
                              label: const Text('Load Sample', style: TextStyle(fontSize: 12)),
                            ),
                          ],
                        ),
                        const SizedBox(height: 8),
                        TextFormField(
                          controller: _serviceAccountController,
                          maxLines: 8,
                          style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
                          decoration: const InputDecoration(
                            hintText: 'Paste the contents of your Firebase Service Account JSON file here...',
                            alignLabelWithHint: true,
                          ),
                          validator: (value) {
                            if (value == null || value.trim().isEmpty) {
                              return 'Service account credentials JSON is required';
                            }
                            return null;
                          },
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 16),

                // CARD 2: Notification Payload
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Row(
                          children: [
                            Icon(Icons.notification_important_rounded, color: Color(0xFFFF9100)),
                            SizedBox(width: 8),
                            Expanded(
                              child: Text(
                                '2. Notification Payload',
                                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 16),
                        TextFormField(
                          controller: _titleController,
                          decoration: const InputDecoration(
                            labelText: 'Notification Title',
                            prefixIcon: Icon(Icons.title),
                          ),
                          validator: (value) =>
                              value == null || value.trim().isEmpty ? 'Title is required' : null,
                        ),
                        const SizedBox(height: 12),
                        TextFormField(
                          controller: _bodyController,
                          decoration: const InputDecoration(
                            labelText: 'Notification Body',
                            prefixIcon: Icon(Icons.notes),
                          ),
                          validator: (value) =>
                              value == null || value.trim().isEmpty ? 'Body is required' : null,
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 16),

                // CARD 3: Destination (Token vs Topic)
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Row(
                          children: [
                            Icon(Icons.send_rounded, color: Color(0xFFFF9100)),
                            SizedBox(width: 8),
                            Expanded(
                              child: Text(
                                '3. Destination Settings',
                                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 12),
                        SwitchListTile(
                          title: const Text('Send to Topic instead of Token'),
                          subtitle: Text(
                            _sendToTopic
                                ? 'Delivers to topic subscribers'
                                : 'Delivers to a specific device token',
                            style: const TextStyle(fontSize: 12),
                          ),
                          value: _sendToTopic,
                          onChanged: (val) {
                            setState(() {
                              _sendToTopic = val;
                            });
                          },
                        ),
                        const Divider(height: 20),
                        if (_sendToTopic)
                          TextFormField(
                            controller: _topicController,
                            decoration: const InputDecoration(
                              labelText: 'Topic Path',
                              hintText: 'e.g., F2chat',
                              prefixIcon: Icon(Icons.tag),
                            ),
                            validator: (value) => _sendToTopic && (value == null || value.trim().isEmpty)
                                ? 'Topic path is required'
                                : null,
                          )
                        else
                          TextFormField(
                            controller: _tokenController,
                            decoration: const InputDecoration(
                              labelText: 'Device Token',
                              hintText: 'Paste target device registration token',
                              prefixIcon: Icon(Icons.devices_other),
                            ),
                            validator: (value) => !_sendToTopic && (value == null || value.trim().isEmpty)
                                ? 'Device token is required'
                                : null,
                          ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 16),

                // CARD 4: Custom Data Payload
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            const Icon(Icons.data_object_rounded, color: Color(0xFFFF9100)),
                            const SizedBox(width: 8),
                            const Expanded(
                              child: Text(
                                '4. Custom Data (Optional)',
                                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                              ),
                            ),
                            IconButton(
                              icon: const Icon(Icons.add_circle_outline, color: Color(0xFFFF9100)),
                              tooltip: 'Add Field',
                              onPressed: () => _addDataRow(),
                            ),
                          ],
                        ),
                        const SizedBox(height: 8),
                        if (_customData.isEmpty)
                          const Padding(
                            padding: EdgeInsets.symmetric(vertical: 12.0),
                            child: Text(
                              'No custom data rows added. Default fallback data will be sent.',
                              style: TextStyle(
                                color: Colors.grey,
                                fontSize: 12,
                                fontStyle: FontStyle.italic,
                              ),
                            ),
                          )
                        else
                          ListView.builder(
                            shrinkWrap: true,
                            physics: const NeverScrollableScrollPhysics(),
                            itemCount: _customData.length,
                            itemBuilder: (context, index) {
                              return Padding(
                                padding: const EdgeInsets.only(bottom: 8.0),
                                child: Row(
                                  children: [
                                    Expanded(
                                      child: TextFormField(
                                        controller: _customData[index].key,
                                        decoration: const InputDecoration(
                                          labelText: 'Key',
                                          isDense: true,
                                          contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
                                        ),
                                      ),
                                    ),
                                    const SizedBox(width: 8),
                                    Expanded(
                                      child: TextFormField(
                                        controller: _customData[index].value,
                                        decoration: const InputDecoration(
                                          labelText: 'Value',
                                          isDense: true,
                                          contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
                                        ),
                                      ),
                                    ),
                                    IconButton(
                                      icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
                                      onPressed: () => _removeDataRow(index),
                                    ),
                                  ],
                                ),
                              );
                            },
                          ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 16),

                // CARD 5: Configuration & Debug Options
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Row(
                          children: [
                            Icon(Icons.settings_rounded, color: Color(0xFFFF9100)),
                            SizedBox(width: 8),
                            Expanded(
                              child: Text(
                                '5. Settings & Logs',
                                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 8),
                        SwitchListTile(
                          title: const Text('Verbose Debug Mode'),
                          subtitle: const Text('Logs full request details & credentials in the console.'),
                          value: _isDebug,
                          onChanged: (val) {
                            setState(() {
                              _isDebug = val;
                            });
                          },
                        ),
                        const Divider(),
                        const Text(
                          'Status Monitor:',
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
                        ),
                        const SizedBox(height: 4),
                        Container(
                          width: double.infinity,
                          padding: const EdgeInsets.all(12),
                          decoration: BoxDecoration(
                            color: const Color(0x4D000000), // 30% black
                            borderRadius: BorderRadius.circular(8),
                            border: Border.all(color: const Color(0x33FFFFFF)), // 20% white
                          ),
                          child: Text(
                            _statusMessage,
                            style: TextStyle(
                              color: _statusColor,
                              fontSize: 12,
                              fontFamily: 'monospace',
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 24),

                // Trigger Button
                SizedBox(
                  width: double.infinity,
                  height: 52,
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: const Color(0xFFFF8F00),
                      foregroundColor: Colors.white,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(14),
                      ),
                      elevation: 4,
                    ),
                    onPressed: _isLoading ? null : _sendNotification,
                    child: _isLoading
                        ? const CircularProgressIndicator(color: Colors.white)
                        : const Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              Icon(Icons.send_time_extension_rounded),
                              SizedBox(width: 8),
                              Text(
                                'Dispatch FCM Notification',
                                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                              ),
                            ],
                          ),
                  ),
                ),
                const SizedBox(height: 40),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
0
likes
150
points
--
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter package to construct and dispatch Firebase Cloud Messaging (FCM) v1 notifications from Dart using Service Account OAuth2 credentials.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

firebase_database, firebase_messaging, flutter, googleapis_auth, http

More

Packages that depend on firebase_fcm