From 5b4bd214668d356a8e4f8fdb9d6a89c172b0ee10 Mon Sep 17 00:00:00 2001 From: Libin Lu Date: Tue, 13 Sep 2016 16:47:03 -0400 Subject: [PATCH] local notification --- README.md | 120 +++++-- android/react-native-fcm.iml | 28 +- .../evollu/react/fcm/BundleJSONConverter.java | 203 ++++++++++++ .../react/fcm/FIRLocalMessagingHelper.java | 293 ++++++++++++++++++ .../react/fcm/FIRLocalMessagingPublisher.java | 15 + .../evollu/react/fcm/FIRMessagingModule.java | 52 +++- .../react/fcm/FIRSystemBootEventReceiver.java | 27 ++ index.js | 42 ++- ios/RNFIRMessaging.h | 1 + ios/RNFIRMesssaging.m | 186 +++++++++-- 10 files changed, 901 insertions(+), 66 deletions(-) create mode 100644 android/src/main/java/com/evollu/react/fcm/BundleJSONConverter.java create mode 100644 android/src/main/java/com/evollu/react/fcm/FIRLocalMessagingHelper.java create mode 100644 android/src/main/java/com/evollu/react/fcm/FIRLocalMessagingPublisher.java create mode 100644 android/src/main/java/com/evollu/react/fcm/FIRSystemBootEventReceiver.java diff --git a/README.md b/README.md index 55c5b17..8a4f5c7 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,40 @@ Edit `AppDelegate.m`: ### FCM config file In [firebase console](https://console.firebase.google.com/), you can get `google-services.json` file and place it in `android/app` directory and get `GoogleService-Info.plist` file and place it in `/ios/your-project-name` directory (next to your `Info.plist`) + +### Setup Local Notifications + +#### IOS + +Edit Appdelegate.m +```diff ++ -(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification ++ { ++ [[NSNotificationCenter defaultCenter] postNotificationName:FCMLocalNotificationReceived object:self userInfo:notification.userInfo]; ++ } +``` + +#### Android +Edit AndroidManifest.xml +```diff + ++ ++ + + + + + + + + + + + + + + + + + + + +``` +NOTE: `com.evollu.react.fcm.FIRLocalMessagingPublisher` is required for presenting local notifications. `com.evollu.react.fcm.FIRSystemBootEventReceiver` is required only if you need to schedule future or recurring local notifications + ## Usage @@ -157,29 +191,68 @@ In [firebase console](https://console.firebase.google.com/), you can get `google import FCM from 'react-native-fcm'; class App extends Component { - componentDidMount() { - FCM.requestPermissions(); // for iOS - FCM.getFCMToken().then(token => { - console.log(token) - // store fcm token in your server - }); - this.notificationUnsubscribe = FCM.on('notification', (notif) => { - // there are two parts of notif. notif.notification contains the notification payload, notif.data contains data payload - }); - this.refreshUnsubscribe = FCM.on('refreshToken', (token) => { - console.log(token) - // fcm token may not be available on first load, catch it here - }); - - FCM.subscribeToTopic('/topics/foo-bar'); - FCM.unsubscribeFromTopic('/topics/foo-bar'); - } + componentDidMount() { + FCM.requestPermissions(); // for iOS + FCM.getFCMToken().then(token => { + console.log(token) + // store fcm token in your server + }); + this.notificationUnsubscribe = FCM.on('notification', (notif) => { + // there are two parts of notif. notif.notification contains the notification payload, notif.data contains data payload + }); + this.localNotificationUnsubscribe = FCM.on('localNotification', (notif) => { + // notif.notification contains the data + }); + this.refreshUnsubscribe = FCM.on('refreshToken', (token) => { + console.log(token) + // fcm token may not be available on first load, catch it here + }); + } - componentWillUnmount() { - // prevent leaking - this.refreshUnsubscribe(); - this.notificationUnsubscribe(); - } + componentWillUnmount() { + // prevent leaking + this.refreshUnsubscribe(); + this.notificationUnsubscribe(); + this.localNotificationUnsubscribe(); + } + + otherMethods(){ + FCM.subscribeToTopic('/topics/foo-bar'); + FCM.unsubscribeFromTopic('/topics/foo-bar'); + FCM.getInitialNotification().then(...); + FCM.presentLocalNotification({ + id: "UNIQ_ID_STRING", // (optional for instant notification) + title: "My Notification Title", // as FCM payload + body: "My Notification Message", // as FCM payload (required) + sound: "default", // as FCM payload + priority: "high", // as FCM payload + badge: 10 // as FCM payload IOS only, set 0 to clear badges + number: 10 // Android only + ticker: "My Notification Ticker", // Android only + auto_cancel: true, // Android only (default true) + largeIcon: "ic_launcher", // Android only + icon: "ic_notification", // as FCM payload + big_text: "Show when notification is expanded", // Android only + sub_text: "This is a subText", // Android only + color: "red", // Android only + vibrate: 300, // Android only default: 300, no vibration is you pass null + tag: 'some_tag', // Android only + group: "group", // Android only + my_custom_data:'my_custom_field_value', // extra data you want to throw + }); + + FCM.scheduleLocalNotification({ + fire_date: new Date(), + id: "UNIQ_ID_STRING" //REQUIRED! this is what you use to lookup and delete notification + body: "from future past" + }) + + FCM.getScheduledLocalNotifications().then(...); + FCM.cancelLocalNotification("UNIQ_ID_STRING"); + FCM.cancelAllLocalNotifications(); + FCM.setBadgeNumber(); + FCM.getBadgeNumber(); + } } ``` @@ -271,3 +344,6 @@ All available features are [here](https://firebase.google.com/docs/cloud-messagi #### Some features are missing Issues and pull requests are welcome. Let's make this thing better! + +#### Thanks +Local notification implementation is inspired by react-native-push-notification by zo0r and Neson diff --git a/android/react-native-fcm.iml b/android/react-native-fcm.iml index 1281143..17bc181 100644 --- a/android/react-native-fcm.iml +++ b/android/react-native-fcm.iml @@ -102,32 +102,34 @@ + + - - - + - + - - + + + + + - + - - - + + + + + - - - \ No newline at end of file diff --git a/android/src/main/java/com/evollu/react/fcm/BundleJSONConverter.java b/android/src/main/java/com/evollu/react/fcm/BundleJSONConverter.java new file mode 100644 index 0000000..8dbb70a --- /dev/null +++ b/android/src/main/java/com/evollu/react/fcm/BundleJSONConverter.java @@ -0,0 +1,203 @@ +//steal from https://github.com/facebook/facebook-android-sdk/blob/master/facebook/src/main/java/com/facebook/internal/BundleJSONConverter.java + +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, + * copy, modify, and distribute this software in source code or binary form for use + * in connection with the web services and APIs provided by Facebook. + * + * As with any software that integrates with the Facebook platform, your use of + * this software is subject to the Facebook Developer Principles and Policies + * [http://developers.facebook.com/policy/]. This copyright notice shall be + * included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.evollu.react.fcm; + + import android.os.Bundle; + import org.json.JSONArray; + import org.json.JSONException; + import org.json.JSONObject; + + import java.util.*; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for + * Android. Use of any of the classes in this package is unsupported, and they may be modified or + * removed without warning at any time. + * + * A helper class that can round trip between JSON and Bundle objects that contains the types: + * Boolean, Integer, Long, Double, String + * If other types are found, an IllegalArgumentException is thrown. + */ +public class BundleJSONConverter { + private static final Map, Setter> SETTERS = new HashMap, Setter>(); + + static { + SETTERS.put(Boolean.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putBoolean(key, (Boolean) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(Integer.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putInt(key, (Integer) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(Long.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putLong(key, (Long) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(Double.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putDouble(key, (Double) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(String.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putString(key, (String) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(String[].class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + throw new IllegalArgumentException("Unexpected type from JSON"); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + JSONArray jsonArray = new JSONArray(); + for (String stringValue : (String[])value) { + jsonArray.put(stringValue); + } + json.put(key, jsonArray); + } + }); + + SETTERS.put(JSONArray.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + JSONArray jsonArray = (JSONArray)value; + ArrayList stringArrayList = new ArrayList(); + // Empty list, can't even figure out the type, assume an ArrayList + if (jsonArray.length() == 0) { + bundle.putStringArrayList(key, stringArrayList); + return; + } + + // Only strings are supported for now + for (int i = 0; i < jsonArray.length(); i++) { + Object current = jsonArray.get(i); + if (current instanceof String) { + stringArrayList.add((String)current); + } else { + throw new IllegalArgumentException("Unexpected type in an array: " + current.getClass()); + } + } + bundle.putStringArrayList(key, stringArrayList); + } + + @Override + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + throw new IllegalArgumentException("JSONArray's are not supported in bundles."); + } + }); + } + + public interface Setter { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException; + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException; + } + + public static JSONObject convertToJSON(Bundle bundle) throws JSONException { + JSONObject json = new JSONObject(); + + for(String key : bundle.keySet()) { + Object value = bundle.get(key); + if (value == null) { + // Null is not supported. + continue; + } + + // Special case List as getClass would not work, since List is an interface + if (value instanceof List) { + JSONArray jsonArray = new JSONArray(); + @SuppressWarnings("unchecked") + List listValue = (List)value; + for (String stringValue : listValue) { + jsonArray.put(stringValue); + } + json.put(key, jsonArray); + continue; + } + + // Special case Bundle as it's one way, on the return it will be JSONObject + if (value instanceof Bundle) { + json.put(key, convertToJSON((Bundle)value)); + continue; + } + + Setter setter = SETTERS.get(value.getClass()); + if (setter == null) { + throw new IllegalArgumentException("Unsupported type: " + value.getClass()); + } + setter.setOnJSON(json, key, value); + } + + return json; + } + + public static Bundle convertToBundle(JSONObject jsonObject) throws JSONException { + Bundle bundle = new Bundle(); + @SuppressWarnings("unchecked") + Iterator jsonIterator = jsonObject.keys(); + while (jsonIterator.hasNext()) { + String key = jsonIterator.next(); + Object value = jsonObject.get(key); + if (value == null || value == JSONObject.NULL) { + // Null is not supported. + continue; + } + + // Special case JSONObject as it's one way, on the return it would be Bundle. + if (value instanceof JSONObject) { + bundle.putBundle(key, convertToBundle((JSONObject)value)); + continue; + } + + Setter setter = SETTERS.get(value.getClass()); + if (setter == null) { + throw new IllegalArgumentException("Unsupported type: " + value.getClass()); + } + setter.setOnBundle(bundle, key, value); + } + + return bundle; + } +} \ No newline at end of file diff --git a/android/src/main/java/com/evollu/react/fcm/FIRLocalMessagingHelper.java b/android/src/main/java/com/evollu/react/fcm/FIRLocalMessagingHelper.java new file mode 100644 index 0000000..71044c4 --- /dev/null +++ b/android/src/main/java/com/evollu/react/fcm/FIRLocalMessagingHelper.java @@ -0,0 +1,293 @@ +//Credits to react-native-push-notification + +package com.evollu.react.fcm; + +import android.app.*; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.content.SharedPreferences; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + + +public class FIRLocalMessagingHelper { + private static final long DEFAULT_VIBRATION = 300L; + private static final String TAG = FIRLocalMessagingHelper.class.getSimpleName(); + private final static String PREFERENCES_KEY = "ReactNativeSystemNotification"; + + private Context mContext; + private SharedPreferences sharedPreferences = null; + + public FIRLocalMessagingHelper(Application context) { + mContext = context; + sharedPreferences = (SharedPreferences) mContext.getSharedPreferences(PREFERENCES_KEY, Context.MODE_PRIVATE); + } + + public Class getMainActivityClass() { + String packageName = mContext.getPackageName(); + Intent launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(packageName); + String className = launchIntent.getComponent().getClassName(); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + return null; + } + } + + private AlarmManager getAlarmManager() { + return (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + } + + public void sendNotification(Bundle bundle) { + try { + Class intentClass = getMainActivityClass(); + if (intentClass == null) { + return; + } + + if (bundle.getString("body") == null) { + return; + } + + Resources res = mContext.getResources(); + String packageName = mContext.getPackageName(); + + String title = bundle.getString("title"); + if (title == null) { + ApplicationInfo appInfo = mContext.getApplicationInfo(); + title = mContext.getPackageManager().getApplicationLabel(appInfo).toString(); + } + + NotificationCompat.Builder notification = new NotificationCompat.Builder(mContext) + .setContentTitle(title) + .setContentText(bundle.getString("body")) + .setTicker(bundle.getString("ticker")) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setAutoCancel(bundle.getBoolean("auto_cancel", true)) + .setNumber(bundle.getInt("number")) + .setSubText(bundle.getString("sub_text")) + .setGroup(bundle.getString("group")) + .setVibrate(new long[]{0, DEFAULT_VIBRATION}) + .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) + .setExtras(bundle.getBundle("data")); + + //priority + String priority = bundle.getString("priority", ""); + switch(priority) { + case "min": + notification.setPriority(NotificationCompat.PRIORITY_MIN); + break; + case "high": + notification.setPriority(NotificationCompat.PRIORITY_HIGH); + break; + case "max": + notification.setPriority(NotificationCompat.PRIORITY_MAX); + break; + default: + notification.setPriority(NotificationCompat.PRIORITY_DEFAULT); + } + + //icon + String smallIcon = bundle.getString("icon", "ic_launcher"); + int smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName); + notification.setSmallIcon(smallIconResId); + + //large icon + String largeIcon = bundle.getString("large-icon"); + if(largeIcon != null){ + int largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName); + Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId); + + if (largeIconResId != 0 && (largeIcon != null || android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP)) { + notification.setLargeIcon(largeIconBitmap); + } + } + + //big text + String bigText = bundle.getString("big_text"); + if(bigText != null){ + notification.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); + } + + //sound + if (bundle.containsKey("sound")) { + int soundResourceId = res.getIdentifier(bundle.getString("sound"), "raw", packageName); + notification.setSound(Uri.parse("android.resource://" + packageName + "/" + soundResourceId)); + } + + //color + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + notification.setCategory(NotificationCompat.CATEGORY_CALL); + + String color = bundle.getString("color"); + if (color != null) { + notification.setColor(Color.parseColor(color)); + } + } + + //vibrate + if(bundle.containsKey("vibrate")){ + long vibrate = bundle.getLong("vibrate", Math.round(bundle.getDouble("vibrate", bundle.getInt("vibrate")))); + if(vibrate > 0){ + notification.setVibrate(new long[]{0, vibrate}); + }else{ + notification.setVibrate(null); + } + } + + + Intent intent = new Intent(mContext, intentClass); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra("notification", bundle); + intent.putExtra("localNotification", true); + + int notificationID = bundle.containsKey("id") ? bundle.getString("id", "").hashCode() : (int) System.currentTimeMillis(); + PendingIntent pendingIntent = PendingIntent.getActivity(mContext, notificationID, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + + notification.setContentIntent(pendingIntent); + + Notification info = notification.build(); + + if (bundle.containsKey("tag")) { + String tag = bundle.getString("tag"); + notificationManager.notify(tag, notificationID, info); + } else { + notificationManager.notify(notificationID, info); + } + + //clear out one time scheduled notification once fired + if(!bundle.containsKey("repeat_interval") && bundle.containsKey("fire_date")) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(bundle.getString("id")); + editor.apply(); + } + } catch (Exception e) { + Log.e(TAG, "failed to send local notification", e); + } + } + + public void sendNotificationScheduled(Bundle bundle) { + Class intentClass = getMainActivityClass(); + if (intentClass == null) { + return; + } + + String notificationId = bundle.getString("id"); + if(notificationId == null){ + Log.e(TAG, "failed to schedule notification because id is missing"); + return; + } + + Long fireDate = bundle.getLong("fire_date", Math.round(bundle.getDouble("fire_date"))); + if (fireDate == 0) { + Log.e(TAG, "failed to schedule notification because fire date is missing"); + return; + } + + Intent notificationIntent = new Intent(mContext, FIRLocalMessagingPublisher.class); + notificationIntent.putExtras(bundle); + PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + Long interval = null; + switch (bundle.getString("repeat_interval", "")) { + case "minute": + interval = (long) 60000; + break; + case "hour": + interval = AlarmManager.INTERVAL_HOUR; + break; + case "day": + interval = AlarmManager.INTERVAL_DAY; + break; + case "week": + interval = AlarmManager.INTERVAL_DAY * 7; + break; + } + + if(interval != null){ + getAlarmManager().setRepeating(AlarmManager.RTC_WAKEUP, fireDate, interval, pendingIntent); + } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ + getAlarmManager().setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + }else { + getAlarmManager().set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + } + + //store intent + SharedPreferences.Editor editor = sharedPreferences.edit(); + JSONObject json = null; + try { + json = BundleJSONConverter.convertToJSON(bundle); + } catch (JSONException e) { + e.printStackTrace(); + } + editor.putString(notificationId, json.toString()); + editor.apply(); + } + + public void cancelLocalNotification(String notificationId) { + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + + notificationManager.cancel(notificationId.hashCode()); + + cancelAlarm(notificationId); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(notificationId); + editor.apply(); + } + + public void cancelAllLocalNotifications() { + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + + notificationManager.cancelAll(); + + java.util.Map keyMap = sharedPreferences.getAll(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + for(java.util.Map.Entry entry:keyMap.entrySet()){ + cancelAlarm(entry.getKey()); + } + editor.clear(); + editor.apply(); + } + + public void cancelAlarm(String notificationId) { + Intent notificationIntent = new Intent(mContext, FIRLocalMessagingPublisher.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + getAlarmManager().cancel(pendingIntent); + } + + public ArrayList getScheduledLocalNotifications(){ + ArrayList array = new ArrayList(); + java.util.Map keyMap = sharedPreferences.getAll(); + for(java.util.Map.Entry entry:keyMap.entrySet()){ + try { + JSONObject json = new JSONObject((String)entry.getValue()); + Bundle bundle = BundleJSONConverter.convertToBundle(json); + array.add(bundle); + } catch (JSONException e) { + e.printStackTrace(); + } + } + return array; + } +} \ No newline at end of file diff --git a/android/src/main/java/com/evollu/react/fcm/FIRLocalMessagingPublisher.java b/android/src/main/java/com/evollu/react/fcm/FIRLocalMessagingPublisher.java new file mode 100644 index 0000000..2a44fb5 --- /dev/null +++ b/android/src/main/java/com/evollu/react/fcm/FIRLocalMessagingPublisher.java @@ -0,0 +1,15 @@ +package com.evollu.react.fcm; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class FIRLocalMessagingPublisher extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + new FIRLocalMessagingHelper((Application) context.getApplicationContext()).sendNotification(intent.getExtras()); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/evollu/react/fcm/FIRMessagingModule.java b/android/src/main/java/com/evollu/react/fcm/FIRMessagingModule.java index 4497bb3..0a418ad 100644 --- a/android/src/main/java/com/evollu/react/fcm/FIRMessagingModule.java +++ b/android/src/main/java/com/evollu/react/fcm/FIRMessagingModule.java @@ -12,27 +12,33 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.RemoteMessage; +import android.app.Application; import android.os.Bundle; import android.util.Log; import android.content.Context; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; public class FIRMessagingModule extends ReactContextBaseJavaModule implements LifecycleEventListener, ActivityEventListener { private final static String TAG = FIRMessagingModule.class.getCanonicalName(); + private FIRLocalMessagingHelper mFIRLocalMessagingHelper; Intent initIntent; public FIRMessagingModule(ReactApplicationContext reactContext) { super(reactContext); + mFIRLocalMessagingHelper = new FIRLocalMessagingHelper((Application) reactContext.getApplicationContext()); getReactApplicationContext().addLifecycleEventListener(this); getReactApplicationContext().addActivityEventListener(this); registerTokenRefreshHandler(); @@ -50,6 +56,11 @@ public class FIRMessagingModule extends ReactContextBaseJavaModule implements Li return "RNFIRMessaging"; } + @ReactMethod + public void getInitialNotification(Promise promise){ + promise.resolve(getCurrentActivity().getIntent()); + } + @ReactMethod public void requestPermissions(){ } @@ -60,6 +71,27 @@ public class FIRMessagingModule extends ReactContextBaseJavaModule implements Li promise.resolve(FirebaseInstanceId.getInstance().getToken()); } + @ReactMethod + public void presentLocalNotification(ReadableMap details) { + Bundle bundle = Arguments.toBundle(details); + mFIRLocalMessagingHelper.sendNotification(bundle); + } + + @ReactMethod + public void scheduleLocalNotification(ReadableMap details) { + Bundle bundle = Arguments.toBundle(details); + mFIRLocalMessagingHelper.sendNotificationScheduled(bundle); + } + + @ReactMethod + public void cancelLocalNotification(String notificationID) { + mFIRLocalMessagingHelper.cancelLocalNotification(notificationID); + } + @ReactMethod + public void cancelAllLocalNotifications() { + mFIRLocalMessagingHelper.cancelAllLocalNotifications(); + } + @ReactMethod public void subscribeToTopic(String topic){ FirebaseMessaging.getInstance().subscribeToTopic(topic); @@ -70,10 +102,20 @@ public class FIRMessagingModule extends ReactContextBaseJavaModule implements Li FirebaseMessaging.getInstance().unsubscribeFromTopic(topic); } + @ReactMethod + public void getScheduledLocalNotifications(Promise promise){ + ArrayList bundles = mFIRLocalMessagingHelper.getScheduledLocalNotifications(); + WritableArray array = Arguments.createArray(); + for(Bundle bundle:bundles){ + array.pushMap(Arguments.fromBundle(bundle)); + } + promise.resolve(array); + } + private void sendEvent(String eventName, Object params) { - getReactApplicationContext() - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(eventName, params); + getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); } private void registerTokenRefreshHandler() { @@ -161,6 +203,8 @@ public class FIRMessagingModule extends ReactContextBaseJavaModule implements Li @Override public void onNewIntent(Intent intent){ - sendEvent("FCMNotificationReceived", parseIntent(intent)); + Bundle bundle = intent.getExtras(); + Boolean isLocalNotification = bundle.getBoolean("localNotification", false); + sendEvent(isLocalNotification ? "FCMLocalNotificationReceived" : "FCMNotificationReceived", parseIntent(intent)); } } diff --git a/android/src/main/java/com/evollu/react/fcm/FIRSystemBootEventReceiver.java b/android/src/main/java/com/evollu/react/fcm/FIRSystemBootEventReceiver.java new file mode 100644 index 0000000..c70eace --- /dev/null +++ b/android/src/main/java/com/evollu/react/fcm/FIRSystemBootEventReceiver.java @@ -0,0 +1,27 @@ +package com.evollu.react.fcm; + + import android.app.Application; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + + import java.util.ArrayList; + + import android.os.Bundle; + import android.util.Log; + +/** + * Set alarms for scheduled notification after system reboot. + */ +public class FIRSystemBootEventReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + Log.i("FCMSystemBootReceiver", "Received reboot event"); + FIRLocalMessagingHelper helper = new FIRLocalMessagingHelper((Application) context.getApplicationContext()); + ArrayList bundles = helper.getScheduledLocalNotifications(); + for(Bundle bundle: bundles){ + helper.sendNotificationScheduled(bundle); + } + } +} \ No newline at end of file diff --git a/index.js b/index.js index d73264d..69b24f2 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,19 @@ -import {NativeModules, DeviceEventEmitter} from 'react-native'; +import {NativeModules, DeviceEventEmitter, Platform} from 'react-native'; const eventsMap = { refreshToken: 'FCMTokenRefreshed', - notification: 'FCMNotificationReceived' + notification: 'FCMNotificationReceived', + localNotification: 'FCMLocalNotificationReceived' }; const FIRMessaging = NativeModules.RNFIRMessaging; const FCM = {}; +FCM.getInitialNotification = () => { + return FIRMessaging.getInitialNotification(); +} + FCM.getFCMToken = () => { return FIRMessaging.getFCMToken(); }; @@ -17,9 +22,36 @@ FCM.requestPermissions = () => { return FIRMessaging.requestPermissions(); }; +FCM.presentLocalNotification = (details) => { + FIRMessaging.presentLocalNotification(details); +}; + +FCM.scheduleLocalNotification = function(details) { + FIRMessaging.scheduleLocalNotification(details); +}; + +FCM.getScheduledLocalNotifications = function() { + return FIRMessaging.getScheduledLocalNotifications(); +}; + +FCM.cancelLocalNotification = (notificationID) => { + FIRMessaging.cancelLocalNotification(notificationID); +}; + +FCM.cancelAllLocalNotifications = () => { + FIRMessaging.cancelAllLocalNotifications(); +}; + +FCM.setBadgeNumber = () => { + FIRMessaging.setBadgeNumber(); +} + +FCM.getBadgeNumber = () => { + return FIRMessaging.getBadgeNumber(); +} + FCM.on = (event, callback) => { const nativeEvent = eventsMap[event]; - const listener = DeviceEventEmitter.addListener(nativeEvent, callback); return function remove() { @@ -36,8 +68,8 @@ FCM.unsubscribeFromTopic = (topic) => { }; //once doesn't seem to work -DeviceEventEmitter.addListener('FCMInitData', (data)=>{ - FCM.initialData = data; +DeviceEventEmitter.addListener('FCMInitData', (data) => { + FCM.initialData = data; }); FCM.initialData = FIRMessaging.initialData; diff --git a/ios/RNFIRMessaging.h b/ios/RNFIRMessaging.h index 98aa5ce..0a4fb1a 100644 --- a/ios/RNFIRMessaging.h +++ b/ios/RNFIRMessaging.h @@ -7,6 +7,7 @@ extern NSString *const FCMNotificationReceived; +extern NSString *const FCMLocalNotificationReceived; @interface RNFIRMessaging : NSObject diff --git a/ios/RNFIRMesssaging.m b/ios/RNFIRMesssaging.m index 65a32e6..11eab33 100644 --- a/ios/RNFIRMesssaging.m +++ b/ios/RNFIRMesssaging.m @@ -16,7 +16,55 @@ #endif NSString *const FCMNotificationReceived = @"FCMNotificationReceived"; +NSString *const FCMLocalNotificationReceived = @"FCMLocalNotificationReceived"; +@implementation RCTConvert (NSCalendarUnit) + ++ (NSCalendarUnit *)NSCalendarUnit:(id)json +{ + NSString* key = [self NSString:json]; + if([key isEqualToString:@"minute"]){ + return NSCalendarUnitMinute; + } + if([key isEqualToString:@"second"]){ + return NSCalendarUnitSecond; + } + if([key isEqualToString:@"day"]){ + return NSCalendarUnitDay; + } + if([key isEqualToString:@"month"]){ + return NSCalendarUnitMonth; + } + if([key isEqualToString:@"week"]){ + return NSCalendarUnitWeekOfYear; + } + if([key isEqualToString:@"year"]){ + return NSCalendarUnitYear; + } + return 0; +} + +@end + +@implementation RCTConvert (UILocalNotification) + ++ (UILocalNotification *)UILocalNotification:(id)json +{ + NSDictionary *details = [self NSDictionary:json]; + UILocalNotification *notification = [UILocalNotification new]; + notification.fireDate = [RCTConvert NSDate:details[@"fire_date"]] ?: [NSDate date]; + notification.alertTitle = [RCTConvert NSString:details[@"title"]]; + notification.alertBody = [RCTConvert NSString:details[@"body"]]; + notification.alertAction = [RCTConvert NSString:details[@"alert_action"]]; + notification.soundName = [RCTConvert NSString:details[@"sound"]] ?: UILocalNotificationDefaultSoundName; + notification.userInfo = details; + notification.category = [RCTConvert NSString:details[@"click_action"]]; + notification.repeatInterval = [RCTConvert NSCalendarUnit:details[@"repeat_interval"]]; + notification.applicationIconBadgeNumber = [RCTConvert NSInteger:details[@"badge"]]; + return notification; +} + +@end @implementation RNFIRMessaging @@ -38,7 +86,7 @@ RCT_EXPORT_MODULE() - (void)setBridge:(RCTBridge *)bridge { _bridge = bridge; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRemoteNotificationReceived:) name:FCMNotificationReceived @@ -52,10 +100,15 @@ RCT_EXPORT_MODULE() selector:@selector(connectToFCM) name:UIApplicationDidBecomeActiveNotification object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleFCMLocalNotificationReceived:) + name:FCMLocalNotificationReceived + object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTokenRefresh) name:kFIRInstanceIDTokenRefreshNotification object:nil]; - + } - (void)connectToFCM @@ -75,13 +128,61 @@ RCT_EXPORT_MODULE() NSLog(@"Disconnected from FCM"); } -RCT_REMAP_METHOD(getFCMToken, - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ + resolve([_bridge.launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] copy]); +} + +RCT_EXPORT_METHOD(getFCMToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { resolve([[FIRInstanceID instanceID] token]); } +RCT_EXPORT_METHOD(getScheduledLocalNotifications:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + NSMutableArray* list = [[NSMutableArray alloc] init]; + for(UILocalNotification * notif in [RCTSharedApplication() scheduledLocalNotifications]){ + NSString* interval; + + switch(notif.repeatInterval){ + case NSCalendarUnitMinute: + interval = @"minute"; + break; + case NSCalendarUnitSecond: + interval = @"second"; + break; + case NSCalendarUnitDay: + interval = @"day"; + break; + case NSCalendarUnitMonth: + interval = @"month"; + break; + case NSCalendarUnitWeekOfYear: + interval = @"week"; + break; + case NSCalendarUnitYear: + interval = @"year"; + break; + } + NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; + if (notif.fireDate) { + NSDateFormatter *formatter = [NSDateFormatter new]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; + NSString *fireDateString = [formatter stringFromDate:notif.fireDate]; + formattedLocalNotification[@"fire_date"] = fireDateString; + } + formattedLocalNotification[@"alert_action"] = RCTNullIfNil(notif.alertAction); + formattedLocalNotification[@"body"] = RCTNullIfNil(notif.alertBody); + formattedLocalNotification[@"title"] = RCTNullIfNil(notif.alertTitle); + formattedLocalNotification[@"badge"] = @(notif.applicationIconBadgeNumber); + formattedLocalNotification[@"click_action"] = RCTNullIfNil(notif.category); + formattedLocalNotification[@"sound"] = RCTNullIfNil(notif.soundName); + formattedLocalNotification[@"repeat_interval"] = RCTNullIfNil(interval); + formattedLocalNotification[@"data"] = RCTNullIfNil(RCTJSONClean(notif.userInfo)); + [list addObject:formattedLocalNotification]; + } + resolve(list); +} + - (void) onTokenRefresh { [_bridge.eventDispatcher sendDeviceEventWithName:@"FCMTokenRefreshed" body:[[FIRInstanceID instanceID] token]]; @@ -89,21 +190,21 @@ RCT_REMAP_METHOD(getFCMToken, RCT_EXPORT_METHOD(requestPermissions) { - if (RCTRunningInAppExtension()) { - return; - } - - UIUserNotificationType types = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound; - - UIApplication *app = RCTSharedApplication(); - if ([app respondsToSelector:@selector(registerUserNotificationSettings:)]) { - UIUserNotificationSettings *notificationSettings = - [UIUserNotificationSettings settingsForTypes:(NSUInteger)types categories:nil]; - [app registerUserNotificationSettings:notificationSettings]; - [app registerForRemoteNotifications]; - } else { - [app registerForRemoteNotificationTypes:(NSUInteger)types]; - } + if (RCTRunningInAppExtension()) { + return; + } + + UIUserNotificationType types = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound; + + UIApplication *app = RCTSharedApplication(); + if ([app respondsToSelector:@selector(registerUserNotificationSettings:)]) { + UIUserNotificationSettings *notificationSettings = + [UIUserNotificationSettings settingsForTypes:(NSUInteger)types categories:nil]; + [app registerUserNotificationSettings:notificationSettings]; + [app registerForRemoteNotifications]; + } else { + [app registerForRemoteNotificationTypes:(NSUInteger)types]; + } } RCT_EXPORT_METHOD(subscribeToTopic: (NSString*) topic) @@ -116,12 +217,53 @@ RCT_EXPORT_METHOD(unsubscribeFromTopic: (NSString*) topic) [[FIRMessaging messaging] unsubscribeFromTopic:topic]; } +RCT_EXPORT_METHOD(presentLocalNotification:(UILocalNotification *)notification) +{ + [RCTSharedApplication() presentLocalNotificationNow:notification]; +} + +RCT_EXPORT_METHOD(scheduleLocalNotification:(UILocalNotification *)notification) +{ + [RCTSharedApplication() scheduleLocalNotification:notification]; +} + +RCT_EXPORT_METHOD(cancelAllLocalNotifications) +{ + [RCTSharedApplication() cancelAllLocalNotifications]; +} + +RCT_EXPORT_METHOD(cancelLocalNotification:(NSString*) notificationId) +{ + for (UILocalNotification *notification in [UIApplication sharedApplication].scheduledLocalNotifications) { + NSDictionary *notificationInfo = notification.userInfo; + if([notificationId isEqualToString:[notificationInfo valueForKey:@"id"]]){ + [[UIApplication sharedApplication] cancelLocalNotification:notification]; + } + } +} + +RCT_EXPORT_METHOD(setBadgeNumber: (NSInteger*) number) +{ + [RCTSharedApplication() setApplicationIconBadgeNumber:number]; +} + +RCT_EXPORT_METHOD(getBadgeNumber: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + resolve(@([RCTSharedApplication() applicationIconBadgeNumber])); +} + +- (void)handleFCMLocalNotificationReceived:(UILocalNotification *)notification +{ + NSMutableDictionary *data = [[NSMutableDictionary alloc]initWithDictionary: notification.userInfo]; + [data setValue:@(RCTSharedApplication().applicationState == UIApplicationStateInactive) forKey:@"opened_from_tray"]; + [_bridge.eventDispatcher sendDeviceEventWithName:FCMLocalNotificationReceived body:data]; +} + - (void)handleRemoteNotificationReceived:(NSNotification *)notification { NSMutableDictionary *data = [[NSMutableDictionary alloc]initWithDictionary: notification.userInfo]; [data setValue:@(RCTSharedApplication().applicationState == UIApplicationStateInactive) forKey:@"opened_from_tray"]; - [_bridge.eventDispatcher sendDeviceEventWithName:FCMNotificationReceived - body:data]; + [_bridge.eventDispatcher sendDeviceEventWithName:FCMNotificationReceived body:data]; } @end -- 2.26.2