react-native-qpush 0.0.0-stage → 1.0.0

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 (98) hide show
  1. package/README.md +439 -2
  2. package/android/build.gradle +77 -0
  3. package/android/gradle.properties +5 -0
  4. package/android/proguard-rules.pro +5 -0
  5. package/android/src/main/AndroidManifest.xml +29 -0
  6. package/android/src/main/java/com/qpush/reactnative/QPushDeviceInfo.kt +96 -0
  7. package/android/src/main/java/com/qpush/reactnative/QPushEventBus.kt +153 -0
  8. package/android/src/main/java/com/qpush/reactnative/QPushFcm.kt +108 -0
  9. package/android/src/main/java/com/qpush/reactnative/QPushHttp.kt +92 -0
  10. package/android/src/main/java/com/qpush/reactnative/QPushImages.kt +103 -0
  11. package/android/src/main/java/com/qpush/reactnative/QPushLog.kt +39 -0
  12. package/android/src/main/java/com/qpush/reactnative/QPushMessagingService.kt +77 -0
  13. package/android/src/main/java/com/qpush/reactnative/QPushNotificationOpenActivity.kt +121 -0
  14. package/android/src/main/java/com/qpush/reactnative/QPushNotificationRenderer.kt +256 -0
  15. package/android/src/main/java/com/qpush/reactnative/QPushPayload.kt +114 -0
  16. package/android/src/main/java/com/qpush/reactnative/QPushPushModule.kt +368 -0
  17. package/android/src/main/java/com/qpush/reactnative/QPushPushPackage.kt +15 -0
  18. package/android/src/main/java/com/qpush/reactnative/QPushStore.kt +108 -0
  19. package/android/src/main/res/values/qpush_strings.xml +6 -0
  20. package/app.plugin.js +1 -0
  21. package/ios/Extension/QPushNotificationServiceExtension.swift +261 -0
  22. package/ios/Extension/QPushPushExtension.podspec +34 -0
  23. package/ios/QPushPush.m +62 -0
  24. package/ios/QPushPush.swift +186 -0
  25. package/ios/QPushPushBridge.h +5 -0
  26. package/ios/QPushPushCore.swift +457 -0
  27. package/ios/QPushPushHooks.m +237 -0
  28. package/ios/QPushPushSupport.swift +249 -0
  29. package/ios_nse_template/Info.plist +38 -0
  30. package/ios_nse_template/NotificationService.entitlements +10 -0
  31. package/ios_nse_template/NotificationService.swift +44 -0
  32. package/lib/NativeQPushPush.d.ts +44 -0
  33. package/lib/NativeQPushPush.js +4 -0
  34. package/lib/QPush.d.ts +117 -0
  35. package/lib/QPush.js +206 -0
  36. package/lib/core/QPushCore.d.ts +148 -0
  37. package/lib/core/QPushCore.js +1387 -0
  38. package/lib/core/constants.d.ts +37 -0
  39. package/lib/core/constants.js +40 -0
  40. package/lib/core/events.d.ts +11 -0
  41. package/lib/core/events.js +40 -0
  42. package/lib/core/http.d.ts +41 -0
  43. package/lib/core/http.js +123 -0
  44. package/lib/core/logger.d.ts +18 -0
  45. package/lib/core/logger.js +43 -0
  46. package/lib/core/operationQueue.d.ts +117 -0
  47. package/lib/core/operationQueue.js +264 -0
  48. package/lib/core/operations.d.ts +80 -0
  49. package/lib/core/operations.js +88 -0
  50. package/lib/core/payload.d.ts +20 -0
  51. package/lib/core/payload.js +187 -0
  52. package/lib/core/platform.d.ts +72 -0
  53. package/lib/core/platform.js +7 -0
  54. package/lib/core/state.d.ts +51 -0
  55. package/lib/core/state.js +74 -0
  56. package/lib/core/storage.d.ts +34 -0
  57. package/lib/core/storage.js +68 -0
  58. package/lib/core/subscription.d.ts +67 -0
  59. package/lib/core/subscription.js +98 -0
  60. package/lib/core/tags.d.ts +39 -0
  61. package/lib/core/tags.js +123 -0
  62. package/lib/core/url.d.ts +17 -0
  63. package/lib/core/url.js +64 -0
  64. package/lib/core/uuid.d.ts +2 -0
  65. package/lib/core/uuid.js +18 -0
  66. package/lib/index.d.ts +7 -0
  67. package/lib/index.js +15 -0
  68. package/lib/notification.d.ts +67 -0
  69. package/lib/notification.js +73 -0
  70. package/lib/reactNativePlatform.d.ts +3 -0
  71. package/lib/reactNativePlatform.js +164 -0
  72. package/lib/types.d.ts +157 -0
  73. package/lib/types.js +31 -0
  74. package/package.json +70 -4
  75. package/plugin/withQPush.js +330 -0
  76. package/react-native-qpush.podspec +30 -0
  77. package/react-native.config.js +18 -0
  78. package/src/NativeQPushPush.ts +57 -0
  79. package/src/QPush.ts +292 -0
  80. package/src/core/QPushCore.ts +1515 -0
  81. package/src/core/constants.ts +42 -0
  82. package/src/core/events.ts +34 -0
  83. package/src/core/http.ts +146 -0
  84. package/src/core/logger.ts +42 -0
  85. package/src/core/operationQueue.ts +324 -0
  86. package/src/core/operations.ts +166 -0
  87. package/src/core/payload.ts +175 -0
  88. package/src/core/platform.ts +84 -0
  89. package/src/core/state.ts +127 -0
  90. package/src/core/storage.ts +76 -0
  91. package/src/core/subscription.ts +133 -0
  92. package/src/core/tags.ts +116 -0
  93. package/src/core/url.ts +58 -0
  94. package/src/core/uuid.ts +13 -0
  95. package/src/index.ts +23 -0
  96. package/src/notification.ts +113 -0
  97. package/src/reactNativePlatform.ts +172 -0
  98. package/src/types.ts +174 -0
@@ -0,0 +1,256 @@
1
+ package com.qpush.reactnative
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.app.PendingIntent
6
+ import android.content.ContentResolver
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.graphics.Bitmap
10
+ import android.media.RingtoneManager
11
+ import android.net.Uri
12
+ import android.os.Build
13
+ import android.os.Bundle
14
+ import androidx.core.app.NotificationCompat
15
+ import androidx.core.app.NotificationManagerCompat
16
+ import java.util.Random
17
+
18
+ /** Builds and posts notifications for QPush payloads (NotificationCompat). */
19
+ internal object QPushNotificationRenderer {
20
+ const val DEFAULT_CHANNEL_ID = "qpush_default"
21
+ const val NOTIFICATION_TAG = "qpush"
22
+ const val EXTRA_QPUSH_ID = "qpush_id"
23
+
24
+ // Trampoline intent extras.
25
+ const val EXTRA_PAYLOAD = "qpush_payload"
26
+ const val EXTRA_ACTION_ID = "qpush_action_id"
27
+ const val EXTRA_NOTIFICATION_ID = "qpush_android_notification_id"
28
+
29
+ private const val DEFAULT_SMALL_ICON = "ic_stat_qpush_default"
30
+ private const val META_DEFAULT_ACCENT_COLOR = "com.qpush.NotificationAccentColor.DEFAULT"
31
+ private const val LARGE_ICON_SIZE = 256
32
+ private const val BIG_PICTURE_SIZE = 1440
33
+
34
+ private val random = Random()
35
+
36
+ /** Renders the notification. Blocking (downloads images): call off the main thread. */
37
+ fun render(context: Context, payload: QPushPayload) {
38
+ val appContext = context.applicationContext
39
+ if (payload.isSilent) {
40
+ QPushLog.d("Notification ${payload.id} has no visible content, not displayed")
41
+ return
42
+ }
43
+ val manager = NotificationManagerCompat.from(appContext)
44
+ if (!manager.areNotificationsEnabled()) {
45
+ QPushLog.i("Notifications are disabled, ${payload.id} not displayed")
46
+ return
47
+ }
48
+
49
+ val channelId = ensureChannel(appContext, payload.channelId)
50
+ val notificationId = payload.androidNotificationId
51
+ val builder = NotificationCompat.Builder(appContext, channelId)
52
+ .setSmallIcon(resolveSmallIcon(appContext, payload.smallIcon))
53
+ .setAutoCancel(true)
54
+ .setShowWhen(true)
55
+ .setWhen(System.currentTimeMillis())
56
+ .setPriority(mapPriority(payload.priority))
57
+ .setContentIntent(openIntent(appContext, payload, null, notificationId))
58
+ .addExtras(Bundle().apply { putString(EXTRA_QPUSH_ID, payload.id) })
59
+
60
+ payload.title?.let { builder.setContentTitle(it) }
61
+ payload.body?.let { builder.setContentText(it) }
62
+
63
+ resolveAccentColor(appContext, payload.accentColor)?.let { builder.setColor(it) }
64
+ mapVisibility(payload.visibility)?.let { builder.setVisibility(it) }
65
+ payload.group?.let { builder.setGroup(it) }
66
+
67
+ // Sound / vibration (pre-Oreo; on Android 8+ the channel decides).
68
+ val sound = resolveSound(appContext, payload.sound)
69
+ if (sound != null) {
70
+ builder.setSound(sound)
71
+ builder.setDefaults(NotificationCompat.DEFAULT_VIBRATE or NotificationCompat.DEFAULT_LIGHTS)
72
+ } else {
73
+ builder.setDefaults(NotificationCompat.DEFAULT_ALL)
74
+ }
75
+
76
+ val largeIcon: Bitmap? = QPushImages.load(appContext, payload.largeIcon, LARGE_ICON_SIZE)
77
+ largeIcon?.let { builder.setLargeIcon(it) }
78
+
79
+ val bigPicture: Bitmap? = payload.image?.let { QPushImages.load(appContext, it, BIG_PICTURE_SIZE) }
80
+ if (bigPicture != null) {
81
+ val style = NotificationCompat.BigPictureStyle().bigPicture(bigPicture)
82
+ payload.title?.let { style.setBigContentTitle(it) }
83
+ payload.body?.let { style.setSummaryText(it) }
84
+ if (largeIcon != null) style.bigLargeIcon(null as Bitmap?)
85
+ builder.setStyle(style)
86
+ if (largeIcon == null) builder.setLargeIcon(bigPicture)
87
+ } else if (!payload.body.isNullOrEmpty()) {
88
+ builder.setStyle(NotificationCompat.BigTextStyle().bigText(payload.body))
89
+ }
90
+
91
+ for (button in payload.buttons.take(3)) {
92
+ val icon = QPushImages.resourceId(appContext, button.icon)
93
+ builder.addAction(
94
+ NotificationCompat.Action.Builder(icon, button.text, openIntent(appContext, payload, button.id, notificationId))
95
+ .build(),
96
+ )
97
+ }
98
+
99
+ try {
100
+ manager.notify(NOTIFICATION_TAG, notificationId, builder.build())
101
+ payload.group?.let { postGroupSummary(appContext, it, channelId, payload) }
102
+ QPushLog.d("Displayed notification ${payload.id}")
103
+ } catch (e: SecurityException) {
104
+ // POST_NOTIFICATIONS revoked between the check and notify().
105
+ QPushLog.w("Not allowed to post notifications", e)
106
+ }
107
+ }
108
+
109
+ /** Android 7+ only groups notifications that have a summary. */
110
+ private fun postGroupSummary(context: Context, group: String, channelId: String, payload: QPushPayload) {
111
+ if (Build.VERSION.SDK_INT < 24) return
112
+ val summary = NotificationCompat.Builder(context, channelId)
113
+ .setSmallIcon(resolveSmallIcon(context, payload.smallIcon))
114
+ .setGroup(group)
115
+ .setGroupSummary(true)
116
+ .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
117
+ .setAutoCancel(true)
118
+ .setContentIntent(openIntent(context, null, null, null))
119
+ resolveAccentColor(context, payload.accentColor)?.let { summary.setColor(it) }
120
+ try {
121
+ NotificationManagerCompat.from(context).notify(NOTIFICATION_TAG, groupSummaryId(group), summary.build())
122
+ } catch (e: SecurityException) {
123
+ QPushLog.w("Not allowed to post the group summary", e)
124
+ }
125
+ }
126
+
127
+ fun groupSummaryId(group: String): Int = "group:$group".hashCode()
128
+
129
+ private fun openIntent(context: Context, payload: QPushPayload?, actionId: String?, notificationId: Int?): PendingIntent {
130
+ val intent = Intent(context, QPushNotificationOpenActivity::class.java).apply {
131
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION)
132
+ payload?.let { putExtra(EXTRA_PAYLOAD, it.raw) }
133
+ actionId?.let { putExtra(EXTRA_ACTION_ID, it) }
134
+ notificationId?.let { putExtra(EXTRA_NOTIFICATION_ID, it) }
135
+ }
136
+ var flags = PendingIntent.FLAG_UPDATE_CURRENT
137
+ if (Build.VERSION.SDK_INT >= 23) flags = flags or PendingIntent.FLAG_IMMUTABLE
138
+ return PendingIntent.getActivity(context, random.nextInt(), intent, flags)
139
+ }
140
+
141
+ /** Returns a usable channel id, creating `qpush_default` when needed. */
142
+ fun ensureChannel(context: Context, requested: String?): String {
143
+ if (Build.VERSION.SDK_INT < 26) return requested ?: DEFAULT_CHANNEL_ID
144
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
145
+ if (requested != null) {
146
+ if (manager.getNotificationChannel(requested) != null) return requested
147
+ QPushLog.w("Notification channel \"$requested\" does not exist, using $DEFAULT_CHANNEL_ID")
148
+ }
149
+ if (manager.getNotificationChannel(DEFAULT_CHANNEL_ID) == null) {
150
+ val channel = NotificationChannel(
151
+ DEFAULT_CHANNEL_ID,
152
+ context.getString(R.string.qpush_default_channel_name),
153
+ NotificationManager.IMPORTANCE_HIGH,
154
+ )
155
+ channel.enableVibration(true)
156
+ manager.createNotificationChannel(channel)
157
+ }
158
+ return DEFAULT_CHANNEL_ID
159
+ }
160
+
161
+ /** Payload icon -> `ic_stat_qpush_default` -> app icon. */
162
+ fun resolveSmallIcon(context: Context, name: String?): Int {
163
+ QPushImages.resourceId(context, name).takeIf { it != 0 }?.let { return it }
164
+ QPushImages.resourceId(context, DEFAULT_SMALL_ICON).takeIf { it != 0 }?.let { return it }
165
+ val appIcon = context.applicationInfo.icon
166
+ return if (appIcon != 0) appIcon else android.R.drawable.ic_dialog_info
167
+ }
168
+
169
+ /** `FF0070F3`, `#0070F3`, ... -> ARGB int. */
170
+ fun parseColor(value: String?): Int? {
171
+ if (value.isNullOrBlank()) return null
172
+ var hex = value.trim().removePrefix("#")
173
+ if (hex.length == 6) hex = "FF$hex"
174
+ if (hex.length != 8) return null
175
+ return hex.toLongOrNull(16)?.toInt()
176
+ }
177
+
178
+ private fun resolveAccentColor(context: Context, value: String?): Int? =
179
+ parseColor(value) ?: parseColor(QPushStore.manifestString(context, META_DEFAULT_ACCENT_COLOR))
180
+
181
+ private fun resolveSound(context: Context, name: String?): Uri? {
182
+ if (name.isNullOrBlank()) return null
183
+ if (name.equals("default", ignoreCase = true)) {
184
+ return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
185
+ }
186
+ val resourceName = name.substringBeforeLast('.')
187
+ @Suppress("DiscouragedApi")
188
+ val id = context.resources.getIdentifier(resourceName, "raw", context.packageName)
189
+ if (id == 0) return null
190
+ return Uri.parse("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${context.packageName}/$id")
191
+ }
192
+
193
+ /** 10 -> high, 5 -> default (protocol). */
194
+ private fun mapPriority(priority: Int?): Int = when {
195
+ priority == null -> NotificationCompat.PRIORITY_HIGH
196
+ priority >= 9 -> NotificationCompat.PRIORITY_HIGH
197
+ priority >= 5 -> NotificationCompat.PRIORITY_DEFAULT
198
+ priority >= 1 -> NotificationCompat.PRIORITY_LOW
199
+ else -> NotificationCompat.PRIORITY_MIN
200
+ }
201
+
202
+ /** 1 public, 0 private, -1 secret. */
203
+ private fun mapVisibility(visibility: Int?): Int? = when (visibility) {
204
+ 1 -> NotificationCompat.VISIBILITY_PUBLIC
205
+ 0 -> NotificationCompat.VISIBILITY_PRIVATE
206
+ -1 -> NotificationCompat.VISIBILITY_SECRET
207
+ else -> null
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Removal
212
+ // ---------------------------------------------------------------------------
213
+
214
+ fun clearAll(context: Context) {
215
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
216
+ if (Build.VERSION.SDK_INT >= 23) {
217
+ for (notification in manager.activeNotifications) {
218
+ if (notification.tag == NOTIFICATION_TAG) manager.cancel(notification.tag, notification.id)
219
+ }
220
+ } else {
221
+ manager.cancelAll()
222
+ }
223
+ }
224
+
225
+ fun remove(context: Context, qpushId: String) {
226
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
227
+ if (Build.VERSION.SDK_INT >= 23) {
228
+ for (notification in manager.activeNotifications) {
229
+ if (notification.tag == NOTIFICATION_TAG &&
230
+ notification.notification.extras?.getString(EXTRA_QPUSH_ID) == qpushId
231
+ ) {
232
+ manager.cancel(notification.tag, notification.id)
233
+ }
234
+ }
235
+ } else {
236
+ manager.cancel(NOTIFICATION_TAG, qpushId.hashCode())
237
+ }
238
+ }
239
+
240
+ fun removeGroup(context: Context, group: String) {
241
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
242
+ if (Build.VERSION.SDK_INT >= 23) {
243
+ for (notification in manager.activeNotifications) {
244
+ if (notification.tag == NOTIFICATION_TAG && notification.notification.group == group) {
245
+ manager.cancel(notification.tag, notification.id)
246
+ }
247
+ }
248
+ } else {
249
+ manager.cancel(NOTIFICATION_TAG, groupSummaryId(group))
250
+ }
251
+ }
252
+
253
+ fun cancel(context: Context, notificationId: Int) {
254
+ NotificationManagerCompat.from(context).cancel(NOTIFICATION_TAG, notificationId)
255
+ }
256
+ }
@@ -0,0 +1,114 @@
1
+ package com.qpush.reactnative
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+
6
+ /** Action button of a QPush notification. */
7
+ internal data class QPushButton(
8
+ val id: String,
9
+ val text: String,
10
+ val icon: String?,
11
+ val url: String?,
12
+ )
13
+
14
+ /**
15
+ * Decoded `data["qpush"]` JSON (see docs/sdk-protocol.md, "Push payloads").
16
+ * Missing keys are `null` and mean "use the default".
17
+ */
18
+ internal data class QPushPayload(
19
+ val id: String,
20
+ val title: String?,
21
+ val body: String?,
22
+ val image: String?,
23
+ val largeIcon: String?,
24
+ val smallIcon: String?,
25
+ val accentColor: String?,
26
+ val sound: String?,
27
+ val channelId: String?,
28
+ val priority: Int?,
29
+ val group: String?,
30
+ val collapseId: String?,
31
+ val visibility: Int?,
32
+ val url: String?,
33
+ val buttons: List<QPushButton>,
34
+ /** The original JSON string, forwarded untouched to JS. */
35
+ val raw: String,
36
+ ) {
37
+ /** Data-only payload without visible content (nothing to render). */
38
+ val isSilent: Boolean
39
+ get() = title.isNullOrEmpty() && body.isNullOrEmpty() && image.isNullOrEmpty()
40
+
41
+ /** Id used with NotificationManager: notifications sharing a collapse_id replace each other. */
42
+ val androidNotificationId: Int
43
+ get() = (collapseId ?: id).hashCode()
44
+
45
+ fun buttonUrl(actionId: String?): String? =
46
+ if (actionId == null) null else buttons.firstOrNull { it.id == actionId }?.url
47
+
48
+ companion object {
49
+ fun parse(raw: String?): QPushPayload? {
50
+ if (raw.isNullOrEmpty()) return null
51
+ val json = try {
52
+ JSONObject(raw)
53
+ } catch (e: Exception) {
54
+ QPushLog.w("Invalid qpush payload", e)
55
+ return null
56
+ }
57
+ val id = json.string("id") ?: return null
58
+ return QPushPayload(
59
+ id = id,
60
+ title = json.string("title"),
61
+ body = json.string("body"),
62
+ image = json.string("image"),
63
+ largeIcon = json.string("large_icon"),
64
+ smallIcon = json.string("small_icon"),
65
+ accentColor = json.string("accent_color"),
66
+ sound = json.string("sound"),
67
+ channelId = json.string("channel_id"),
68
+ priority = json.int("priority"),
69
+ group = json.string("group"),
70
+ collapseId = json.string("collapse_id"),
71
+ visibility = json.int("visibility"),
72
+ url = json.string("url"),
73
+ buttons = parseButtons(json.opt("buttons")),
74
+ raw = raw,
75
+ )
76
+ }
77
+
78
+ private fun parseButtons(value: Any?): List<QPushButton> {
79
+ val array = when (value) {
80
+ is JSONArray -> value
81
+ is String -> try {
82
+ JSONArray(value)
83
+ } catch (e: Exception) {
84
+ return emptyList()
85
+ }
86
+ else -> return emptyList()
87
+ }
88
+ val buttons = ArrayList<QPushButton>()
89
+ for (i in 0 until array.length()) {
90
+ val entry = array.optJSONObject(i) ?: continue
91
+ val id = entry.string("id") ?: continue
92
+ val text = entry.string("text") ?: continue
93
+ buttons.add(QPushButton(id, text, entry.string("icon"), entry.string("url")))
94
+ }
95
+ return buttons
96
+ }
97
+
98
+ /** `optString` returns "null" for JSON null and "" for missing keys; normalize both to null. */
99
+ private fun JSONObject.string(key: String): String? {
100
+ if (!has(key) || isNull(key)) return null
101
+ val value = opt(key)?.toString() ?: return null
102
+ return if (value.isEmpty()) null else value
103
+ }
104
+
105
+ private fun JSONObject.int(key: String): Int? {
106
+ if (!has(key) || isNull(key)) return null
107
+ return when (val value = opt(key)) {
108
+ is Number -> value.toInt()
109
+ is String -> value.trim().toDoubleOrNull()?.toInt()
110
+ else -> null
111
+ }
112
+ }
113
+ }
114
+ }