pulyaevskiy / firebase-admin-interop

Firebase Admin Interop Library for Dart
BSD 3-Clause "New" or "Revised" License
80 stars 37 forks source link

[Feature request] please add support for admin.messaging #50

Open CurrySenpai opened 4 years ago

Skquark commented 4 years ago

I second this feature request.. I've got my interop all built out in dart, and everything works up until I need to push a notification with FCM or PubSub, and that was the whole purpose for me. Without this ability, I need to abandon this package and rebuild the whole thing with the js version, which I really don't want. If I can help, I would since this is one of the last feature I'm missing before I can publish my app as usable. Refer to my previous issue here: https://github.com/pulyaevskiy/firebase-functions-interop/issues/51

pulyaevskiy commented 4 years ago

Of course, you're more than welcome to submit a PR for adding messaging support. I'm pretty limited on free time and there are several packages I'm maintaining so any help would be much appreciated.

It shouldn't be that hard to implement. Just need to follow similar patterns used in other modules (firestore, auth). In general, implementation consists of two pieces:

  1. Create JS-specific binding classes which simply define interfaces of types exposed by the Node.js SDK. For examples, see lib/src/bindings.dart, lib/src/firestor_bindings.dart and lib/src/storage)bindings.dart. Can use official documentation to get information about all messaging types. There are quite a few of them, but the good news is that we only need declarations of abstract classes and methods without any implementations.

One thing that I'd mention here to not forget to copy-paste official docs for all methods and classes into dartdocs in the code.

Here is how Auth service interface looks like in lib/src/bindings.dart. Notice it's an abstract class and all the methods and getters are declared external.

/// The Firebase Auth service interface.
@JS()
@anonymous
abstract class Auth {
  /// The app associated with this Auth service instance.
  external App get app;

  /// Creates a new Firebase custom token (JWT) that can be sent back to a client
  /// device to use to sign in with the client SDKs' signInWithCustomToken()
  /// methods.
  ///
  /// Returns a promise fulfilled with a custom token string for the provided uid
  /// and payload.
  external Promise createCustomToken(String uid, developerClaims);

  /// Creates a new user.
  ///
  /// Returns a promise fulfilled with [UserRecord] corresponding to the newly
  /// created user.
  external Promise createUser(CreateUserRequest properties);

  /// Deletes an existing user.
  ///
  /// Returns a promise containing `void`.
  external Promise deleteUser(String uid);

  // .. continues
}
  1. Create Dart-specific classes that wrap bindings from step (1) and map data from/to JS types (e.g. Dart Future to JS promise and vice versa). The goal here is to only expose Dart types in the public interface of this library so it's convenient to work with. For examples see lib/src/auth.dart, lib/src/firestore.dart and such. You can notice a pattern that these classes simply take a nativeInstance in their constructor and forward all methods calls to this object, of course, with some mapping of data types.

Here is again Auth class which is exposed by this library:

class Auth {
  Auth(this.nativeInstance);

  @protected
  final js.Auth nativeInstance;

  /// Creates a new Firebase custom token (JWT) that can be sent back to a client
  /// device to use to sign in with the client SDKs' signInWithCustomToken()
  /// methods.
  ///
  /// Returns a [Future] containing a custom token string for the provided [uid]
  /// and payload.
  Future<String> createCustomToken(String uid,
          [Map<String, String> developerClaims]) =>
      promiseToFuture(
          nativeInstance.createCustomToken(uid, jsify(developerClaims)));

  /// Creates a new user.
  Future<UserRecord> createUser(CreateUserRequest properties) =>
      promiseToFuture(nativeInstance.createUser(properties));

  // continues...
}

Notice usage of promiseToFuture and jsify methods.

Please ask any other questions if you need more help on understanding how this works.

Skquark commented 4 years ago

Alright, that was a fun learning experience.. Worked out what I could and think I got it fairly accurate but not tested and not sure about certain bits, so you're gonna have to verify. I wasn't sure which properties needed jsify() since I only saw it used for Map/Object and that wasn't in the methods. Also didn't know how to handle the parameters that took String | String[] to allow for an array. And not sure if I made the Message class correctly because it's listed as a type alias for TokenMessage | TopicMessage | ConditionMessage but is still used and not documented clearly.. The other ones I'd double check are sendToDevice registrationToken, subscribeToTopic & unsubscribeToTopic registrationTokens, and DataMessagePayload, Hopefully the rest I did right.

Here's what I added into bindings.dart:

// admin.messaging ================================================================

/// The Firebase Messaging service interface.
@JS()
@anonymous
abstract class Messaging {
  /// The app associated with this Messaging service instance.
  external App get app;

  /// Sends the given message via FCM.
  /// 
  /// Returns Promise<string> fulfilled with a unique message ID string after the 
  /// message has been successfully handed off to the FCM service for delivery
  external Promise send(Message message, [bool dryRun]);

  /// Sends all the messages in the given array via Firebase Cloud Messaging.
  /// 
  /// Returns Promise<BatchResponse> fulfilled with an object representing the result of the send operation.
  external Promise sendAll(List<Message> messages, [bool dryRun]);

  /// Sends the given multicast message to all the FCM registration tokens specified in it.
  /// 
  /// Returns Promise<BatchResponse> fulfilled with an object representing the result of the send operation.
  external Promise sendMulticast(MulticastMessage message, [bool dryRun]);

  /// Sends an FCM message to a condition.
  /// 
  /// Returns Promise<MessagingConditionResponse> fulfilled with the server's response after the message has been sent.
  external Promise sendToCondition(String condition, MessagingPayload payload, [MessagingOptions options]);

  /// Sends an FCM message to a single device corresponding to the provided registration token.
  /// 
  /// Returns Promise<MessagingDevicesResponse> fulfilled with the server's response after the message has been sent.
  external Promise sendToDevice(String registrationToken, MessagingPayload payload, [MessagingOptions options]);

  /// Sends an FCM message to a device group corresponding to the provided notification key.
  /// 
  /// Returns Promise<MessagingDevicesResponse> fulfilled with the server's response after the message has been sent.
  external Promise sendToDeviceGroup(String notificationKey, MessagingPayload payload, [MessagingOptions options]);

  /// Sends an FCM message to a topic.
  /// 
  /// Returns Promise<MessagingTopicResponse> fulfilled with the server's response after the message has been sent.
  external Promise sendToTopic(String topic, MessagingPayload payload, [MessagingOptions options]);

  /// Subscribes a device to an FCM topic.
  /// 
  /// Returns Promise<MessagingTopicManagementResponse> fulfilled with the server's response after the device has been subscribed to the topic.
  external Promise subscribeToTopic(String registrationTokens, String topic);

  /// Unsubscribes a device from an FCM topic.
  /// 
  /// Returns Promise<MessagingTopicManagementResponse> fulfilled with the server's response after the device has been subscribed to the topic.
  external Promise unsubscribeFromTopic(String registrationTokens, String topic);
}

@JS()
@anonymous
abstract class Message {
  external String get data;
  external Notification get notification;
  external String get token;
}

@JS()
@anonymous
abstract class TopicMessage {
  external AndroidConfig get android;
  external ApnsConfig get apns;
  external dynamic get data;
  external String get key;
  external FcmOptions get fcmOptions;
  external Notification get notification;
  /// Required
  external String get topic;
  external WebpushConfig get webpush;
}

@JS()
@anonymous
abstract class TokenMessage {
  external AndroidConfig get android;
  external ApnsConfig get apns;
  external dynamic get data;
  external String get key;
  external FcmOptions get fcmOptions;
  external Notification get notification;
  /// Required
  external String get token;
  external WebpushConfig get webpush;
}

@JS()
@anonymous
abstract class ConditionMessage {
  external AndroidConfig get android;
  external ApnsConfig get apns;
  /// Required
  external String get condition;
  external dynamic get data;
  external String get key;
  external FcmOptions get fcmOptions;
  external Notification get notification;
  external WebpushConfig get webpush;
}

@JS()
@anonymous
abstract class MulticastMessage {
  external AndroidConfig get android;
  external ApnsConfig get apns;
  external dynamic get data;
  external String get key;
  external FcmOptions get fcmOptions;
  external Notification get notification;
  /// Required
  external List<String> get tokens;
  external WebpushConfig get webpush;
}

/// A notification that can be included in admin.messaging.Message.
@JS()
@anonymous
abstract class Notification {
  /// The notification body
  external String get body;

  /// URL of an image to be displayed in the notification.
  external String get imageUrl;

  /// The title of the notification.
  external String get title;
}

/// Represents the WebPush-specific notification options that can be included in admin.messaging.WebpushConfig.
@JS()
@anonymous
abstract class WebpushNotification {
  /// An array of notification actions representing the actions available to the user when the notification is presented.
  external List<dynamic> get actions;

  /// URL of the image used to represent the notification when there is not enough space to display the notification itself.
  external String get badge;

  /// Body text of the notification.
  external String get body;

  /// Arbitrary data that you want associated with the notification. This can be of any data type.
  external dynamic get data;

  /// The direction in which to display the notification. Must be one of auto, ltr or rtl.
  external String get dir;

  /// URL to the notification icon.
  external String get icon;

  /// URL of an image to be displayed in the notification.
  external String get image;

  /// The notification's language as a BCP 47 language tag.
  external String get lang;

  /// A boolean specifying whether the user should be notified after a new notification replaces an old one. Defaults to false.
  external bool get renotify;

  /// Indicates that a notification should remain active until the user clicks or dismisses it, rather than closing automatically. Defaults to false.
  external bool get requireInteraction;

  /// A boolean specifying whether the notification should be silent. Defaults to false.
  external bool get silent;

  /// An identifying tag for the notification.
  external String get tag;

  /// Timestamp of the notification
  external num get timestamp;

  /// Title text of the notification.
  external String get title;

  /// A vibration pattern for the device's vibration hardware to emit when the notification fires.
  external num get vibrate;
}

/// Represents the WebPush protocol options that can be included in an admin.messaging.Message.
@JS()
@anonymous
abstract class WebpushConfig {
  /// A collection of data fields.
  external dynamic get data;

  /// Options for features provided by the FCM SDK for Web.
  external FcmOptions get fcmOptions;

  /// A collection of WebPush headers. Header values must be strings.
  external dynamic get headers;

  /// A WebPush notification payload to be included in the message.
  external WebpushNotification get notification;
}

/// Represents options for features provided by the FCM SDK for Web (which are not part of the Webpush standard).
@JS()
@anonymous
abstract class WebpushFcmOptions {
  /// The link to open when the user clicks on the notification. For all URL values, HTTPS is required.
  external String get link;
}

/// Options for features provided by the FCM SDK for Web.
@JS()
@anonymous
abstract class FcmOptions {
  /// The label associated with the message's analytics data.
  external String get analyticsLabel;
}

/// Interface representing a Firebase Cloud Messaging message payload. One or both of the data and notification keys are required.
@JS()
@anonymous
abstract class MessagingPayload {
  /// The data message payload.
  external DataMessagePayload get data;

  /// The notification message payload.
  external NotificationMessagePayload get notification;
}

/// Interface representing an FCM legacy API data message payload. Data messages let developers send up to 4KB of custom key-value pairs. The keys and values must both be strings.
@JS()
@anonymous
abstract class DataMessagePayload {
  /// Keys can be any custom string, except for the following reserved strings: 
  /// "from" and anything starting with "google."
  external String get key;

  external dynamic get value;
}

/// Interface representing an FCM legacy API notification message payload. Notification messages let developers send up to 4KB of predefined key-value pairs. 
@JS()
@anonymous
abstract class NotificationMessagePayload {
  /// An array of notification actions representing the actions available to the user when the notification is presented.
  external List<dynamic> get actions;

  /// URL of the image used to represent the notification when there is not enough space to display the notification itself.
  external String get badge;

  /// Body text of the notification.
  external String get body;

  /// Variable string values to be used in place of the format specifiers in body_loc_key to use to localize the body text to 
  /// the user's current localization.
  /// 
  /// The value should be a stringified JSON array.
  external String get bodyLocArgs;

  /// The key to the body string in the app's string resources to use to localize the body text to the user's current localization.
  external String get bodyLocKey;

  /// Action associated with a user click on the notification. If specified, an activity with a matching Intent Filter is 
  /// launched when a user clicks on the notification.
  external String get clickAction;

  /// The notification icon's color, expressed in #rrggbb format.
  external String get color;

  /// URL to the notification icon.
  external String get icon;

  /// Identifier used to replace existing notifications in the notification drawer.
  external String get sound;

  /// An identifying tag for the notification.
  external String get tag;

  /// The notification's title.
  external String get title;

  /// Variable string values to be used in place of the format specifiers in title_loc_key to use to localize the 
  /// title text to the user's current localization.
  /// The value should be a stringified JSON array.
  external String get titleLocArgs;

  /// The key to the title string in the app's string resources to use to localize the title text to the user's current localization.
  external String get titleLocKey;
}

/// Interface representing the options that can be provided when sending a message via the FCM legacy APIs.
@JS()
@anonymous
abstract class MessagingOptions {
  /// String identifying a group of messages (for example, "Updates Available") that can be collapsed, so that only the last message 
  /// gets sent when delivery can be resumed. This is used to avoid sending too many of the same messages when the device comes back online or becomes active.
  external String get collapseKey;

  /// On iOS, use this field to represent content-available in the APNs payload. When a notification or data message is sent and this is set to true, 
  /// an inactive client app is awoken. On Android, data messages wake the app by default. On Chrome, this flag is currently not supported.
  external bool get contentAvailable;

  /// Whether or not the message should actually be sent. When set to true, allows developers to test a request without actually sending a message. 
  /// When set to false, the message will be sent.
  external bool get dryRun;

  /// On iOS, use this field to represent mutable-content in the APNs payload. When a notification is sent and this is set to true, the content of the 
  /// notification can be modified before it is displayed, using a Notification Service app extension
  /// On Android and Web, this parameter will be ignored.
  external bool get mutableContent;

  /// The priority of the message. Valid values are "normal" and "high". On iOS, these correspond to APNs priorities 5 and 10.
  external String get priority;

  /// The package name of the application which the registration tokens must match in order to receive the message.
  external String get restrictedPackageName;

  /// How long (in seconds) the message should be kept in FCM storage if the device is offline. The maximum time to live supported is four weeks, and the default value is also four weeks. 
  external num get timeToLive;
}

/// Represents the Android-specific options that can be included in an admin.messaging.Message.
@JS()
@anonymous
abstract class AndroidConfig {
  /// Collapse key for the message. Collapse key serves as an identifier for a group of messages that can be collapsed, so that only 
  /// the last message gets sent when delivery can be resumed. A maximum of four different collapse keys may be active at any given time.
  external String get collapseKey;

  /// A collection of data fields to be included in the message. All values must be strings. When provided, overrides any data fields 
  /// set on the top-level admin.messaging.Message.
  external dynamic get data;

  /// Options for features provided by the FCM SDK for Android.
  external AndroidFcmOptions get fcmOptions;

  /// Android notification to be included in the message.
  external AndroidNotification get notification;

  /// Priority of the message. Must be either normal or high.
  external String get priority;

  /// Package name of the application where the registration tokens must match in order to receive the message.
  external String get restrictedPackageName;

  /// Time-to-live duration of the message in milliseconds.
  external num get ttl;
}

/// Represents options for features provided by the FCM SDK for Android.
@JS()
@anonymous
abstract class AndroidFcmOptions {
  /// The label associated with the message's analytics data.
  external String get analyticsLabel;
}

/// Represents the Android-specific notification options that can be included in admin.messaging.AndroidConfig.
@JS()
@anonymous
abstract class AndroidNotification {
  /// Body of the Android notification. When provided, overrides the body set via admin.messaging.Notification
  external String get body;

  /// An array of resource keys that will be used in place of the format specifiers in bodyLocKey.
  external List<String> get bodyLocArgs;

  /// The key to the body string in the app's string resources to use to localize the body text to the user's current localization.
  external String get bodyLocKey;

  /// The Android notification channel ID (new in Android O). 
  external String get channelId;

  /// Action associated with a user click on the notification. If specified, an activity with a matching Intent Filter is launched when a user clicks on the notification.
  external String get clickAction;

  /// Notification icon color in #rrggbb format.
  external String get color;

  /// Icon resource for the Android notification.
  external String get icon;

  /// URL of an image to be displayed in the notification.
  external String get imageUrl;

  /// File name of the sound to be played when the device receives the notification.
  external String get sound;

  /// Notification tag. This is an identifier used to replace existing notifications
  /// in the notification drawer. If not specified, each request creates a new notification.
  external String get tag;

  /// Title of the Android notification. When provided, overrides the title set via admin.messaging.Notification.
  external String get title;

  /// An array of resource keys that will be used in place of the format specifiers in titleLocKey.
  external List<String> get titleLocArgs;

  /// Key of the title string in the app's string resource to use to localize the title text.
  external String get titleLocKey;
}

/// Represents the APNs-specific options that can be included in an admin.messaging.Message. 
@JS()
@anonymous
abstract class ApnsConfig {
  /// Options for features provided by the FCM SDK for iOS.
  external ApnsFcmOptions get fcmOptions;

  /// A collection of APNs headers. Header values must be strings.
  external dynamic get headers;

  /// An APNs payload to be included in the message.
  external ApnsPayload get payload;
}

/// Represents options for features provided by the FCM SDK for iOS.
@JS()
@anonymous
abstract class ApnsFcmOptions {
  /// The label associated with the message's analytics data.
  external String get analyticsLabel;

  /// URL of an image to be displayed in the notification.
  external String get imageUrl;
}

/// Represents options for features provided by the FCM SDK for iOS.
@JS()
@anonymous
abstract class ApnsPayload {
  /// The aps dictionary to be included in the message.
  external Aps get aps;
}

/// Represents the aps dictionary that is part of APNs messages.
@JS()
@anonymous
abstract class Aps {
  /// Alert to be included in the message. This may be a string or an object of type admin.messaging.ApsAlert
  external String get alert;

  /// Badge to be displayed with the message. Set to 0 to remove the badge. When not specified, the badge will remain unchanged.
  external num get badge;

  /// Type of the notification.
  external String get category;

  /// Specifies whether to configure a background update notification.
  external bool get contentAvailable;

  /// Specifies whether to set the mutable-content property on the message so the clients can modify the notification via app extensions.
  external bool get mutableContent;

  /// Sound to be played with the message.
  external String get sound;

  /// An app-specific identifier for grouping notifications.
  external String get threadId;
}

@JS()
@anonymous
abstract class ApsAlert {
  external String get actionLocKey;
  external String get body;
  external String get launchImage;
  external List<String> get locArgs;
  external String get locKey;
  external String get subtitle;
  external List<String> get subtitleLocArgs;
  external String get subtitleLocKey;
  external String get title;
  external List<String> get titleLocArgs;
  external String get titleLocKey;
}

/// Represents a critical sound configuration that can be included in the aps dictionary of an APNs payload.
@JS()
@anonymous
abstract class CriticalSound {
  /// The critical alert flag. Set to true to enable the critical alert.
  external bool get critical;

  /// The name of a sound file in the app's main bundle or in the Library/Sounds folder of the app's container directory. 
  /// Specify the string "default" to play the system sound.
  external String get name;

  /// The volume for the critical alert's sound. Must be a value between 0.0 (silent) and 1.0 (full volume).
  external num get volume;

}
/// Interface representing the server response from the sendAll() and sendMulticast() methods.
@JS()
@anonymous
abstract class BatchResponse {
  /// The number of messages that resulted in errors when sending.
  external num get failureCount;

  /// An array of responses, each corresponding to a message.
  external List<SendResponse> get responses;

  /// The number of messages that were successfully handed off for sending.
  external num get successCount;
}

/// Interface representing the status of an individual message that was sent as part of a batch request.
@JS()
@anonymous
abstract class SendResponse {
  /// An error, if the message was not handed off to FCM successfully.
  external FirebaseError get error;

  /// A unique message ID string, if the message was handed off to FCM for delivery.
  external String get messageId;

  /// A boolean indicating if the message was successfully handed off to FCM or not. When true, the 
  /// messageId attribute is guaranteed to be set. When false, the error attribute is guaranteed to be set.
  external bool get success;
}

/// Interface representing the server response from the legacy sendToCondition() method.
@JS()
@anonymous
abstract class MessagingConditionResponse {
  /// The message ID for a successfully received request which FCM will attempt to deliver to all subscribed devices.
  external num get messageId;
}

/// Interface representing the server response from the sendToDeviceGroup() method.
@JS()
@anonymous
abstract class MessagingDeviceGroupResponse {
  /// An array of registration tokens that failed to receive the message.
  external List<String> get failedRegistrationTokens;

  /// The number of messages that could not be processed and resulted in an error.
  external num get failureCount;

  /// The number of messages that could not be processed and resulted in an error.
  external num get successCount;
}

/// Interface representing the status of a message sent to an individual device via the FCM legacy APIs.
@JS()
@anonymous
abstract class MessagingDeviceResult {
  /// The canonical registration token for the client app that the message was processed and sent to. 
  /// You should use this value as the registration token for future requests. Otherwise, 
  /// future messages might be rejected.
  external String get canonicalRegistrationToken;

  /// The error that occurred when processing the message for the recipient.
  external FirebaseError get error;

  /// A unique ID for the successfully processed message.
  external String get messageId;
}

/// Interface representing the server response from the legacy sendToDevice() method.
@JS()
@anonymous
abstract class MessagingDevicesResponse {
  /// The number of results that contain a canonical registration token. A canonical registration token 
  /// is the registration token corresponding to the last registration requested by the client app. 
  /// This is the token that you should use when sending future messages to the device.
  /// You can access the canonical registration tokens within the results property.
  external num get canonicalRegistrationTokenCount;

  /// The number of messages that could not be processed and resulted in an error.
  external num get failureCount;

  /// The unique ID number identifying this multicast message.
  external num get multicastId;

  /// An array of [MessagingDeviceResult] objects representing the status of the processed messages. 
  /// The objects are listed in the same order as in the request. That is, for each registration token 
  /// in the request, its result has the same index in this array. 
  /// If only a single registration token is provided, this array will contain a single object.
  external List<MessagingDeviceResult> get results;

  /// The number of messages that were successfully processed and sent.
  external num get successCount;
}

/// Interface representing the server response from the legacy sendToTopic() method.
@JS()
@anonymous
abstract class MessagingTopicResponse {
  /// The message ID for a successfully received request which FCM will attempt to deliver to all subscribed devices.
  external num get messageId;
}

/// Interface representing the server response from the subscribeToTopic() and 
/// admin.messaging.Messaging#unsubscribeFromTopic unsubscribeFromTopic() methods.
@JS()
@anonymous
abstract class MessagingTopicManagementResponse {
  /// An array of errors corresponding to the provided registration token(s). The length of this array will be equal to failureCount.
  external List<FirebaseArrayIndexError> get errors;

  /// The number of registration tokens that could not be subscribed to the topic and resulted in an error.
  external num get failureCount;

  /// The number of registration tokens that were successfully subscribed to the topic.
  external num get successCount;
}

On top, in class FirebaseAdmin, added this:

  /// Gets the [Messaging] service for the default app or a given [app].
  external Messaging messaging([App app]);

In abstract class App, added this:

  /// Gets the [Messaging] service for this app.
  external Messaging messaging(); 

Now here's what I got for the new messaging.dart file:

// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code
// is governed by a BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:meta/meta.dart';
import 'package:node_interop/util.dart';

import 'bindings.dart' as js show Messaging;
import 'bindings.dart'
    show
        Message,
        MulticastMessage,
        MessagingPayload,
        MessagingOptions,
        MessagingDevicesResponse,
        MessagingConditionResponse,
        MessagingDeviceGroupResponse,
        MessagingTopicResponse,
        MessagingTopicManagementResponse,
        BatchResponse;

export 'bindings.dart'
    show
        Message,
        TopicMessage,
        TokenMessage,
        ConditionMessage,
        MulticastMessage,
        Notification,
        WebpushNotification,
        WebpushFcmOptions,
        WebpushConfig,
        FcmOptions,
        MessagingPayload,
        DataMessagePayload,
        NotificationMessagePayload,
        MessagingOptions,
        MessagingDevicesResponse,
        MessagingDeviceResult,
        MessagingConditionResponse,
        MessagingDeviceGroupResponse,
        MessagingTopicResponse,
        MessagingTopicManagementResponse,
        AndroidConfig,
        AndroidFcmOptions,
        AndroidNotification,
        ApnsConfig,
        ApnsFcmOptions,
        ApnsPayload,
        Aps,
        ApsAlert,
        CriticalSound,
        BatchResponse,
        SendResponse;

class Messaging {
  Messaging(this.nativeInstance);

  @protected
  final js.Messaging nativeInstance;

  /// Sends the given [message] via FCM.
  /// 
  /// Returns Promise<string> fulfilled with a unique message ID string after the 
  /// message has been successfully handed off to the FCM service for delivery
  Future<String> send(Message message, [bool dryRun]) {
    if (dryRun != null)
      return promiseToFuture(nativeInstance.send(message, dryRun));
    else
      return promiseToFuture(nativeInstance.send(message));
  }

  /// Sends all the [messages] in the given array via Firebase Cloud Messaging.
  /// 
  /// Returns Promise<BatchResponse> fulfilled with an object representing the result of the send operation.
  Future<BatchResponse> sendAll(List<Message> messages, [bool dryRun]) {
    if (dryRun != null)
      return promiseToFuture(nativeInstance.sendAll(messages, dryRun));
    else
      return promiseToFuture(nativeInstance.sendAll(messages));
  }

  /// Sends the given multicast [message] to all the FCM registration tokens specified in it.
  /// 
  /// Returns Promise<BatchResponse> fulfilled with an object representing the result of the send operation.
  Future<BatchResponse> sendMulticast(MulticastMessage message, [bool dryRun]) {
    if (dryRun != null)
      return promiseToFuture(nativeInstance.sendMulticast(message, dryRun));
    else
      return promiseToFuture(nativeInstance.sendMulticast(message));
  }

  /// Sends an FCM message to a [condition].
  /// 
  /// Returns Promise<MessagingConditionResponse> fulfilled with the server's response after the message has been sent.
  Future<MessagingConditionResponse> sendToCondition(String condition, MessagingPayload payload, [MessagingOptions options]) {
    if (options != null)
      return promiseToFuture(nativeInstance.sendToCondition(condition, payload, options));
    else
      return promiseToFuture(nativeInstance.sendToCondition(condition, payload));
  }

  /// Sends an FCM message to a single device corresponding to the provided [registrationToken].
  /// 
  /// Returns Promise<MessagingDevicesResponse> fulfilled with the server's response after the message has been sent.
  Future<MessagingDevicesResponse> sendToDevice(String registrationToken, MessagingPayload payload, [MessagingOptions options]) {
    if (options != null)
      return promiseToFuture(nativeInstance.sendToDevice(registrationToken, payload, options));
    else
      return promiseToFuture(nativeInstance.sendToDevice(registrationToken, payload));
  }

  /// Sends an FCM message to a device group corresponding to the provided [notificationKey].
  /// 
  /// Returns Promise<MessagingDevicesResponse> fulfilled with the server's response after the message has been sent.
  Future<MessagingDeviceGroupResponse> sendToDeviceGroup(String notificationKey, MessagingPayload payload, [MessagingOptions options]) {
    if (options != null)
      return promiseToFuture(nativeInstance.sendToDeviceGroup(notificationKey, payload, options));
    else
      return promiseToFuture(nativeInstance.sendToDeviceGroup(notificationKey, payload));
  }

  /// Sends an FCM message to a [topic].
  /// 
  /// Returns Promise<MessagingTopicResponse> fulfilled with the server's response after the message has been sent.
  Future<MessagingTopicResponse> sendToTopic(String topic, MessagingPayload payload, [MessagingOptions options]) {
    if (options != null)
      return promiseToFuture(nativeInstance.sendToTopic(topic, payload, options));
    else
      return promiseToFuture(nativeInstance.sendToTopic(topic, payload));
  }

  /// Subscribes a device to an FCM [topic].
  /// 
  /// Returns Promise<MessagingTopicManagementResponse> fulfilled with the server's response after the device has been subscribed to the topic.
  Future<MessagingTopicManagementResponse> subscribeToTopic(String registrationTokens, String topic) =>
      promiseToFuture(nativeInstance.subscribeToTopic(registrationTokens, topic));

  /// Unsubscribes a device from an FCM [topic].
  /// 
  /// Returns Promise<MessagingTopicManagementResponse> fulfilled with the server's response after the device has been subscribed to the topic.
  Future<MessagingTopicManagementResponse> unsubscribeFromTopic(String registrationTokens, String topic) =>
      promiseToFuture(nativeInstance.unsubscribeFromTopic(registrationTokens, topic));
}

Then finally in firebase_admin_interop.dart added this: export 'src/messaging.dart';

Did the best I could to keep consistent with the pattern and adding the docs, should save you a lot of time getting it up and running for the next version of admin-interop, looking forward to putting it to use. Thanks...

Skquark commented 4 years ago

I tried to test it locally in my app, and discovered the class Message is already used in firebase_functions_interop so we're getting a conflict.. That's a tricky one to deal with (for me) since it's needed for send and sendAll functions. Should it be renamed to FcmMessage or something? I assume it can still be passed to the JS with a different name, but haven't tried yet. I also realized that I was missing the app access point, wasn't too hard to figure out. Made this addition to app.dart:

import 'messaging.dart';
...
  /// Gets [Messaging] client for this application.
  Messaging messaging() =>
      _messaging ??= new Messaging(nativeInstance.messaging());
  Messaging _messaging;

That's better.. I was also thinking some of those messaging classes could use a factory constructor like you have on some others, just wasn't sure which ones needed it. The more I played with it, I realized that all the exposed classes needed the external factory constructor or they are abstracts that can't be used. Still working on it, but here's what I changed so far in bindings.dart:

@JS()
@anonymous
abstract class FcmMessage {
  external String get data;
  external Notification get notification;
  external String get token;
  external factory FcmMessage({
    String data, 
    Notification notification, 
    String token,
  });
}

@JS()
@anonymous
abstract class TopicMessage {
  external AndroidConfig get android;
  external ApnsConfig get apns;
  external dynamic get data;
  external String get key;
  external FcmOptions get fcmOptions;
  external Notification get notification;
  /// Required
  external String get topic;
  external WebpushConfig get webpush;
  external factory TopicMessage({
    AndroidConfig android,
    ApnsConfig apns,
    dynamic data,
    String key,
    FcmOptions fcmOptions,
    Notification notification,
    String topic,
    WebpushConfig webpush,
  });
}

@JS()
@anonymous
abstract class TokenMessage {
  external AndroidConfig get android;
  external ApnsConfig get apns;
  external dynamic get data;
  external String get key;
  external FcmOptions get fcmOptions;
  external Notification get notification;
  /// Required
  external String get token;
  external WebpushConfig get webpush;
  external factory TokenMessage({
    AndroidConfig android,
    ApnsConfig apns,
    dynamic data,
    String key,
    FcmOptions fcmOptions,
    Notification notification,
    String token,
    WebpushConfig webpush,
  });
}

@JS()
@anonymous
abstract class ConditionMessage {
  external AndroidConfig get android;
  external ApnsConfig get apns;
  /// Required
  external String get condition;
  external dynamic get data;
  external String get key;
  external FcmOptions get fcmOptions;
  external Notification get notification;
  external WebpushConfig get webpush;
  external factory ConditionMessage({
    AndroidConfig android,
    ApnsConfig apns,
    String condition,
    dynamic data,
    String key,
    FcmOptions fcmOptions,
    Notification notification,
    WebpushConfig webpush,
  });
}

@JS()
@anonymous
abstract class MulticastMessage {
  external AndroidConfig get android;
  external ApnsConfig get apns;
  external dynamic get data;
  external String get key;
  external FcmOptions get fcmOptions;
  external Notification get notification;
  /// Required
  external List<String> get tokens;
  external WebpushConfig get webpush;
  external factory MulticastMessage({
    AndroidConfig android,
    ApnsConfig apns,
    dynamic data,
    String key,
    FcmOptions fcmOptions,
    Notification notification,
    List<String> tokens,
    WebpushConfig webpush,
  });
}

/// Interface representing a Firebase Cloud Messaging message payload. One or both of the data and notification keys are required.
@JS()
@anonymous
abstract class MessagingPayload {
  /// The data message payload.
  external DataMessagePayload get data;

  /// The notification message payload.
  external NotificationMessagePayload get notification;

  external factory MessagingPayload({
    DataMessagePayload data,
    NotificationMessagePayload notification,
  });
}

/// Interface representing an FCM legacy API data message payload. Data messages let developers send up to 4KB of custom key-value pairs. The keys and values must both be strings.
@JS()
@anonymous
abstract class DataMessagePayload {
  /// Keys can be any custom string, except for the following reserved strings: 
  /// "from" and anything starting with "google."
  external String get key;
  external dynamic get value;

  external factory DataMessagePayload({
    String key,
    dynamic value,
  });
}

I think I've got to do more of them, but that was enough for me to test further. I'm also thinking that the DataMessagePayload class is supposed to contain a Map<String, String> but the docs didn't tell me what to name it. Progress though.

CurrySenpai commented 4 years ago

@Skquark how's it going? :) P.D: thank you

Skquark commented 4 years ago

Well, I took what I got so far and put it in my Fork.. Check it out here https://github.com/Skquark/firebase-admin-interop I still haven't fully tested it, and there were still those questions about implementing correctly that I asked and still don't know for sure. I was also experiencing some breaking changes with functions-interop after some npm and flutter upgrades effecting the nodes, I had to change the versions of some dependencies in my functions pubspec.yaml to get it to build again.. So here's my firebase-functions-interop yaml changes:

  node_interop: ^1.0.3
  node_io: ^1.0.1+2
  node_http: ^1.0.0
  #firebase_admin_interop: ^1.2.2
  firebase_admin_interop:
    git:
      url: https://github.com/Skquark/firebase-admin-interop

If you can test it out and see if it miraculously works the way it is, that'd be good. Was still questioning whether any of the functions needed jsify, but I believe everything else is done right. Once confirmed, will make a Pull Request with the update. For some strange reason, I'm currently getting this when I try to deploy: Error: Could not read source directory. Remove links and shortcuts and try again. I'm sure it's unrelated, but trying to get that resolved.

SpencerRiddering commented 4 years ago

@Skquark, I did a quick test and your branch is working for me. I was able to send a plain (title, sub-title) push notification. Thanks for building this out.

CosmicPangolin commented 4 years ago

@pulyaevskiy I know this repo hasn't been a priority, but any chance of merging in @Skquark's messaging work? Looks like he hustled pretty hard to implement, and these interop packages are integral to my project as well given some architectural constraints (firestore triggers + dart models).

Also the 'Remove links and shortcuts and try again' error he mentions seems related to the symbolic link in build/node. Not familiar enough with the codegen to know if that can be fixed, but I have to delete that symlink to upload to Firestore.

Many thanks for the work to both of you! :)

Levi-Lesches commented 4 years ago

@pulyaevskiy Any updates on this?

Skquark commented 4 years ago

I guess an update has been long overdue since it's a pretty practical feature I can imaging a lot of people's projects would need. Just officially submitted a PR to make it easier, I think it's been tested well enough and all checks out. I also took the liberty of updating the other dependancy versions in pubspec that I found pending in someone else's PR, hope you don't mind. You'll need to update you're firebase_functions_interop pubspec versions as well to roll out the update.. Here's the example (also added to changelog) on how to use it in a dart functions interop project:

  NotificationMessagePayload notification = NotificationMessagePayload(
    title: title,
    body: body,
    clickAction: "FLUTTER_NOTIFICATION_CLICK",
  );
  MessagingPayload payload = new MessagingPayload(notification: notification, data: DataMessagePayload(data: {"doc" : event.reference.path}));
  MessagingDevicesResponse result = await firestoreApp.messaging().sendToDevice(token, payload);
  // or firestoreApp.messaging().sendToTopic(topic, payload);

Looks good, I got mine functional within regularly scheduled cron jobs to send messages appropriately, felt nice getting that working the way I needed. Cheers.

pulyaevskiy commented 4 years ago

This was released in 2.1.0. Thanks @Skquark for the implementation!

You'll need to update you're firebase_functions_interop pubspec versions as well to roll out the update.

Which versions exactly need to be updated? functions already depend on admin ^2.0.0 so should pick up latest after pub upgrade.

Levi-Lesches commented 4 years ago

@Skquark, the example you gave doesn't work for me. Right now, DataMessagePayload has this constructor:

DataMessagePayload({String key, dynamic value})

Which only allows a single value. The docs clearly make it sound like this was supposed to be a Map<String, dynamic>. Is that correct?

Thanks for all your work though! This is great timing for me, since I just finished using the Cloud FIrestore side of this library and am almost done porting my Python implementation to Dart. It's really nice to be able to keep all my project files in the same language.

Skquark commented 4 years ago

You're right, sorry, that was an object I was a bit confused on the implementation because of the way it was documented in Google's api doc at https://firebase.google.com/docs/reference/admin/node/admin.messaging.DataMessagePayload which didn't make the parameter clear to me. We should change that to take Map<String, dynamic> data and convert it to key/value string set. I also read in another discussion that the data payload value can only be a string, and they didn't update the docs, so I think we have to change it from dynamic. I'm still a little unsure how the Dart Map translates in the binding to node.js since Map isn't a js object.. In the doc, it describes the data as "map (key: string, value: string)" - "An object containing a list of "key": value pairs. Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }." so it almost seems like it needs us to make a simple custom abstract class with String key; & dynamic value, then message data would be a list of that object. The more I'm looking at it, it could work like this in bindings.dart:

abstract class MessagingPayload {
  /// The data message payload.
  external List<DataMessagePayload> get data;

  /// The notification message payload.
  external NotificationMessagePayload get notification;

  external factory MessagingPayload({
    List<DataMessagePayload> data,
    NotificationMessagePayload notification,
  });
}

/// Interface representing an FCM legacy API data message payload. Data messages let developers send up to 4KB of custom key-value pairs. The keys and values must both be strings.
@JS()
@anonymous
abstract class DataMessagePayload {
  /// Keys can be any custom string, except for the following reserved strings: 
  /// "from" and anything starting with "google."
  external String get key;
  external String get value;

  external factory DataMessagePayload({
    String key,
    String value,
  });
}

That might be the wrong way though, I saw a Typescript implementation that looks like this:

export interface DataMessagePayload {
  [key: string]: string;
}

But that syntax looks weird to me, don't know honestly, but it gives a clue. Hopefully you or someone else here has a better grasp than I.

Regarding the other dependency updates I mentioned, I was referring to the functions_interop pubspec since that is our entry point to use admin_interop. I made a quick fork of it to update the versions in yaml myself, plus added example sendMessage.. Got it here: https://github.com/Skquark/firebase-functions-interop I could PR that to you if liked, but maybe after we get it fixed. Almost, at least it currently sends message well if you leave out the data option..

Levi-Lesches commented 4 years ago

I'm still a little unsure how the Dart Map translates in the binding to node.js since Map isn't a js object.

I just helped fix another typed Map issue in this repo -- check out Auth.setCustomUserClaims. That's a pretty good example of using jsify to convert a typed Map to a JavaScript object (disclaimer: the jsify used there is actually a different function defined in the node_interop library that calls the original one from the js package).

From the Firebase docs for the Node.js admin SDK:

Data messages let developers send up to 4KB of custom key-value pairs. The keys and values must both be strings.

So it definitely sounds like we're talking about a Map<String, String>

Levi-Lesches commented 4 years ago

I know things are crazy right now given current events, hope you guys are okay. Any updates? If this needs to take a backseat for a while I don't mind picking it up, or helping in any way I can. Stay safe everyone.

pulyaevskiy commented 4 years ago

@Levi-Lesches sorry, not sure which issue are referring to?

Messaging was added in 2.1.

pulyaevskiy commented 4 years ago

@Levi-Lesches sorry, not sure which issue are referring to?

Messaging was added in 2.1.

pulyaevskiy commented 4 years ago

@Levi-Lesches sorry, not sure which issue are referring to?

Messaging was added in 2.1.

pulyaevskiy commented 4 years ago

@Levi-Lesches sorry, not sure which issue are referring to?

Messaging was added in 2.1.

Levi-Lesches commented 4 years ago

Right now, the DataMessagePayload accepted by MessagePayload which is what's passed into Messaging.sendToTopic only accepts one key and value pair. Additionally, DataMessagePayload.value is dynamic. Meanwhile, the documentation states:

Data messages let developers send up to 4KB of custom key-value pairs. The keys and values must both be strings.

Which means that

  1. More than 1 key-value pair can be passed
  2. The value type must be String, not dynamic

I think the most straightforward solution is to replace DataMessagePayload with a Map<String, String> and using jsify to convert between Dart and JavaScript.

EDIT: Additionally, FcmMessage currently declares its data property as a String while it too should be a Map<String, String>

Thanks again @Skquark for all your work, it takes care of a LOT that can be very difficult to implement.