expo-notifications 1.0.0-canary-20240927-ab8a962 → 1.0.0-canary-20241008-90b13ad

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/android/src/main/java/expo/modules/notifications/notifications/NotificationManager.java +8 -0
  3. package/android/src/main/java/expo/modules/notifications/notifications/NotificationSerializer.java +7 -1
  4. package/android/src/main/java/expo/modules/notifications/notifications/background/BackgroundRemoteNotificationTaskConsumer.java +2 -1
  5. package/android/src/main/java/expo/modules/notifications/notifications/debug/DebugLogging.kt +1 -1
  6. package/android/src/main/java/expo/modules/notifications/notifications/handling/NotificationsHandler.kt +3 -1
  7. package/android/src/main/java/expo/modules/notifications/notifications/handling/SingleNotificationHandlerTask.java +2 -4
  8. package/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt +2 -2
  9. package/android/src/main/java/expo/modules/notifications/notifications/interfaces/NotificationBuilder.kt +1 -9
  10. package/android/src/main/java/expo/modules/notifications/notifications/interfaces/NotificationListener.java +1 -1
  11. package/android/src/main/java/expo/modules/notifications/notifications/model/NotificationAction.java +3 -0
  12. package/android/src/main/java/expo/modules/notifications/notifications/model/NotificationCategory.java +2 -0
  13. package/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java +1 -1
  14. package/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt +1 -1
  15. package/android/src/main/java/expo/modules/notifications/notifications/presentation/ExpoNotificationPresentationModule.kt +16 -2
  16. package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/BaseNotificationBuilder.kt +148 -0
  17. package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt +98 -29
  18. package/android/src/main/java/expo/modules/notifications/notifications/scheduling/NotificationScheduler.kt +18 -0
  19. package/android/src/main/java/expo/modules/notifications/notifications/triggers/MonthlyTrigger.java +85 -0
  20. package/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt +11 -6
  21. package/android/src/main/java/expo/modules/notifications/service/delegates/ExpoHandlingDelegate.kt +11 -0
  22. package/android/src/main/java/expo/modules/notifications/service/delegates/ExpoPresentationDelegate.kt +6 -5
  23. package/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt +13 -2
  24. package/build/NotificationScheduler.types.d.ts +8 -1
  25. package/build/NotificationScheduler.types.d.ts.map +1 -1
  26. package/build/NotificationScheduler.types.js.map +1 -1
  27. package/build/Notifications.types.d.ts +35 -2
  28. package/build/Notifications.types.d.ts.map +1 -1
  29. package/build/Notifications.types.js +1 -0
  30. package/build/Notifications.types.js.map +1 -1
  31. package/build/NotificationsEmitter.d.ts.map +1 -1
  32. package/build/NotificationsEmitter.js +8 -3
  33. package/build/NotificationsEmitter.js.map +1 -1
  34. package/build/scheduleNotificationAsync.d.ts.map +1 -1
  35. package/build/scheduleNotificationAsync.js +25 -1
  36. package/build/scheduleNotificationAsync.js.map +1 -1
  37. package/ios/EXNotifications/Notifications/Emitter/EXNotificationsEmitter.h +1 -0
  38. package/ios/EXNotifications/Notifications/Emitter/EXNotificationsEmitter.m +1 -1
  39. package/ios/EXNotifications/Notifications/Scheduling/EXNotificationSchedulerModule.m +16 -0
  40. package/package.json +7 -7
  41. package/plugin/build/withNotificationsIOS.js +3 -1
  42. package/plugin/src/withNotificationsIOS.ts +3 -1
  43. package/src/NotificationScheduler.types.ts +9 -0
  44. package/src/Notifications.types.ts +30 -2
  45. package/src/NotificationsEmitter.ts +9 -3
  46. package/src/scheduleNotificationAsync.ts +32 -1
  47. package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/BaseNotificationBuilder.java +0 -50
  48. package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/CategoryAwareNotificationBuilder.java +0 -80
  49. package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ChannelAwareNotificationBuilder.java +0 -134
package/CHANGELOG.md CHANGED
@@ -10,13 +10,18 @@
10
10
  ### 🎉 New features
11
11
 
12
12
  - Add clearLastNotificationResponseAsync to API. ([#31607](https://github.com/expo/expo/pull/31607) by [@douglowder](https://github.com/douglowder))
13
+ - New monthly trigger type for scheduled notifications. ([#31823](https://github.com/expo/expo/pull/31823) by [@douglowder](https://github.com/douglowder))
13
14
 
14
15
  ### 🐛 Bug fixes
15
16
 
17
+ - throw improved error on invalid subscription in removeNotificationSubscription ([#31842](https://github.com/expo/expo/pull/31842) by [@vonovak](https://github.com/vonovak))
18
+ - [android] fix notifications actions not being presented ([#31795](https://github.com/expo/expo/pull/31795) by [@vonovak](https://github.com/vonovak))
16
19
  - Add missing `react` and `react-native` peer dependencies for isolated modules. ([#30478](https://github.com/expo/expo/pull/30478) by [@byCedric](https://github.com/byCedric))
20
+ - [iOS] do not overwrite existing aps entitlement. ([#31892](https://github.com/expo/expo/pull/31892) by [@douglowder](https://github.com/douglowder))
17
21
 
18
22
  ### 💡 Others
19
23
 
24
+ - [android] refactor ExpoNotificationBuilder ([#31838](https://github.com/expo/expo/pull/31838) by [@vonovak](https://github.com/vonovak))
20
25
  - Warn about limited support in Expo Go ([#31573](https://github.com/expo/expo/pull/31573) by [@vonovak](https://github.com/vonovak))
21
26
  - Keep using the legacy event emitter as the module is not fully migrated to Expo Modules API. ([#28946](https://github.com/expo/expo/pull/28946) by [@tsapeta](https://github.com/tsapeta))
22
27
 
@@ -30,6 +30,10 @@ public class NotificationManager implements SingletonModule, expo.modules.notifi
30
30
  public NotificationManager() {
31
31
  mListenerReferenceMap = new WeakHashMap<>();
32
32
 
33
+ // TODO @vonovak there's a chain of listeners:
34
+ // ExpoHandlingDelegate -> NotificationManager -> NotificationsEmitter
35
+ // -> NotificationsHandler
36
+ // it seems it could be shorter?
33
37
  // Registers this singleton instance in static ExpoHandlingDelegate listeners collection.
34
38
  // Since it doesn't hold strong reference to the object this should be safe.
35
39
  ExpoHandlingDelegate.Companion.addListener(this);
@@ -81,6 +85,10 @@ public class NotificationManager implements SingletonModule, expo.modules.notifi
81
85
  * Calls {@link NotificationListener#onNotificationReceived(Notification)} on all values
82
86
  * of {@link NotificationManager#mListenerReferenceMap}.
83
87
  *
88
+ * In practice, that means calling {@link NotificationsEmitter} (just emits an event to JS) and
89
+ * {@link NotificationsHandler} which calls `handleNotification` in JS to determine the behavior.
90
+ * Then `SingleNotificationHandlerTask.processNotificationWithBehavior` may present it.
91
+ *
84
92
  * @param notification Notification received
85
93
  */
86
94
  public void onNotificationReceived(Notification notification) {
@@ -32,6 +32,7 @@ import expo.modules.notifications.notifications.model.triggers.FirebaseNotificat
32
32
 
33
33
  import expo.modules.notifications.notifications.triggers.DailyTrigger;
34
34
  import expo.modules.notifications.notifications.triggers.DateTrigger;
35
+ import expo.modules.notifications.notifications.triggers.MonthlyTrigger;
35
36
  import expo.modules.notifications.notifications.triggers.TimeIntervalTrigger;
36
37
  import expo.modules.notifications.notifications.triggers.WeeklyTrigger;
37
38
  import expo.modules.notifications.notifications.triggers.YearlyTrigger;
@@ -101,7 +102,7 @@ public class NotificationSerializer {
101
102
  public static Bundle toBundle(INotificationContent content) {
102
103
  Bundle serializedContent = new Bundle();
103
104
  serializedContent.putString("title", content.getTitle());
104
- serializedContent.putString("subtitle", content.getSubtitle());
105
+ serializedContent.putString("subtitle", content.getSubText());
105
106
  serializedContent.putString("body", content.getText());
106
107
  if (content.getColor() != null) {
107
108
  serializedContent.putString("color", String.format("#%08X", content.getColor().intValue()));
@@ -211,6 +212,11 @@ public class NotificationSerializer {
211
212
  bundle.putInt("weekday", ((WeeklyTrigger) trigger).getWeekday());
212
213
  bundle.putInt("hour", ((WeeklyTrigger) trigger).getHour());
213
214
  bundle.putInt("minute", ((WeeklyTrigger) trigger).getMinute());
215
+ } else if (trigger instanceof MonthlyTrigger) {
216
+ bundle.putString("type", "monthly");
217
+ bundle.putInt("day", ((MonthlyTrigger) trigger).getDay());
218
+ bundle.putInt("hour", ((MonthlyTrigger) trigger).getHour());
219
+ bundle.putInt("minute", ((MonthlyTrigger) trigger).getMinute());
214
220
  } else if (trigger instanceof YearlyTrigger) {
215
221
  bundle.putString("type", "yearly");
216
222
  bundle.putInt("day", ((YearlyTrigger) trigger).getDay());
@@ -25,9 +25,10 @@ import expo.modules.interfaces.taskManager.TaskManagerUtilsInterface;
25
25
  /**
26
26
  * Represents a task to be run when the app is receives a remote push
27
27
  * notification. Map of current tasks is maintained in {@link FirebaseMessagingDelegate}.
28
+ *
29
+ * Instances are instantiated by expo task manager, after being registered in ExpoBackgroundNotificationTasksModule
28
30
  */
29
31
  public class BackgroundRemoteNotificationTaskConsumer extends TaskConsumer implements TaskConsumerInterface {
30
- private static final String TAG = BackgroundRemoteNotificationTaskConsumer.class.getSimpleName();
31
32
  private static final String NOTIFICATION_KEY = "notification";
32
33
 
33
34
  private TaskInterface mTask;
@@ -70,7 +70,7 @@ object DebugLogging {
70
70
  """
71
71
  $caller:
72
72
  notification.notificationRequest.content.title: ${notification.notificationRequest.content.title}
73
- notification.notificationRequest.content.subtitle: ${notification.notificationRequest.content.subtitle}
73
+ notification.notificationRequest.content.subText: ${notification.notificationRequest.content.subText}
74
74
  notification.notificationRequest.content.text: ${notification.notificationRequest.content.text}
75
75
  notification.notificationRequest.content.sound: ${notification.notificationRequest.content.soundName}
76
76
  notification.notificationRequest.content.channelID: ${notification.notificationRequest.trigger.notificationChannel}
@@ -99,7 +99,7 @@ open class NotificationsHandler : Module(), NotificationListener {
99
99
  ?: throw NotificationWasAlreadyHandledException(identifier)
100
100
 
101
101
  with(behavior) {
102
- task.handleResponse(
102
+ task.processNotificationWithBehavior(
103
103
  NotificationBehavior(shouldShowAlert, shouldPlaySound, shouldSetBadge, priority),
104
104
  promise
105
105
  )
@@ -110,6 +110,8 @@ open class NotificationsHandler : Module(), NotificationListener {
110
110
  * Callback called by [NotificationManager] to inform its listeners of new messages.
111
111
  * Starts up a new [SingleNotificationHandlerTask] which will take it on from here.
112
112
  *
113
+ * SingleNotificationHandlerTask.processNotificationWithBehavior can then present it
114
+ *
113
115
  * @param notification Notification received
114
116
  */
115
117
  override fun onNotificationReceived(notification: Notification) {
@@ -34,7 +34,6 @@ public class SingleNotificationHandlerTask {
34
34
  private Handler mHandler;
35
35
  private EventEmitter mEventEmitter;
36
36
  private Notification mNotification;
37
- private NotificationBehavior mBehavior;
38
37
  private Context mContext;
39
38
  private NotificationsHandler mDelegate;
40
39
 
@@ -88,12 +87,11 @@ public class SingleNotificationHandlerTask {
88
87
  * @param behavior Behavior requested by the app
89
88
  * @param promise Promise to fulfill once the behavior is applied to the notification.
90
89
  */
91
- /* package */ void handleResponse(NotificationBehavior behavior, final Promise promise) {
92
- mBehavior = behavior;
90
+ /* package */ void processNotificationWithBehavior(final NotificationBehavior behavior, final Promise promise) {
93
91
  mHandler.post(new Runnable() {
94
92
  @Override
95
93
  public void run() {
96
- NotificationsService.Companion.present(mContext, mNotification, mBehavior, new ResultReceiver(mHandler) {
94
+ NotificationsService.Companion.present(mContext, mNotification, behavior, new ResultReceiver(mHandler) {
97
95
  @Override
98
96
  protected void onReceiveResult(int resultCode, Bundle resultData) {
99
97
  super.onReceiveResult(resultCode, resultData);
@@ -17,7 +17,7 @@ import org.json.JSONObject
17
17
  interface INotificationContent : Parcelable {
18
18
  val title: String?
19
19
  val text: String?
20
- val subtitle: String?
20
+ val subText: String?
21
21
  val badgeCount: Number?
22
22
  val shouldPlayDefaultSound: Boolean
23
23
 
@@ -27,7 +27,7 @@ interface INotificationContent : Parcelable {
27
27
  val shouldUseDefaultVibrationPattern: Boolean
28
28
  val vibrationPattern: LongArray?
29
29
  val body: JSONObject?
30
- val priority: NotificationPriority
30
+ val priority: NotificationPriority?
31
31
  val color: Number?
32
32
  val isAutoDismiss: Boolean
33
33
  val categoryId: String?
@@ -8,14 +8,6 @@ import expo.modules.notifications.notifications.model.NotificationBehavior
8
8
  * on a [NotificationContent] spec.
9
9
  */
10
10
  interface NotificationBuilder {
11
- /**
12
- * Pass in a [Notification] based on which the Android notification should be based.
13
- *
14
- * @param notification [Notification] on which the notification should be based.
15
- * @return The same instance of [NotificationBuilder] updated with the notification.
16
- */
17
- fun setNotification(notification: Notification?): NotificationBuilder?
18
-
19
11
  /**
20
12
  * Pass in a [NotificationBehavior] if you want to override the behavior
21
13
  * of the notification, i.e. whether it should show a heads-up alert, set badge, etc.
@@ -23,7 +15,7 @@ interface NotificationBuilder {
23
15
  * @param behavior [NotificationBehavior] to which the presentation effect should conform.
24
16
  * @return The same instance of [NotificationBuilder] updated with the remote message.
25
17
  */
26
- fun setAllowedBehavior(behavior: NotificationBehavior?): NotificationBuilder?
18
+ fun setAllowedBehavior(behavior: NotificationBehavior?): NotificationBuilder
27
19
 
28
20
  /**
29
21
  * Builds the Android notification based on passed in data.
@@ -13,7 +13,7 @@ import expo.modules.notifications.notifications.model.NotificationResponse;
13
13
  */
14
14
  public interface NotificationListener {
15
15
  /**
16
- * Callback called when new notification is received.
16
+ * Callback called when new notification is received while the app is in foreground.
17
17
  *
18
18
  * @param notification Notification received
19
19
  */
@@ -7,6 +7,9 @@ import java.io.Serializable;
7
7
 
8
8
  /**
9
9
  * A class representing a single notification action button.
10
+ *
11
+ * TODO vonovak: no need to implement serializable, parcelable is enough for storing
12
+ *
10
13
  */
11
14
  public class NotificationAction implements Parcelable, Serializable {
12
15
  private final String mIdentifier;
@@ -9,6 +9,8 @@ import java.util.List;
9
9
 
10
10
  /**
11
11
  * A class representing a collection of notification actions.
12
+ *
13
+ * TODO vonovak: no need to implement serializable, parcelable is enough for storing
12
14
  */
13
15
  public class NotificationCategory implements Parcelable, Serializable {
14
16
  private final String mIdentifier;
@@ -75,7 +75,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
75
75
  }
76
76
 
77
77
  @Nullable
78
- public String getSubtitle() {
78
+ public String getSubText() {
79
79
  return mSubtitle;
80
80
  }
81
81
 
@@ -75,7 +75,7 @@ class RemoteNotificationContent(private val remoteMessage: RemoteMessage) : INot
75
75
  override val isSticky: Boolean
76
76
  get() = remoteMessage.data["sticky"]?.toBoolean() ?: false
77
77
 
78
- override val subtitle: String?
78
+ override val subText: String?
79
79
  get() = remoteMessage.data["subtitle"]
80
80
 
81
81
  override val badgeCount: Number?
@@ -11,6 +11,8 @@ import expo.modules.notifications.ResultReceiverBody
11
11
  import expo.modules.notifications.createDefaultResultReceiver
12
12
  import expo.modules.notifications.notifications.ArgumentsNotificationContentBuilder
13
13
  import expo.modules.notifications.notifications.NotificationSerializer
14
+ import expo.modules.notifications.notifications.interfaces.INotificationContent
15
+ import expo.modules.notifications.notifications.interfaces.NotificationTrigger
14
16
  import expo.modules.notifications.notifications.model.Notification
15
17
  import expo.modules.notifications.notifications.model.NotificationRequest
16
18
  import expo.modules.notifications.service.NotificationsService
@@ -31,7 +33,7 @@ open class ExpoNotificationPresentationModule : Module() {
31
33
 
32
34
  AsyncFunction("presentNotificationAsync") { identifier: String, payload: ReadableArguments, promise: Promise ->
33
35
  val content = ArgumentsNotificationContentBuilder(context).setPayload(payload).build()
34
- val request = NotificationRequest(identifier, content, null)
36
+ val request = createNotificationRequest(identifier, content, null)
35
37
  val notification = Notification(request)
36
38
  present(
37
39
  context,
@@ -54,7 +56,7 @@ open class ExpoNotificationPresentationModule : Module() {
54
56
  createResultReceiver { resultCode: Int, resultData: Bundle? ->
55
57
  val notifications = resultData?.getParcelableArrayList<Notification>(NotificationsService.NOTIFICATIONS_KEY)
56
58
  if (resultCode == NotificationsService.SUCCESS_CODE && notifications != null) {
57
- promise.resolve(notifications.map(NotificationSerializer::toBundle))
59
+ promise.resolve(serializeNotifications(notifications))
58
60
  } else {
59
61
  val e = resultData?.getSerializable(NotificationsService.EXCEPTION_KEY) as? Exception
60
62
  promise.reject("ERR_NOTIFICATIONS_FETCH_FAILED", "A list of displayed notifications could not be fetched.", e)
@@ -96,4 +98,16 @@ open class ExpoNotificationPresentationModule : Module() {
96
98
  }
97
99
  )
98
100
  }
101
+
102
+ protected open fun createNotificationRequest(
103
+ identifier: String,
104
+ content: INotificationContent,
105
+ trigger: NotificationTrigger?
106
+ ): NotificationRequest {
107
+ return NotificationRequest(identifier, content, null)
108
+ }
109
+
110
+ protected open fun serializeNotifications(notifications: Collection<Notification>): List<Bundle> {
111
+ return notifications.map(NotificationSerializer::toBundle)
112
+ }
99
113
  }
@@ -0,0 +1,148 @@
1
+ package expo.modules.notifications.notifications.presentation.builders
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.content.Context
6
+ import android.os.Build
7
+ import android.util.Log
8
+ import androidx.annotation.RequiresApi
9
+ import androidx.core.app.NotificationCompat
10
+ import expo.modules.notifications.R
11
+ import expo.modules.notifications.notifications.channels.managers.AndroidXNotificationsChannelGroupManager
12
+ import expo.modules.notifications.notifications.channels.managers.AndroidXNotificationsChannelManager
13
+ import expo.modules.notifications.notifications.channels.managers.NotificationsChannelManager
14
+ import expo.modules.notifications.notifications.interfaces.INotificationContent
15
+ import expo.modules.notifications.notifications.interfaces.NotificationBuilder
16
+ import expo.modules.notifications.notifications.model.Notification
17
+ import expo.modules.notifications.notifications.model.NotificationBehavior
18
+
19
+ /**
20
+ * A foundation class for [NotificationBuilder] implementations. Takes care
21
+ * of accepting [.mNotification] and [.mNotificationBehavior].
22
+ */
23
+ abstract class BaseNotificationBuilder protected constructor(protected val context: Context, protected val notification: Notification) :
24
+ NotificationBuilder {
25
+
26
+ protected var notificationBehavior: NotificationBehavior? = null
27
+ private set
28
+
29
+ override fun setAllowedBehavior(behavior: NotificationBehavior?): NotificationBuilder {
30
+ notificationBehavior = behavior
31
+ return this
32
+ }
33
+
34
+ fun createBuilder(): NotificationCompat.Builder {
35
+ val builder = channelId?.let { NotificationCompat.Builder(context, it) } ?: NotificationCompat.Builder(context)
36
+ return builder
37
+ }
38
+
39
+ protected val channelId: String?
40
+ /**
41
+ * @return A [NotificationChannel]'s identifier to use for the notification.
42
+ */
43
+ get() {
44
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
45
+ // Returning null on incompatible platforms won't be an error.
46
+ return null
47
+ }
48
+
49
+ val trigger = notification.notificationRequest?.trigger
50
+ if (trigger == null) {
51
+ Log.e(
52
+ "notifications",
53
+ String.format(
54
+ "Couldn't get channel for the notifications - trigger is 'null'. Fallback to '%s' channel",
55
+ FALLBACK_CHANNEL_ID
56
+ )
57
+ )
58
+ return fallbackNotificationChannel!!.id
59
+ }
60
+
61
+ val requestedChannelId = trigger.notificationChannel
62
+ ?: return fallbackNotificationChannel!!.id
63
+
64
+ val channelForRequestedId =
65
+ notificationsChannelManager.getNotificationChannel(requestedChannelId)
66
+ if (channelForRequestedId == null) {
67
+ Log.e(
68
+ "notifications",
69
+ String.format(
70
+ "Channel '%s' doesn't exists. Fallback to '%s' channel",
71
+ requestedChannelId,
72
+ FALLBACK_CHANNEL_ID
73
+ )
74
+ )
75
+ return fallbackNotificationChannel!!.id
76
+ }
77
+
78
+ return channelForRequestedId.id
79
+ }
80
+
81
+ open val notificationsChannelManager: NotificationsChannelManager
82
+ get() = AndroidXNotificationsChannelManager(
83
+ context,
84
+ AndroidXNotificationsChannelGroupManager(context)
85
+ )
86
+
87
+ private val fallbackNotificationChannel: NotificationChannel?
88
+ /**
89
+ * Fetches the fallback notification channel, and if it doesn't exist yet - creates it.
90
+ *
91
+ *
92
+ * Returns null on [NotificationChannel]-incompatible platforms.
93
+ *
94
+ * @return Fallback [NotificationChannel] or null.
95
+ */
96
+ get() {
97
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
98
+ return null
99
+ }
100
+
101
+ val channel = notificationManager.getNotificationChannel(FALLBACK_CHANNEL_ID)
102
+ return channel ?: createFallbackChannel()
103
+ }
104
+
105
+ /**
106
+ * Creates a fallback channel of [.FALLBACK_CHANNEL_ID] ID, name fetched
107
+ * from Android resources ([.getFallbackChannelName]
108
+ * and importance set by [.FALLBACK_CHANNEL_IMPORTANCE].
109
+ *
110
+ * @return Newly created channel.
111
+ */
112
+ @RequiresApi(api = Build.VERSION_CODES.O)
113
+ protected fun createFallbackChannel(): NotificationChannel {
114
+ val channel = NotificationChannel(
115
+ FALLBACK_CHANNEL_ID,
116
+ fallbackChannelName,
117
+ FALLBACK_CHANNEL_IMPORTANCE
118
+ )
119
+ channel.setShowBadge(true)
120
+ channel.enableVibration(true)
121
+ notificationManager.createNotificationChannel(channel)
122
+ return channel
123
+ }
124
+
125
+ private val fallbackChannelName: String
126
+ /**
127
+ * Fetches fallback channel name from Android resources. Overridable by Android resources system
128
+ *
129
+ * @return Name of the fallback channel
130
+ */
131
+ get() = context.getString(R.string.expo_notifications_fallback_channel_name)
132
+
133
+ private val notificationManager: NotificationManager
134
+ get() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
135
+
136
+ companion object {
137
+ private const val FALLBACK_CHANNEL_ID = "expo_notifications_fallback_notification_channel"
138
+
139
+ // Behaviors we will want to impose on received notifications include
140
+ // being displayed as a heads-up notification. For that we will need
141
+ // a channel of high importance.
142
+ @RequiresApi(api = Build.VERSION_CODES.N)
143
+ private val FALLBACK_CHANNEL_IMPORTANCE = NotificationManager.IMPORTANCE_HIGH
144
+ }
145
+
146
+ protected val notificationContent: INotificationContent
147
+ get() = notification.notificationRequest.content
148
+ }
@@ -10,21 +10,86 @@ import android.os.Parcel
10
10
  import android.provider.Settings
11
11
  import android.util.Log
12
12
  import androidx.core.app.NotificationCompat
13
+ import androidx.core.app.RemoteInput
13
14
  import expo.modules.notifications.notifications.SoundResolver
14
15
  import expo.modules.notifications.notifications.enums.NotificationPriority
15
16
  import expo.modules.notifications.notifications.model.NotificationAction
17
+ import expo.modules.notifications.notifications.model.NotificationCategory
16
18
  import expo.modules.notifications.notifications.model.NotificationRequest
17
19
  import expo.modules.notifications.notifications.model.NotificationResponse
20
+ import expo.modules.notifications.notifications.model.TextInputNotificationAction
21
+ import expo.modules.notifications.service.NotificationsService
18
22
  import expo.modules.notifications.service.NotificationsService.Companion.createNotificationResponseIntent
23
+ import expo.modules.notifications.service.delegates.SharedPreferencesNotificationCategoriesStore
24
+ import java.io.IOException
19
25
  import kotlin.math.max
20
26
  import kotlin.math.min
21
27
 
22
28
  /**
23
29
  * [NotificationBuilder] interpreting a JSON request object.
24
30
  */
25
- open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotificationBuilder(context) {
31
+ open class ExpoNotificationBuilder(
32
+ context: Context,
33
+ notification: expo.modules.notifications.notifications.model.Notification,
34
+ private val store: SharedPreferencesNotificationCategoriesStore
35
+ ) : BaseNotificationBuilder(context, notification) {
36
+
37
+ open fun addActionsToBuilder(
38
+ builder: NotificationCompat.Builder,
39
+ categoryIdentifier: String
40
+ ) {
41
+ var actions = emptyList<NotificationAction>()
42
+ try {
43
+ val category: NotificationCategory? = store.getNotificationCategory(categoryIdentifier)
44
+ if (category != null) {
45
+ actions = category.actions
46
+ }
47
+ } catch (e: ClassNotFoundException) {
48
+ Log.e(
49
+ "expo-notifications",
50
+ String.format(
51
+ "Could not read category with identifier: %s. %s",
52
+ categoryIdentifier,
53
+ e.message
54
+ )
55
+ )
56
+ } catch (e: IOException) {
57
+ Log.e(
58
+ "expo-notifications",
59
+ String.format(
60
+ "Could not read category with identifier: %s. %s",
61
+ categoryIdentifier,
62
+ e.message
63
+ )
64
+ )
65
+ }
66
+ for (action in actions) {
67
+ if (action is TextInputNotificationAction) {
68
+ builder.addAction(buildTextInputAction(action))
69
+ } else {
70
+ builder.addAction(buildButtonAction(action))
71
+ }
72
+ }
73
+ }
74
+
75
+ protected fun buildButtonAction(action: NotificationAction): NotificationCompat.Action {
76
+ val intent = createNotificationResponseIntent(context, notification, action)
77
+ return NotificationCompat.Action.Builder(icon, action.title, intent).build()
78
+ }
79
+
80
+ protected fun buildTextInputAction(action: TextInputNotificationAction): NotificationCompat.Action {
81
+ val intent = createNotificationResponseIntent(context, notification, action)
82
+ val remoteInput = RemoteInput.Builder(NotificationsService.USER_TEXT_RESPONSE_KEY)
83
+ .setLabel(action.placeholder)
84
+ .build()
85
+
86
+ return NotificationCompat.Action.Builder(icon, action.title, intent)
87
+ .addRemoteInput(remoteInput).build()
88
+ }
89
+
26
90
  override suspend fun build(): Notification {
27
- val builder = super.createBuilder()
91
+ val builder = createBuilder()
92
+
28
93
  builder.setSmallIcon(icon)
29
94
  builder.setPriority(priority)
30
95
 
@@ -33,14 +98,17 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
33
98
  builder.setAutoCancel(content.isAutoDismiss)
34
99
  builder.setOngoing(content.isSticky)
35
100
 
101
+ // see "Notification anatomy" https://developer.android.com/develop/ui/views/notifications#Templates
36
102
  builder.setContentTitle(content.title)
37
103
  builder.setContentText(content.text)
38
- builder.setSubText(content.subtitle)
104
+ builder.setSubText(content.subText)
39
105
  // Sets the text/contentText as the bigText to allow the notification to be expanded and the
40
106
  // entire text to be viewed.
41
107
  builder.setStyle(NotificationCompat.BigTextStyle().bigText(content.text))
42
108
 
43
109
  color?.let { builder.color = it.toInt() }
110
+ notificationContent.badgeCount?.toInt()?.let { builder.setNumber(it) }
111
+ notificationContent.categoryId?.let { addActionsToBuilder(builder, it) }
44
112
 
45
113
  val shouldPlayDefaultSound = shouldPlaySound() && content.shouldPlayDefaultSound
46
114
  if (shouldPlayDefaultSound && shouldVibrate()) {
@@ -147,10 +215,9 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
147
215
  * @return Whether the notification should play a sound.
148
216
  */
149
217
  private fun shouldPlaySound(): Boolean {
150
- val behaviorAllowsSound =
151
- notificationBehavior == null || notificationBehavior.shouldPlaySound()
152
-
153
- val contentAllowsSound = notificationContent.shouldPlayDefaultSound || notificationContent.soundName != null
218
+ val behaviorAllowsSound = notificationBehavior?.shouldPlaySound() ?: true
219
+ val contentAllowsSound =
220
+ notificationContent.shouldPlayDefaultSound || notificationContent.soundName != null
154
221
 
155
222
  return behaviorAllowsSound && contentAllowsSound
156
223
  }
@@ -166,8 +233,7 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
166
233
  * @return Whether the notification should vibrate.
167
234
  */
168
235
  private fun shouldVibrate(): Boolean {
169
- val behaviorAllowsVibration =
170
- notificationBehavior == null || notificationBehavior.shouldPlaySound()
236
+ val behaviorAllowsVibration = notificationBehavior?.shouldPlaySound() ?: true
171
237
 
172
238
  val contentAllowsVibration =
173
239
  notificationContent.shouldUseDefaultVibrationPattern || notificationContent.vibrationPattern != null
@@ -194,6 +260,7 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
194
260
  get() {
195
261
  val requestPriority = notificationContent.priority
196
262
 
263
+ val notificationBehavior = notificationBehavior
197
264
  // If we know of a behavior guideline, let's honor it...
198
265
  if (notificationBehavior != null) {
199
266
  // ...by using the priority override...
@@ -258,7 +325,7 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
258
325
  return null
259
326
  }
260
327
 
261
- protected val icon: Int
328
+ protected open val icon: Int
262
329
  /**
263
330
  * The method first tries to get the icon from the manifest's meta-data [.META_DATA_DEFAULT_ICON_KEY].
264
331
  * If a custom setting is not found, the method falls back to using app icon.
@@ -280,7 +347,7 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
280
347
  return context.applicationInfo.icon
281
348
  }
282
349
 
283
- protected val color: Number?
350
+ protected open val color: Number?
284
351
  /**
285
352
  * The method responsible for finding and returning a custom color used to color the notification icon.
286
353
  * It first tries to use a custom color defined in notification content, then it tries to fetch color
@@ -290,27 +357,29 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
290
357
  * or null if the default should be used.
291
358
  */
292
359
  get() {
293
- if (notificationContent.color != null) {
294
- return notificationContent.color
295
- }
296
-
297
- try {
298
- val ai = context.packageManager.getApplicationInfo(
299
- context.packageName,
300
- PackageManager.GET_META_DATA
301
- )
302
- if (ai.metaData.containsKey(META_DATA_DEFAULT_COLOR_KEY)) {
303
- return context.resources.getColor(
304
- ai.metaData.getInt(META_DATA_DEFAULT_COLOR_KEY),
305
- null
360
+ return notificationContent.color ?: run {
361
+ try {
362
+ val ai = context.packageManager.getApplicationInfo(
363
+ context.packageName,
364
+ PackageManager.GET_META_DATA
365
+ )
366
+ if (ai.metaData.containsKey(META_DATA_DEFAULT_COLOR_KEY)) {
367
+ return context.resources.getColor(
368
+ ai.metaData.getInt(META_DATA_DEFAULT_COLOR_KEY),
369
+ null
370
+ )
371
+ }
372
+ } catch (e: Exception) {
373
+ Log.e(
374
+ "expo-notifications",
375
+ "Could not have fetched default notification color.",
376
+ e
306
377
  )
307
378
  }
308
- } catch (e: Exception) {
309
- Log.e("expo-notifications", "Could not have fetched default notification color.", e)
310
- }
311
379
 
312
- // No custom color
313
- return null
380
+ // No custom color
381
+ return null
382
+ }
314
383
  }
315
384
 
316
385
  companion object {
@@ -21,6 +21,7 @@ import expo.modules.notifications.notifications.model.NotificationRequest
21
21
  import expo.modules.notifications.notifications.triggers.ChannelAwareTrigger
22
22
  import expo.modules.notifications.notifications.triggers.DailyTrigger
23
23
  import expo.modules.notifications.notifications.triggers.DateTrigger
24
+ import expo.modules.notifications.notifications.triggers.MonthlyTrigger
24
25
  import expo.modules.notifications.notifications.triggers.TimeIntervalTrigger
25
26
  import expo.modules.notifications.notifications.triggers.WeeklyTrigger
26
27
  import expo.modules.notifications.notifications.triggers.YearlyTrigger
@@ -197,6 +198,23 @@ open class NotificationScheduler : Module() {
197
198
  )
198
199
  }
199
200
 
201
+ "monthly" -> {
202
+ val day = params["day"] as? Number
203
+ val hour = params["hour"] as? Number
204
+ val minute = params["minute"] as? Number
205
+
206
+ if (day == null || hour == null || minute == null) {
207
+ throw InvalidArgumentException("Invalid value(s) provided for yearly trigger.")
208
+ }
209
+
210
+ MonthlyTrigger(
211
+ day.toInt(),
212
+ hour.toInt(),
213
+ minute.toInt(),
214
+ channelId
215
+ )
216
+ }
217
+
200
218
  "yearly" -> {
201
219
  val day = params["day"] as? Number
202
220
  val month = params["month"] as? Number