diff --git a/src/Firebase.Notifications/Android/Assets/DefaultIcon.png b/src/Firebase.Notifications/Android/Assets/DefaultIcon.png new file mode 100644 index 0000000..23e2332 Binary files /dev/null and b/src/Firebase.Notifications/Android/Assets/DefaultIcon.png differ diff --git a/src/Firebase.Notifications/Android/Impl.cpp.uxl b/src/Firebase.Notifications/Android/Impl.cpp.uxl new file mode 100644 index 0000000..0f3046e --- /dev/null +++ b/src/Firebase.Notifications/Android/Impl.cpp.uxl @@ -0,0 +1,47 @@ + + + + + + + + ]]> + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + diff --git a/src/Firebase.Notifications/Android/Impl.uno b/src/Firebase.Notifications/Android/Impl.uno new file mode 100644 index 0000000..a681f4d --- /dev/null +++ b/src/Firebase.Notifications/Android/Impl.uno @@ -0,0 +1,144 @@ +using Uno; +using Uno.Graphics; +using Uno.Collections; +using Fuse; +using Fuse.Controls; +using Fuse.Triggers; +using Fuse.Resources; + +using Fuse.Platform; +using Uno.Compiler.ExportTargetInterop; +using Uno.Compiler.ExportTargetInterop.Android; + +namespace Firebase.Notifications +{ + [ForeignInclude(Language.Java, + "java.io.IOException", + "android.app.Activity", + "android.content.Intent", + "android.os.AsyncTask", + "android.content.res.Resources", + "android.content.res.AssetManager", + "android.content.res.AssetFileDescriptor", + "android.os.Bundle", + "com.fuse.firebase.Notifications.PushNotificationReceiver", + "com.google.firebase.messaging.RemoteMessage")] + [Require("Gradle.Dependency.Compile", "com.google.firebase:firebase-messaging:12.0.1")] + extern(Android) + internal class AndroidImpl + { + public static event EventHandler RegistrationSucceeded; + public static event EventHandler RegistrationFailed; + public static event EventHandler> ReceivedNotification; + + static bool _init = false; + static List _cachedMessages = new List(); + + internal static void Init() + { + if (!_init) + { + JInit(); + _init = true; + Lifecycle.EnteringInteractive += OnEnteringInteractive; + Lifecycle.ExitedInteractive += OnExitedInteractive; + } + } + + [Foreign(Language.Java)] + static void JInit() + @{ + // Set up vars and hook into fuse intent listeners + com.fuse.Activity.subscribeToIntents( + new com.fuse.Activity.IntentListener() { + public void onIntent (Intent newIntent) { + String jsonStr = com.fuse.firebase.Notifications.PushNotificationReceiver.ToJsonString(newIntent); + if( jsonStr != "{}") + { + @{OnRecieve(string,bool):Call(jsonStr, false)}; + } + } + }, + "android.intent.action.MAIN"); + String id = com.google.firebase.iid.FirebaseInstanceId.getInstance().getToken(); + @{getRegistrationIdSuccess(string):Call(id)}; + @} + + + [Foreign(Language.Java), ForeignFixedName] + static void RegistrationIDUpdated(string regid) + @{ + @{getRegistrationIdSuccess(string):Call(regid)}; + @} + + static void getRegistrationIdSuccess(string regid) + { + var x = RegistrationSucceeded; + if (x!=null) + x(null, regid); + } + + static void getRegistrationIdError(string message) + { + var x = RegistrationFailed; + if (x!=null) + x(null, message); + } + + //---------------------------------------------------------------------- + + static void OnEnteringInteractive(ApplicationState newState) + { + NoteInteractivity(true); + var x = ReceivedNotification; + if (x!=null) + { + foreach (var message in _cachedMessages) + x(null, new KeyValuePair(message, true)); + } + _cachedMessages.Clear(); + } + + + static void OnExitedInteractive(ApplicationState newState) + { + NoteInteractivity(false); + } + + //---------------------------------------------------------------------- + + [Foreign(Language.Java), ForeignFixedName] + static void OnRecieve2(string message, bool fromNotificationBar) + @{ + @{OnRecieve(string,bool):Call(message, fromNotificationBar)}; + @} + + static void OnRecieve(string message, bool fromNotificationBar) + { + if (Lifecycle.State == ApplicationState.Interactive) + { + var x = ReceivedNotification; + if (x!=null) + x(null, new KeyValuePair(message, fromNotificationBar)); + } + else + { + _cachedMessages.Add(message); + } + } + + //---------------------------------------------------------------------- + + [Foreign(Language.Java)] + static void NoteInteractivity(bool isItInteractive) + @{ + PushNotificationReceiver.InForeground = isItInteractive; + java.util.ArrayList maps = PushNotificationReceiver._cachedBundles; + if (isItInteractive && maps!=null && maps.size()>0) { + for (RemoteMessage remoteMessage : maps) + @{OnRecieve(string,bool):Call(PushNotificationReceiver.ToJsonString(remoteMessage), true)}; + maps.clear(); + } + @} + } +} diff --git a/src/Firebase.Notifications/Android/PushNotificationIDService.java b/src/Firebase.Notifications/Android/PushNotificationIDService.java new file mode 100644 index 0000000..11363a4 --- /dev/null +++ b/src/Firebase.Notifications/Android/PushNotificationIDService.java @@ -0,0 +1,14 @@ +package com.fuse.firebase.Notifications; + +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.FirebaseInstanceIdService; + +public class PushNotificationIDService extends FirebaseInstanceIdService +{ + @Override + public void onTokenRefresh() + { + String refreshedToken = FirebaseInstanceId.getInstance().getToken(); + com.foreign.Firebase.Notifications.AndroidImpl.RegistrationIDUpdated(refreshedToken); + } +} diff --git a/src/Firebase.Notifications/Android/PushNotificationReceiver.java b/src/Firebase.Notifications/Android/PushNotificationReceiver.java new file mode 100644 index 0000000..6257fcb --- /dev/null +++ b/src/Firebase.Notifications/Android/PushNotificationReceiver.java @@ -0,0 +1,282 @@ +package com.fuse.firebase.Notifications; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.support.v4.app.NotificationCompat; +import android.content.res.AssetManager; +import android.media.RingtoneManager; +import android.app.Notification; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; +import org.json.JSONObject; +import org.json.JSONException; + +public class PushNotificationReceiver extends FirebaseMessagingService +{ + public static ArrayList _cachedBundles = new ArrayList(); + public static boolean InForeground = false; + public static String ACTION = "fuseFirebaseBackgroundNotify"; + public static String EXTRA_KEY = "FirebaseRemoteNotification"; + static int _notificationID = -1; + public static int nextID() { return _notificationID += 1; } + private static Object lock = new Object(); + private static final Set GOOGLE_KEYS = + new HashSet(Arrays.asList(new String[] {"google.collapse_key", + "google.from", + "google.message_id", + "google.message_type", + "google.sent_time", + "google.to", + "google.ttl", + "collapse_key"})); + + public static String ToJsonString(Intent newIntent) + { + Bundle map = newIntent.getExtras(); + JSONObject jmessage = new JSONObject(); + JSONObject data = new JSONObject(); + if (newIntent.getExtras() != null) { + for (String key : newIntent.getExtras().keySet()) + { + if (GOOGLE_KEYS.contains(key)) + { + try { jmessage.putOpt(key, map.getString(key)); } catch (Exception e) {} + } + else + { + try + { + data.put(key, new JSONObject(map.getString(key))); + } + catch (Exception e) + { + String val = map.getString(key); + try { data.put(key, val); } catch (JSONException e1) {} + } + } + } + if (data.length() > 0) + { + try { jmessage.putOpt("data", data); } catch (Exception e) {} + } + } + return jmessage.toString(); + } + + public static String ToJsonString(RemoteMessage message) + { + Map map = message.getData(); + JSONObject jmessage = new JSONObject(); + try { jmessage.putOpt("google.collapse_key", message.getCollapseKey()); } catch (Exception e) {} + try { jmessage.putOpt("google.from", message.getFrom()); } catch (Exception e) {} + try { jmessage.putOpt("google.message_id", message.getMessageId()); } catch (Exception e) {} + try { jmessage.putOpt("google.message_type", message.getMessageType()); } catch (Exception e) {} + try { jmessage.putOpt("google.sent_time", Long.toString(message.getSentTime())); } catch (Exception e) {} + try { jmessage.putOpt("google.to", message.getTo()); } catch (Exception e) {} + try { jmessage.putOpt("google.ttl", Long.toString(message.getTtl())); } catch (Exception e) {} + + // extract data + JSONObject data = new JSONObject(); + for (String key : map.keySet()) + { + try + { + data.put(key, new JSONObject(map.get(key))); + } + catch (Exception e) + { + String val = map.get(key); + try + { + data.put(key, val); + } + catch (JSONException e1) + { + e1.printStackTrace(); + } + } + } + if (map.keySet().size()>0) + { + try { + jmessage.put("data", data); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + // extract notification + RemoteMessage.Notification notif = message.getNotification(); + if (notif!=null) + { + JSONObject jnotif = new JSONObject(); + try { jnotif.putOpt("title", notif.getTitle()); } catch (Exception e) {} + try { jnotif.putOpt("body", notif.getBody()); } catch (Exception e) {} + try { jnotif.putOpt("icon", notif.getIcon()); } catch (Exception e) {} + try { jnotif.putOpt("sound", notif.getSound()); } catch (Exception e) {} + try { jnotif.putOpt("tag", notif.getTag()); } catch (Exception e) {} + try { jnotif.putOpt("color", notif.getColor()); } catch (Exception e) {} + try { jnotif.putOpt("click_action", notif.getClickAction()); } catch (Exception e) {} + try { jnotif.putOpt("body_loc_key", notif.getBodyLocalizationKey()); } catch (Exception e) {} + try { jnotif.putOpt("body_loc_args", notif.getBodyLocalizationArgs()); } catch (Exception e) {} + try { jnotif.putOpt("title_loc_key", notif.getTitleLocalizationKey()); } catch (Exception e) {} + try { jnotif.putOpt("title_loc_args", notif.getTitleLocalizationArgs()); } catch (Exception e) {} + try { + jmessage.put("notification", jnotif); + } catch (JSONException e) { + e.printStackTrace(); + } + } + return jmessage.toString(); + } + + static void cacheBundle(RemoteMessage message) + { + PushNotificationReceiver._cachedBundles.add(message); + } + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) + { + synchronized (lock) + { + DoIt(remoteMessage); + } + } + + // OnNotificationRecieved(this, remoteMessage.getFrom(), data); + // static void OnNotificationRecieved(Java.Object listener, String from, String data) + public void DoIt(RemoteMessage remoteMessage) + { + Map map = remoteMessage.getData(); + if (!PushNotificationReceiver.InForeground) + { + if (map.containsKey("title") || map.containsKey("body")) + { + SpitOutNotification(remoteMessage); + } + else + { + cacheBundle(remoteMessage); + } + } + else + { + com.foreign.Firebase.Notifications.AndroidImpl.OnRecieve2(ToJsonString(remoteMessage), false); + } + } + + void SpitOutNotification(RemoteMessage remoteMessage) + { + Context context = this; + int id = PushNotificationReceiver.nextID(); + Map map = remoteMessage.getData(); + String title = map.get("title"); + String body = map.get("body"); + String sound = map.get("sound"); + String icon = map.get("icon"); + + String jsonStr = ToJsonString(remoteMessage); + Intent intent = new Intent(context, @(Activity.Package).@(Activity.Name).class); + + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.setAction(PushNotificationReceiver.ACTION); + intent.putExtra(PushNotificationReceiver.EXTRA_KEY, jsonStr); + + android.app.PendingIntent pendingIntent = + android.app.PendingIntent.getActivity(context, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); + + String channel_name = "my_package_channel"; + String channel_id = "my_package_channel_1"; + String channel_description = "my_package_first_channel"; + + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationCompat.Builder notificationBuilder = null; + if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) + { + NotificationChannel mChannel = notificationManager.getNotificationChannel(channel_id); + if (mChannel == null) + { + mChannel = new NotificationChannel(channel_id, channel_name, importance); + notificationManager.createNotificationChannel(mChannel); + } + // Android > 8 support for Channels because it not supported on previous versions + notificationBuilder = new NotificationCompat.Builder(this, channel_id); + } + else{ + // Android < 7 Notification Builder init + notificationBuilder = new NotificationCompat.Builder(this); + } + + notificationBuilder = new NotificationCompat.Builder(context) + .setSmallIcon(@(Activity.Package).R.mipmap.notif) + .setContentTitle(title) + .setContentText(body) + .setAutoCancel(true) + .setContentIntent(pendingIntent); + + if (sound=="default") + { + android.net.Uri defaultSoundUri= RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + notificationBuilder.setSound(defaultSoundUri); + } + + if (icon != null && !icon.isEmpty()) + { + int iconResourceID = com.fuse.R.get(icon); + if (iconResourceID!=-1) + { + android.graphics.Bitmap bm = android.graphics.BitmapFactory.decodeResource(context.getResources(), iconResourceID); + notificationBuilder .setLargeIcon(bm); + } + else + { + String packageName = "@(Project.Name)"; + java.io.InputStream afs = com.fuse.firebase.BundleFiles.OpenBundledFile(context, packageName, icon); + if (afs!=null) + { + android.graphics.Bitmap bm = android.graphics.BitmapFactory.decodeStream(afs); + try + { + afs.close(); + } + catch (IOException e) + { + Log.d("@(Project.Name)", "Could close the notification icon '" + icon); + e.printStackTrace(); + return; + } + notificationBuilder .setLargeIcon(bm); + } + else + { + Log.d("@(Project.Name)", "Could not the load icon '" + icon + "' as either a bundled file or android resource"); + } + } + } + + Notification n = notificationBuilder .build(); + if (sound!="") + n.defaults |= Notification.DEFAULT_SOUND; + n.defaults |= Notification.DEFAULT_LIGHTS; + n.defaults |= Notification.DEFAULT_VIBRATE; + + notificationManager.notify(id, n); + } +} diff --git a/src/Firebase.Notifications/Common.uno b/src/Firebase.Notifications/Common.uno new file mode 100644 index 0000000..622e672 --- /dev/null +++ b/src/Firebase.Notifications/Common.uno @@ -0,0 +1,169 @@ +using Uno.Compiler.ExportTargetInterop; +using Uno; +using Uno.Graphics; +using Uno.Platform; +using Uno.Collections; +using Fuse; +using Fuse.Controls; +using Uno.Threading; + +namespace Firebase.Notifications +{ + [Require("Entity","Firebase.Core.Init()")] + [ForeignInclude(Language.Java, + "android.util.Log", + "com.google.firebase.iid.FirebaseInstanceId")] + [extern(iOS) Require("Source.Include", "Firebase/Firebase.h")] + public static class NotificationService + { + extern(Android) + static NotificationService() + { + Firebase.Core.Init(); + AndroidImpl.ReceivedNotification += OnReceived; + AndroidImpl.RegistrationFailed += OnRegistrationFailed; + AndroidImpl.RegistrationSucceeded += OnRegistrationSucceeded; + AndroidImpl.Init(); + } + + extern(iOS) + static NotificationService() + { + Firebase.Core.Init(); + iOSImpl.ReceivedNotification += OnReceived; + iOSImpl.NotificationRegistrationFailed += OnRegistrationFailed; + iOSImpl.NotificationRegistrationSucceeded += OnRegistrationSucceeded; + iOSImpl.Init(); + } + + public static void OnReceived(object sender, KeyValuePair notification) + { + var x = _receivedNotification; + if (x!=null){ + x(null, notification); + } + else + _pendingNotifications.Add(notification); + } + + public static void OnRegistrationFailed(object sender, string message) + { + var x = _registrationFailed; + if (x!=null) + x(null, message); + else + { + _pendingSuccess = null; + _pendingFailure = message; + } + } + + public static void OnRegistrationSucceeded(object sender, string message) + { + var x = _registrationSucceeded; + if (x!=null) + x(null, message); + else + { + _pendingFailure = null; + _pendingSuccess = message; + } + } + + static event EventHandler _registrationSucceeded; + static event EventHandler _registrationFailed; + static event EventHandler> _receivedNotification; + static string _pendingSuccess; + static string _pendingFailure; + static List> _pendingNotifications = new List>(); + + internal static event EventHandler> ReceivedNotification + { + add + { + _receivedNotification += value; + foreach (var n in _pendingNotifications) + { + debug_log "_pendingNotifications : "+n.Key+" fromNotificationBar:"+n.Value; + value(null, n); + } + _pendingNotifications.Clear(); + } + remove { + _receivedNotification -= value; + } + } + + // NOTE: We dont clean the _pendingSuccess or _pendingFailure fields + // As each consumer of PushNotifications will need to know these details. + + internal static event EventHandler RegistrationSucceeded + { + add + { + _registrationSucceeded += value; + if (_pendingSuccess!=null) + value(null, _pendingSuccess); + } + remove { + _registrationSucceeded -= value; + } + } + + internal static event EventHandler RegistrationFailed + { + add + { + _registrationFailed += value; + if (_pendingFailure!=null) + value(null, _pendingFailure); + } + remove { + _registrationFailed -= value; + } + } + + [Foreign(Language.ObjC)] + public extern(iOS) static void ClearBadgeNumber() + @{ + [UIApplication sharedApplication].applicationIconBadgeNumber = 0; + @} + + public extern(!iOS) static void ClearBadgeNumber() { } + + [Foreign(Language.ObjC)] + public extern(iOS) static void ClearAllNotifications() + @{ + [UIApplication sharedApplication].applicationIconBadgeNumber = 0; + @} + + [Foreign(Language.Java)] + public extern(Android) static void ClearAllNotifications() + @{ + android.app.Activity activity = com.fuse.Activity.getRootActivity(); + android.app.NotificationManager nMgr = (android.app.NotificationManager)activity.getSystemService(android.content.Context.NOTIFICATION_SERVICE); + nMgr.cancelAll(); + @} + + public extern(!iOS && !Android) static void ClearAllNotifications() { } + + [Foreign(Language.ObjC)] + public extern(iOS) static String GetFCMToken() + @{ + NSString *fcmToken = [[FIRInstanceID instanceID] token]; + return fcmToken; + @} + + [Foreign(Language.Java)] + public extern(Android) static String GetFCMToken() + @{ + String refreshedToken = FirebaseInstanceId.getInstance().getToken(); + Log.d("TOKEN", "Refreshed token: " + refreshedToken); + return refreshedToken; + @} + + public extern(!iOS && !Android) static String GetFCMToken() { return ""; } + + + } +} diff --git a/src/Firebase.Notifications/Firebase.Notifications.unoproj b/src/Firebase.Notifications/Firebase.Notifications.unoproj new file mode 100644 index 0000000..49f8aac --- /dev/null +++ b/src/Firebase.Notifications/Firebase.Notifications.unoproj @@ -0,0 +1,37 @@ +{ + "Copyright": "Copyright (c) Fusetools AS 2018", + "Description": "Support for Firebase Notifications On Mobile", + "Publisher": "Fusetools AS", + "Version": "1.0.0-rc2", + "Package": { + "ProjectUrl": "https://fusetools.com" + }, + "Packages": [ + "Uno.Collections", + "Uno.Threading", + "Fuse.Controls", + "Fuse.Elements", + "Fuse.Reactive", + "Fuse.Scripting", + "Fuse.Common", + "Fuse.Platform" + ], + "Projects": [ + "../Firebase/Firebase.unoproj" + ], + "Includes": [ + "Common.uno:Source", + "JS.uno:Source", + "Android/Impl.uno:Source", + "Android/Assets/DefaultIcon.png:File", + "Android/Impl.cpp.uxl:Extensions", + "Android/PushNotificationReceiver.java:Java:android", + "Android/PushNotificationIDService.java:Java:android", + "iOS/iOSFirebaseNotificationCallbacks.h:ObjCHeader:iOS", + "iOS/iOSFirebaseNotificationCallbacks.mm:ObjCSource:iOS", + "iOS/AppDelegateFirebaseNotify.h:ObjCHeader:iOS", + "iOS/AppDelegateFirebaseNotify.mm:ObjCSource:iOS", + "iOS/iOSImpl.uno:Source", + "iOS/Impl.uxl:Extensions" + ] +} diff --git a/src/Firebase.Notifications/JS.uno b/src/Firebase.Notifications/JS.uno new file mode 100644 index 0000000..0fbfe3c --- /dev/null +++ b/src/Firebase.Notifications/JS.uno @@ -0,0 +1,146 @@ +using Uno; +using Uno.UX; +using Uno.Platform; +using Uno.Collections; +using Uno.Compiler.ExportTargetInterop; +using Fuse; +using Fuse.Scripting; +using Fuse.Reactive; + +namespace Firebase.Notifications +{ + /** + @scriptmodule Firebase/Notifications + + Handles push notification from Firebase + + @include Docs/Guide.md + + ## Remarks + + This module is an @EventEmitter, so the methods from @EventEmitter can be used to listen to events. + + You need to add a reference to `Firebase.Notifications` in your project file to use this feature. + */ + [UXGlobalModule] + public sealed class NotificationModule : NativeEventEmitterModule + { + static readonly NotificationModule _instance; + extern(iOS) readonly iOSImpl _iOSImpl; + static NativeEvent _onRegistrationSucceedediOS; + + static NativeEvent onReceivedMessage; + static NativeEvent onRegistrationFailed; + static NativeEvent onRegistrationSucceeded; + + public NotificationModule() + : base(true, + "receivedMessage", + "registrationSucceeded") + { + if (_instance != null) return; + Resource.SetGlobalKey(_instance = this, "Firebase/Notifications"); + + if defined(iOS) + _iOSImpl = new iOSImpl(); + + onReceivedMessage = new NativeEvent("onReceivedMessage"); + onRegistrationFailed = new NativeEvent("onRegistrationFailed"); + onRegistrationSucceeded = new NativeEvent("onRegistrationSucceeded"); + + // Old-style events for backwards compatibility + On("receivedMessage", onReceivedMessage); + // Note: If we decide to remove these old-style events in the future, the + // "error" event will no longer have a listener by default, meaning that the + // module will then throw an exception on "error" (as per the way + // EventEmitter works), unlike the current behaviour. To retain the current + // behaviour we might then want to add a dummy listener to the "error" + // event. + On("error", onRegistrationFailed); + On("registrationSucceeded", onRegistrationSucceeded); + + AddMember(onReceivedMessage); + AddMember(onRegistrationSucceeded); + AddMember(onRegistrationFailed); + AddMember(new NativeFunction("clearBadgeNumber", ClearBadgeNumber)); + AddMember(new NativeFunction("clearAllNotifications", ClearAllNotifications)); + AddMember(new NativeFunction("getFCMToken", GetFCMToken)); + _onRegistrationSucceedediOS = new NativeEvent("onRegistrationSucceedediOS"); + AddMember(_onRegistrationSucceedediOS); + + Firebase.Notifications.NotificationService.ReceivedNotification += OnReceivedNotification; + Firebase.Notifications.NotificationService.RegistrationSucceeded += OnRegistrationSucceeded; + Firebase.Notifications.NotificationService.RegistrationFailed += OnRegistrationFailed; + } + + /** + @scriptevent receivedMessage + @param message The content of the notification as json + + Triggered when your app receives a notification. + */ + void OnReceivedNotification(object sender, KeyValuePairmessage) + { + Emit("receivedMessage", message.Key, message.Value); + } + + /** + @scriptevent registrationSucceeded + @param message The registration key from the backend + + Triggered when your app registers with the APNS or GCM backend. + */ + void OnRegistrationSucceeded(object sender, string message) + { + Emit("registrationSucceeded", message); + } + + static void OnRegistrationSucceedediOS(string message) { + _onRegistrationSucceedediOS.RaiseAsync(message); + } + + /** + @scriptevent error + @param message A backend specific reason for the failure. + + Called if your app fails to register with the backend. + */ + void OnRegistrationFailed(object sender, string message) + { + EmitError(message); + } + + /** + @scriptmethod clearBadgeNumber + + Clears the badge number shown on the iOS home screen. + + Has no effects on other platforms. + */ + public object ClearBadgeNumber(Context context, object[] args) + { + Firebase.Notifications.NotificationService.ClearBadgeNumber(); + return null; + } + + /** + @scriptmethod clearAllNotifications + + Cancels all previously shown notifications. + */ + public object ClearAllNotifications(Context context, object[] args) + { + Firebase.Notifications.NotificationService.ClearAllNotifications(); + return null; + } + + public object GetFCMToken(Context context, object[] args) + { + var token = Firebase.Notifications.NotificationService.GetFCMToken(); + if (token != null) { + Emit("registrationSucceeded", token); + } + return null; + } + } +} diff --git a/src/Firebase.Notifications/iOS/AppDelegateFirebaseNotify.h b/src/Firebase.Notifications/iOS/AppDelegateFirebaseNotify.h new file mode 100644 index 0000000..51497cc --- /dev/null +++ b/src/Firebase.Notifications/iOS/AppDelegateFirebaseNotify.h @@ -0,0 +1,16 @@ +#pragma once + +#ifdef __OBJC__ +#include +#include +#include + +@interface uContext (FirebaseNotify) +- (void)application:(UIApplication *)application initializeFirebaseNotifications:(NSDictionary *)launchOptions; +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error; +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo; +- (void)application:(UIApplication *)application dispatchPushNotification:(NSDictionary *)userInfo fromBar:(BOOL)fromBar; +@end + +#endif diff --git a/src/Firebase.Notifications/iOS/AppDelegateFirebaseNotify.mm b/src/Firebase.Notifications/iOS/AppDelegateFirebaseNotify.mm new file mode 100644 index 0000000..dfec23c --- /dev/null +++ b/src/Firebase.Notifications/iOS/AppDelegateFirebaseNotify.mm @@ -0,0 +1,36 @@ +#include +#include +#include "AppDelegateFirebaseNotify.h" +@{Fuse.Platform.Lifecycle:IncludeDirective} +@{Firebase.Notifications.iOSImpl:IncludeDirective} + +@implementation uContext (FirebaseNotify) + +- (void)application:(UIApplication *)application initializeFirebaseNotifications:(NSDictionary *)launchOptions { + if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { + [self application:application dispatchPushNotification:launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] fromBar:YES]; + } +#if (!@(Project.iOS.PushNotifications.RegisterOnLaunch:IsSet)) || @(Project.iOS.PushNotifications.RegisterOnLaunch:Or(0)) + @{Firebase.Notifications.iOSImpl.RegisterForPushNotifications():Call()}; +#endif +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { + uAutoReleasePool pool; + @{Fuse.Platform.ApplicationState} state = @{Fuse.Platform.Lifecycle.State:Get()}; + bool fromNotifBar = application.applicationState != UIApplicationStateActive; + [self application:application dispatchPushNotification:userInfo fromBar:fromNotifBar]; +} + +- (void)application:(UIApplication *)application dispatchPushNotification:(NSDictionary *)userInfo fromBar:(BOOL)fromBar { + uAutoReleasePool pool; + NSError* err = NULL; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:userInfo options:0 error:&err]; + if (jsonData) + { + NSString* nsJsonPayload = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + @{Firebase.Notifications.iOSImpl.OnReceivedNotification(string, bool):Call(nsJsonPayload, fromBar)}; + } +} + +@end diff --git a/src/Firebase.Notifications/iOS/Impl.uxl b/src/Firebase.Notifications/iOS/Impl.uxl new file mode 100644 index 0000000..876c504 --- /dev/null +++ b/src/Firebase.Notifications/iOS/Impl.uxl @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Firebase.Notifications/iOS/iOSFirebaseNotificationCallbacks.h b/src/Firebase.Notifications/iOS/iOSFirebaseNotificationCallbacks.h new file mode 100644 index 0000000..659ea5f --- /dev/null +++ b/src/Firebase.Notifications/iOS/iOSFirebaseNotificationCallbacks.h @@ -0,0 +1,5 @@ +#import +#include + +@interface FireNotificationCallbacks : NSObject +@end diff --git a/src/Firebase.Notifications/iOS/iOSFirebaseNotificationCallbacks.mm b/src/Firebase.Notifications/iOS/iOSFirebaseNotificationCallbacks.mm new file mode 100644 index 0000000..2523423 --- /dev/null +++ b/src/Firebase.Notifications/iOS/iOSFirebaseNotificationCallbacks.mm @@ -0,0 +1,17 @@ +#import +#include <@{Firebase.Notifications.NotificationModule:Include}> +#include <@{ObjC.Object:Include}> +#include +@{Firebase.Notifications.iOSImpl:IncludeDirective} + +@implementation FireNotificationCallbacks : NSObject + + +- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken { + @{Firebase.Notifications.iOSImpl.OnNotificationRegistrationSucceeded(string):Call(fcmToken)}; +} +- (void)messaging:(nonnull FIRMessaging *)messaging + didReceiveMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage{ +} + +@end diff --git a/src/Firebase.Notifications/iOS/iOSImpl.uno b/src/Firebase.Notifications/iOS/iOSImpl.uno new file mode 100644 index 0000000..fbfc771 --- /dev/null +++ b/src/Firebase.Notifications/iOS/iOSImpl.uno @@ -0,0 +1,150 @@ +using Uno; +using Uno.UX; +using Uno.Collections; +using Uno.Compiler.ExportTargetInterop; +using Fuse; +using Fuse.Triggers; +using Uno.Threading; +using Uno.Graphics; +using Fuse.Platform; + +namespace Firebase.Notifications +{ + [Require("Cocoapods.Podfile.Target", "pod 'Firebase/Messaging'")] + [extern(iOS) Require("Source.Include", "iOSFirebaseNotificationCallbacks.h")] + [extern(iOS) Require("Source.Include", "Firebase/Firebase.h")] + [Require("uContext.SourceFile.DidFinishLaunching", "[self application:[notification object] initializeFirebaseNotifications:[notification userInfo]];")] + [Require("uContext.SourceFile.Declaration", "#include ")] + + [Require("Entity", "Firebase.Notifications.iOSImpl.OnNotificationRegistrationSucceeded(string)")] + [Require("Entity", "Firebase.Notifications.iOSImpl.OnNotificationRegistrationFailed(string)")] + [Require("Entity", "Firebase.Notifications.iOSImpl.OnReceivedNotification(string,bool)")] + + extern(iOS) + public class iOSImpl + { + + public static event EventHandler> ReceivedNotification; + public static event EventHandler NotificationRegistrationFailed; + public static event EventHandler NotificationRegistrationSucceeded; + static List> DelayedNotifications = new List>(); + + static bool _init = false; + extern(ios) static internal ObjC.Object _iosDelegate; + + public static void Init() + { + if (!_init) + { + if defined(iOS) + { + iOSInit(); + _init = true; + } + } + } + + [Foreign(Language.ObjC)] + extern(iOS) + public static void iOSInit() + @{ + FireNotificationCallbacks* fireCB = [[FireNotificationCallbacks alloc] init]; + @{_iosDelegate:Set(fireCB)}; + [FIRMessaging messaging].delegate = (id)fireCB; + @} + + internal static void OnReceivedNotification(string notification, bool fromNotificationBar) + { + if (Lifecycle.State == ApplicationState.Foreground || + Lifecycle.State == ApplicationState.Interactive) + { + var handler = ReceivedNotification; + if (handler != null) + handler(null, new KeyValuePair(notification, fromNotificationBar)); + } + else + { + DelayedNotifications.Add(new KeyValuePair(notification, fromNotificationBar)); + Lifecycle.EnteringForeground += DispatchDelayedNotifications; + } + } + private static void DispatchDelayedNotifications(ApplicationState state) + { + var handler = ReceivedNotification; + if (handler != null) + foreach (var n in DelayedNotifications) + { + handler(null, n); + } + DelayedNotifications.Clear(); + Lifecycle.EnteringForeground -= DispatchDelayedNotifications; + } + + static string DelayedReason = ""; + internal static void OnNotificationRegistrationFailed(string reason) + { + if (Lifecycle.State == ApplicationState.Foreground || + Lifecycle.State == ApplicationState.Interactive) + { + EventHandler handler = NotificationRegistrationFailed; + if (handler != null) + handler(null, reason); + } + else + { + DelayedReason = reason; + Lifecycle.EnteringForeground += DispatchDelayedReason; + } + } + private static void DispatchDelayedReason(ApplicationState state) + { + EventHandler handler = NotificationRegistrationFailed; + if (handler != null) + handler(null, DelayedReason); + DelayedReason = ""; + Lifecycle.EnteringForeground -= DispatchDelayedReason; + } + + static string DelayedRegToken = ""; + internal static void OnNotificationRegistrationSucceeded(string regID) + { + if (Lifecycle.State == ApplicationState.Foreground || + Lifecycle.State == ApplicationState.Interactive) + { + EventHandler handler = NotificationRegistrationSucceeded; + if (handler != null) + handler(null, regID); + } + else + { + DelayedRegToken = regID; + Lifecycle.EnteringForeground += DispatchDelayedRegToken; + } + } + private static void DispatchDelayedRegToken(ApplicationState state) + { + EventHandler handler = NotificationRegistrationSucceeded; + if (handler != null) + handler(null, DelayedRegToken); + DelayedRegToken = ""; + Lifecycle.EnteringForeground -= DispatchDelayedRegToken; + } + + [Foreign(Language.ObjC)] + internal static void RegisterForPushNotifications() + @{ + UIApplication* application = [UIApplication sharedApplication]; + if ([application respondsToSelector:@selector(registerUserNotificationSettings:)]) { + // use registerUserNotificationSettings + [application registerUserNotificationSettings: [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]]; + [application registerForRemoteNotifications]; + } else { + // use registerForRemoteNotificationTypes: + [application registerForRemoteNotificationTypes: + UIRemoteNotificationTypeBadge | + UIRemoteNotificationTypeSound | + UIRemoteNotificationTypeAlert]; + } + @} + } +}