admin管理员组

文章数量:1287914

are you smarter than AI? We'll apparently I'm not and I'm getting an error when I hit the SEND button. The error is flutter: Exception sending message: UnimplementedError.

This program made in Flutter is supposed to send a Notification to a topic called 'general'. And can send this notification from your own PC, no server needed. Also with topics no need to store any FCM device tokens. The receiving app must be subscribed to this topic which you can do programmatically.

We were building a general-purpose FCM sender, capable of sending messages from any environment, not just from within a Flutter app.

I had a similar program before but Google changed a lot so that was not working anymore. This program is made with the help of Gemini and with that we (Gemini & me) solved a lot of bugs. For the cryptographic part Gemini says it cannot help me further and it is exhausted. According to Gemini the cause and solution must lie in class ASN1Parser.

One thing I could try is to use another Crypto package in Flutter/Dart. But I thought this can be shared because up to now I cannot find a similar tool on the web except the Firebase console itself which is nice for trying but that's it.

You need to enter your own Firebase projectname in the main dart file, and download your own json file from Firebase and put it in folder assets to use this. Note that using the Firebase console I can send and receive the notification on my Android App. I'm writing and testing this application on Linux in Android Studio Lady Bug(latest), but in fact this should work on Win11, Linux or Apple.

Can you help solve the flutter: Exception sending message: UnimplementedError error? I'm not smart enough and have to less insight to solve this, but maybe you can and share the solution. So please don't ask difficult questions which I might not be able to answer.

This is the pubspec.yaml

name: fcm_sender_app
description: "A Firebase FCM Send Tool Project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
  sdk: ^3.6.2

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0 # Or the latest version
  cryptography: ^2.6.0 # Or the latest version

  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.0.0

# The following section is specific to Flutter packages.
flutter:
  assets:
    - assets/

  uses-material-design: true

This is the complete Main.dart file and more you should not need

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:cryptography/cryptography.dart';
//import 'package:cryptography/dart.dart';
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;

//import 'dart:typed_data';

BigInt _decodeBigInt(Uint8List bytes) {
  BigInt result = BigInt.from(0);
  for (int i = 0; i < bytes.length; i++) {
    result = result << 8;
    result += BigInt.from(bytes[i]);
  }
  return result;
}

List<int> _bigIntToBytes(BigInt bigInt) {
  final bytes = <int>[];
  while (bigInt > BigInt.zero) {
    bytes.add((bigInt & BigInt.from(0xFF)).toInt());
    bigInt >>= 8;
  }
  return bytes.reversed.toList();
}

String _base64UrlEncode(List<int> data) {
  final encoded = base64Url.encode(data);
  return encoded.replaceAll('=', '');
}

Future<String> _sign(String data, String privateKey) async {
  final signer = RsaPss(Sha256());
  final privateKeyObject = await _parsePrivateKey(privateKey);
  final signature = await signer.sign(
    Uint8List.fromList(data.codeUnits),
    keyPair: privateKeyObject,
  );
  return _base64UrlEncode(signature.bytes);
}
Future<SimpleKeyPair> _parsePrivateKey(String privateKey) async {
  final lines = privateKey.split('\n');
  final buffer = StringBuffer();
  for (var line in lines) {
    if (!line.startsWith('-----')) {
      buffer.write(line.trim());
    }
  }
  final key = buffer.toString();
  final decoded = base64.decode(key);
  if (kDebugMode) {print('decoded: $decoded');}
  if (kDebugMode) {print('privateKey: $privateKey');}
  final parser = ASN1Parser(decoded);
  final topLevelSeq = parser.nextObject() as ASN1Sequence;
  topLevelSeq.elements![0] as ASN1Integer;
  topLevelSeq.elements![1] as ASN1Sequence;
  final privateKeySeq = topLevelSeq.elements![2] as ASN1OctetString;
  final privateKeyParser = ASN1Parser(privateKeySeq.octets!);
  final privateKeySeq2 = privateKeyParser.nextObject() as ASN1Sequence;
  final modulus = privateKeySeq2.elements![1] as ASN1Integer;
  final privateExponent = privateKeySeq2.elements![3] as ASN1Integer;
  final rsaPrivateKey = SimpleKeyPairData(
    _bigIntToBytes(_decodeBigInt(privateExponent.octets!)),
    type: KeyPairType.rsa,
    publicKey: SimplePublicKey(
      _decodeBigInt(modulus.octets!).toUnsigned(_decodeBigInt(modulus.octets!).bitLength).toRadixString(16).padLeft((_decodeBigInt(modulus.octets!).bitLength / 4).ceil(), '0').codeUnits,
      type: KeyPairType.rsa,
    ),
  );
  return rsaPrivateKey;
}

class ASN1Parser {
  final Uint8List _bytes;
  int _index = 0;

  ASN1Parser(this._bytes);

  ASN1Object? nextObject() {
    if (_index >= _bytes.length) {
      return null;
    }
    final tag = _bytes[_index++];
    int length = _bytes[_index++];
    if (length > 0x80) {
      final numBytes = length - 0x80;
      length = 0;
      for (int i = 0; i < numBytes; i++) {
        length = (length << 8) + _bytes[_index++];
      }
    }
    final value = _bytes.sublist(_index, _index + length);
    _index += length;
    if (tag == 0x30) {
      return ASN1Sequence(value);
    } else if (tag == 0x02) {
      return ASN1Integer(value);
    } else if (tag == 0x04) {
      return ASN1OctetString(value);
    } else {
      return null;
    }
  }
}

abstract class ASN1Object {
  final Uint8List? octets;

  ASN1Object(this.octets);
}

class ASN1Sequence extends ASN1Object {
  List<ASN1Object>? elements;

  ASN1Sequence(Uint8List octets) : super(octets) {
    final parser = ASN1Parser(octets);
    elements = [];
    ASN1Object? obj = parser.nextObject();
    while (obj != null) {
      elements!.add(obj);
      obj = parser.nextObject();
    }
  }
}

class ASN1Integer extends ASN1Object {
  BigInt? valueAsBigInteger;

  ASN1Integer(super.octets) {
    valueAsBigInteger = _decodeBigInt(octets!);
  }
}

class ASN1OctetString extends ASN1Object {
  ASN1OctetString(super.octets);
}

String _mapToQueryString(Map<String, dynamic> map) {
  return map.entries.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}').join('&');
}

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FCM Sender',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FCMSenderScreen(),
    );
  }
}

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

  @override
  FCMSenderScreenState createState() => FCMSenderScreenState();
}

class FCMSenderScreenState extends State<FCMSenderScreen> {
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _bodyController = TextEditingController();
  final TextEditingController _topicController = TextEditingController();
  String _message = '';
  bool _sendToTopic = false;
  String _jsonPayload = ''; // New state variable

  Future<String> _getServiceAccountKey() async {
    return await rootBundle.loadString('assets/service_account_key.json');
  }

  Future<void> _sendFCMMessage() async {
    setState(() {
      _message = 'Sending message...';
    });

    //final String token = _tokenController.text; // Removed
    final String title = _titleController.text;
    final String body = _bodyController.text;
    final String topic = _topicController.text;
    //if ((token.isEmpty && !sendToTopic) || (topic.isEmpty && sendToTopic) || title.isEmpty || body.isEmpty) { // Changed
    if ((topic.isEmpty && _sendToTopic) || title.isEmpty || body.isEmpty) { // Changed
      setState(() {
        _message = 'Please fill in all fields.';
      });
      return;
    }

    try {
      final serviceAccountKey = await _getServiceAccountKey();
      final serviceAccount = jsonDecode(serviceAccountKey);
      final projectId = serviceAccount['yourfirebaseprojectname-12abc'];
      final accessToken = await _getAccessToken(serviceAccount);

      final url = Uri.parse('/$projectId/messages:send');

      final headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $accessToken',
      };

      Map<String, dynamic> payload = {
        'message': {
          'notification': {
            'title': title,
            'body': body,
          },
        },
      };

      if (_sendToTopic) { // Changed
        payload['message']['topic'] = topic;
      } else {
        //payload['message']['token'] = token; // Removed
      }

      // Update the state with the JSON payload
      setState(() {
        _jsonPayload = jsonEncode(payload);
      });

      final response = await http.post(url, headers: headers, body: jsonEncode(payload));
      if (kDebugMode) {print('JSON Payload: $payload');} // show json

      if (response.statusCode == 200) {
        setState(() {
          _message = 'Message sent successfully!';
        });
      } else {
        setState(() {
          _message = 'Failed to send message. Status code: ${response.statusCode}';
        });
        if (kDebugMode) {print('Error sending message: ${response.statusCode}');}
        if (kDebugMode) {print('Response body: ${response.body}');} // Print the response body
      }
    } catch (e) {
      setState(() {
        _message = 'Error sending message: $e';
      });
      if (kDebugMode) {print('Exception sending message: $e');} // Print the exception
    }
  }

  Future<String> _getAccessToken(Map<String, dynamic> serviceAccount) async {
    final url = Uri.parse('');
    final headers = {'Content-Type': 'application/x-www-form-urlencoded'};
    final payload = {
      'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      'assertion': await _createJwt(serviceAccount),
    };

    try {
      final response = await http.post(url, headers: headers, body: _mapToQueryString(payload));

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['access_token'];
      } else {
        if (kDebugMode) {print('Error getting access token: ${response.statusCode}');}
        if (kDebugMode) {print('Response body: ${response.body}');} // Print the response body
        throw Exception('Failed to get access token. Status code: ${response.statusCode}');
      }
    } catch (e) {
      if (kDebugMode) {print('Exception getting access token: $e');} // Print the exception
      throw Exception('Failed to get access token: $e');
    }
  }

  Future<String> _createJwt(Map<String, dynamic> serviceAccount) async {
    final header = jsonEncode({
      'alg': 'RS256',
      'typ': 'JWT',
    });
    final claimSet = jsonEncode({
      'iss': serviceAccount['client_email'],
      'scope': '.messaging',
      'aud': '',
      'exp': DateTime.now().add(const Duration(hours: 1)).millisecondsSinceEpoch ~/ 1000,
      'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
    });
    final encodedHeader = _base64UrlEncode(utf8.encode(header));
    final encodedClaimSet = _base64UrlEncode(utf8.encode(claimSet));
    final signature = await _sign('$encodedHeader.$encodedClaimSet', serviceAccount['private_key']);
    return '$encodedHeader.$encodedClaimSet.$signature';
  }

   @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FCM Sender'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _topicController,
              decoration: const InputDecoration(labelText: 'Topic'),
            ),
            TextField(
              controller: _titleController,
              decoration: const InputDecoration(labelText: 'Notification Title'),
            ),
            TextField(
              controller: _bodyController,
              decoration: const InputDecoration(labelText: 'Notification Body'),
            ),
            CheckboxListTile(
              title: const Text('Send to Topic'),
              value: _sendToTopic,
              onChanged: (bool? value) {
                setState(() {
                  _sendToTopic = value ?? false;
                });
              },
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _sendFCMMessage,
              child: const Text('Send FCM Message'),
            ),
            const SizedBox(height: 20),
            Text(_message),
            const SizedBox(height: 20),
            Text('JSON Payload: $_jsonPayload'), // Display the JSON here
          ],
        ),
      ),
    );
  }
}

本文标签: Request solve Firebase FCM Notification Send Tool written in FlutterDartStack Overflow