react-native-notify-kit 10.6.0 → 10.7.1
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.
- package/README.md +50 -0
- package/dist/fcm/reconstructNotification.js +7 -0
- package/dist/fcm/reconstructNotification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
- package/plugin/build/android/withNotifyKitAndroidNotificationIcons.js +498 -0
- package/plugin/build/index.js +13 -18
- package/plugin/build/options.js +84 -0
- package/server/README.md +4 -0
- package/src/fcm/reconstructNotification.ts +7 -0
- package/src/internal/fcmContract.d.ts +2 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -242,6 +242,54 @@ export default {
|
|
|
242
242
|
|
|
243
243
|
On Android, FCM Mode stays data-only. Configure Firebase and RNFirebase in the app, create the Android channel used by your payloads, then call `notifee.handleFcmMessage(remoteMessage)` from RNFirebase `onMessage` and `setBackgroundMessageHandler`. The Expo smoke app validates foreground and background delivery with the `android-expo-smoke` scenario on a real device.
|
|
244
244
|
|
|
245
|
+
#### Android notification small icons
|
|
246
|
+
|
|
247
|
+
For Expo CNG/prebuild projects, the config plugin can generate small notification icon resources without manually editing generated `android/` directories. This resource-generation option is independent of FCM Mode:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
export default {
|
|
251
|
+
expo: {
|
|
252
|
+
plugins: [
|
|
253
|
+
[
|
|
254
|
+
'react-native-notify-kit',
|
|
255
|
+
{
|
|
256
|
+
android: {
|
|
257
|
+
icons: [
|
|
258
|
+
{
|
|
259
|
+
name: 'notification_message',
|
|
260
|
+
path: './assets/notification-message.png',
|
|
261
|
+
type: 'small',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'notification_warning',
|
|
265
|
+
path: './assets/notification-warning.png',
|
|
266
|
+
type: 'small',
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
During prebuild, each entry generates density-specific PNG drawable resources (`mdpi` 24×24, `hdpi` 36×36, `xhdpi` 48×48, `xxhdpi` 72×72, and `xxxhdpi` 96×96) with a transparent background and `cover` resizing. The source `path` must be a project-relative PNG inside the project root. The `name` must start with a lowercase letter and contain only `a-z`, `0-9`, and `_`; invalid names are rejected rather than normalized. The array supports multiple icons and preserves their order. Each entry must include `type: 'small'`; omitting it or using another type fails during configuration/prebuild. The plugin also writes an Android resource-shrinker keep rule for the generated icons.
|
|
278
|
+
|
|
279
|
+
`android.icons` generates resources; it does not select an icon for a notification. Pass the configured resource `name`—not its file path—to `android.smallIcon`:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
await notifee.displayNotification({
|
|
283
|
+
title: 'New message',
|
|
284
|
+
android: {
|
|
285
|
+
channelId: 'messages',
|
|
286
|
+
smallIcon: 'notification_message',
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
This option does not configure Firebase or set `com.google.firebase.messaging.default_notification_icon`.
|
|
292
|
+
|
|
245
293
|
Android foreground service manifest configuration is also available through the NotifyKit config plugin, but it is explicit opt-in:
|
|
246
294
|
|
|
247
295
|
```ts
|
|
@@ -926,6 +974,8 @@ Three causes account for the vast majority of reports:
|
|
|
926
974
|
|
|
927
975
|
**Naming mismatch.** Android resource names are case-sensitive and only accept `[a-z0-9_]`. A string like `smallIcon: 'icNotification'` will not resolve `ic_notification.png`. Rename the `smallIcon` value to match the file on disk exactly.
|
|
928
976
|
|
|
977
|
+
For Expo CNG/prebuild apps, use the NotifyKit config plugin's `android.icons` option instead of editing generated `android/` directories. It generates the density-specific drawables and writes a resource-shrinker keep rule during prebuild; continue to pass the configured resource `name` to `android.smallIcon`.
|
|
978
|
+
|
|
929
979
|
For the full procedure on creating a small-icon asset via Android Studio, see [docs/react-native/android/appearance.mdx](docs/react-native/android/appearance.mdx).
|
|
930
980
|
|
|
931
981
|
#### Custom sounds for push notifications in background or killed state
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { AndroidStyle } from '../types/NotificationAndroid';
|
|
6
6
|
import { isAndroid, isIOS } from '../utils';
|
|
7
|
+
import { isValidTimestamp } from '../validators/validate';
|
|
7
8
|
const PREFIX = '[react-native-notify-kit]';
|
|
8
9
|
const RESERVED_DATA_KEYS = new Set(['notifee_options', 'notifee_data']);
|
|
9
10
|
const VALID_INTERRUPTION_LEVELS = new Set(['active', 'critical', 'passive', 'timeSensitive']);
|
|
@@ -92,6 +93,12 @@ function buildAndroidConfig(raw, config) {
|
|
|
92
93
|
android.largeIcon = raw.largeIcon;
|
|
93
94
|
if (typeof raw?.color === 'string')
|
|
94
95
|
android.color = raw.color;
|
|
96
|
+
// Timestamp fields — preserve only values accepted by the canonical Android validator
|
|
97
|
+
if (typeof raw?.showTimestamp === 'boolean')
|
|
98
|
+
android.showTimestamp = raw.showTimestamp;
|
|
99
|
+
if (typeof raw?.timestamp === 'number' && isValidTimestamp(raw.timestamp)) {
|
|
100
|
+
android.timestamp = raw.timestamp;
|
|
101
|
+
}
|
|
95
102
|
// Actions array — pass through (trust server validation)
|
|
96
103
|
if (Array.isArray(raw?.actions)) {
|
|
97
104
|
android.actions = raw.actions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconstructNotification.js","sourceRoot":"","sources":["../../src/fcm/reconstructNotification.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAuB,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEjF,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"reconstructNotification.js","sourceRoot":"","sources":["../../src/fcm/reconstructNotification.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAuB,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEjF,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAI1D,MAAM,MAAM,GAAG,2BAA2B,CAAC;AAC3C,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC,CAAC;AAExE,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC,CAAC;AAE9F,MAAM,cAAc,GAA6C;IAC/D,QAAQ,EAAE,YAAY,CAAC,OAAO;IAC9B,WAAW,EAAE,YAAY,CAAC,UAAU;CACrC,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAA4B,EAC5B,aAA+B,EAC/B,MAAiB;IAEjB,MAAM,IAAI,GAAG,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAE7C,MAAM,KAAK,GACT,MAAM,EAAE,KAAK,IAAI,aAAa,CAAC,YAAY,EAAE,KAAK,IAAI,aAAa,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;IAExF,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,aAAa,CAAC,YAAY,EAAE,IAAI,IAAI,aAAa,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC;IAEhG,MAAM,EAAE,GACJ,MAAyC,EAAE,EAAyB;QACtE,aAAa,CAAC,SAAS,CAAC;IAE1B,MAAM,YAAY,GAAiB;QACjC,GAAG,CAAC,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC;QAClD,KAAK;QACL,IAAI;KACL,CAAC;IAEF,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC;IAC3B,CAAC;IAED,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,MAAM,CAAC,gBAAgB,IAAI,MAAM,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAC3F,YAAY,CAAC,OAAO,GAAG,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,KAAK,IAAI,MAAM,EAAE,GAAG,EAAE,CAAC;QACzB,YAAY,CAAC,GAAG,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,8EAA8E;AAC9E,sCAAsC;AACtC,8EAA8E;AAE9E,SAAS,WAAW,CAAC,OAA2C;IAC9D,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,sDAAsD;IACtD,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACtB,CAAC;IACH,CAAC;IAED,sEAAsE;IACtE,MAAM,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAC5C,IAAI,OAAO,cAAc,KAAK,QAAQ,EAAE,CAAC;QACvC,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAA2B,CAAC;YACzE,IAAI,WAAW,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;gBACnD,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,gEAAgE,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IAED,kEAAkE;IAClE,KAAK,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACrC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E,SAAS,kBAAkB,CACzB,GAAwC,EACxC,MAAiB;IAEjB,MAAM,OAAO,GAAwB,EAAE,CAAC;IAExC,sCAAsC;IACtC,MAAM,SAAS,GAAG,GAAG,EAAE,SAAS,IAAI,MAAM,CAAC,gBAAgB,CAAC;IAC5D,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAChC,CAAC;IAED,wCAAwC;IACxC,MAAM,WAAW,GAAG,GAAG,EAAE,WAAW,IAAI,MAAM,CAAC,kBAAkB,CAAC;IAClE,IAAI,WAAW,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACnD,OAAO,CAAC,WAAW,GAAG,WAAiD,CAAC;IAC1E,CAAC;IAED,uBAAuB;IACvB,IAAI,OAAO,GAAG,EAAE,SAAS,KAAK,QAAQ;QAAE,OAAO,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IAC1E,IAAI,OAAO,GAAG,EAAE,SAAS,KAAK,QAAQ;QAAE,OAAO,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IAC1E,IAAI,OAAO,GAAG,EAAE,KAAK,KAAK,QAAQ;QAAE,OAAO,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;IAE9D,sFAAsF;IACtF,IAAI,OAAO,GAAG,EAAE,aAAa,KAAK,SAAS;QAAE,OAAO,CAAC,aAAa,GAAG,GAAG,CAAC,aAAa,CAAC;IACvF,IAAI,OAAO,GAAG,EAAE,SAAS,KAAK,QAAQ,IAAI,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1E,OAAO,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IACpC,CAAC;IAED,yDAAyD;IACzD,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC;QAChC,OAAO,CAAC,OAAO,GAAG,GAAG,CAAC,OAAyC,CAAC;IAClE,CAAC;IAED,+DAA+D;IAC/D,IAAI,GAAG,EAAE,KAAK,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,KAAK,GAAG,GAAG,CAAC,KAA2D,CAAC;QAC9E,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACnC,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC9C,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;gBAC7B,IAAI,UAAU,KAAK,YAAY,CAAC,OAAO,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC1E,OAAO,CAAC,KAAK,GAAG,EAAE,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnE,CAAC;qBAAM,IAAI,UAAU,KAAK,YAAY,CAAC,UAAU,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;oBACvF,OAAO,CAAC,KAAK,GAAG,EAAE,IAAI,EAAE,YAAY,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;gBAC5E,CAAC;qBAAM,CAAC;oBACN,MAAM,KAAK,GAAG,UAAU,KAAK,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;oBACvE,OAAO,CAAC,IAAI,CACV,GAAG,MAAM,wBAAwB,KAAK,CAAC,IAAI,2BAA2B,KAAK,iDAAiD,CAC7H,CAAC;gBACJ,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,gCAAgC,KAAK,CAAC,IAAI,mBAAmB,CAAC,CAAC;YACvF,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E,SAAS,cAAc,CAAC,GAA4B;IAClD,MAAM,GAAG,GAAoB,EAAE,CAAC;IAEhC,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;QAAE,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;IACzD,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ;QAAE,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC;IACxE,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ;QAAE,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;IAElE,0DAA0D;IAC1D,IAAI,OAAO,GAAG,CAAC,iBAAiB,KAAK,QAAQ,EAAE,CAAC;QAC9C,IAAI,yBAAyB,CAAC,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACzD,GAAG,CAAC,iBAAiB,GAAG,GAAG,CAAC,iBAAyD,CAAC;QACxF,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,mCAAmC,GAAG,CAAC,iBAAiB,aAAa,CAAC,CAAC;QAC/F,CAAC;IACH,CAAC;IAED,mEAAmE;IACnE,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;QACnC,GAAG,CAAC,WAAW,GAAI,GAAG,CAAC,WAA8B;aAClD,MAAM,CAAC,CAAC,GAAG,EAAkC,EAAE;YAC9C,IAAI,GAAG,IAAI,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,OAAO,KAAK,CAAC;YACzD,MAAM,CAAC,GAAG,GAA8B,CAAC;YACzC,IAAI,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACpD,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,2DAA2D,CAAC,CAAC;gBACnF,OAAO,KAAK,CAAC;YACf,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;aACD,GAAG,CAAC,GAAG,CAAC,EAAE;YACT,MAAM,MAAM,GAAiC;gBAC3C,GAAG,EAAE,GAAG,CAAC,GAAa;aACvB,CAAC;YACF,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;gBACvC,MAAM,CAAC,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;YAC7B,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACP,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "10.
|
|
1
|
+
export declare const version = "10.7.1";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-notify-kit",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.7.1",
|
|
4
4
|
"author": "Marco Crupi",
|
|
5
5
|
"description": "Maintained Notifee-compatible React Native notifications library for Android, iOS, FCM Mode, rich notifications, foreground services, and Expo CNG development builds.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"format:ios": "npm_config_yes=true npx clang-format -i --glob=\"ios/**/*.{h,cpp,m,mm}\" --style=Google",
|
|
41
41
|
"format:ios:check": "npm_config_yes=true npx clang-format --glob=\"ios/**/*.{h,cpp,m,mm}\" --style=Google -n -Werror"
|
|
42
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@expo/image-utils": "^0.8.14"
|
|
45
|
+
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@babel/preset-env": "^7.25.3",
|
|
45
48
|
"@babel/runtime": "^7.25.0",
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { generateImageAsync } = require('@expo/image-utils');
|
|
6
|
+
|
|
7
|
+
const { normalizeAndroidNotificationIcons } = require('../options');
|
|
8
|
+
|
|
9
|
+
const ANDROID_NOTIFICATION_ICON_DENSITIES = [
|
|
10
|
+
{ density: 'mdpi', size: 24 },
|
|
11
|
+
{ density: 'hdpi', size: 36 },
|
|
12
|
+
{ density: 'xhdpi', size: 48 },
|
|
13
|
+
{ density: 'xxhdpi', size: 72 },
|
|
14
|
+
{ density: 'xxxhdpi', size: 96 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const NOTIFY_KIT_KEEP_FILE_NAME = 'notifykit_keep.xml';
|
|
18
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
19
|
+
|
|
20
|
+
function withNotifyKitAndroidNotificationIcons(config, icons) {
|
|
21
|
+
if (icons.length === 0) {
|
|
22
|
+
return config;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { withDangerousMod } = requireExpoConfigPlugins();
|
|
26
|
+
|
|
27
|
+
return withDangerousMod(config, [
|
|
28
|
+
'android',
|
|
29
|
+
async modConfig => {
|
|
30
|
+
await generateNotifyKitAndroidNotificationIconResources(
|
|
31
|
+
modConfig.modRequest.projectRoot,
|
|
32
|
+
modConfig.modRequest.platformProjectRoot,
|
|
33
|
+
icons,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return modConfig;
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function generateNotifyKitAndroidNotificationIconResources(
|
|
42
|
+
projectRoot,
|
|
43
|
+
platformProjectRoot,
|
|
44
|
+
icons,
|
|
45
|
+
) {
|
|
46
|
+
const normalizedIcons = normalizeAndroidNotificationIcons(icons);
|
|
47
|
+
|
|
48
|
+
if (normalizedIcons.length === 0) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const preparedIcons = normalizedIcons.map(icon => {
|
|
53
|
+
const sourcePath = resolveAndroidNotificationIconSource(projectRoot, icon);
|
|
54
|
+
validateAndroidNotificationIconPngSignature(icon, sourcePath);
|
|
55
|
+
|
|
56
|
+
return { icon, sourcePath };
|
|
57
|
+
});
|
|
58
|
+
const { resolvedPlatformProjectRoot, canonicalPlatformProjectRoot } =
|
|
59
|
+
resolveAndroidPlatformProjectRoot(platformProjectRoot);
|
|
60
|
+
const resourceRoot = path.join(resolvedPlatformProjectRoot, 'app', 'src', 'main', 'res');
|
|
61
|
+
const outputPaths = getAndroidNotificationIconOutputPaths(resourceRoot, preparedIcons);
|
|
62
|
+
|
|
63
|
+
validateAndroidOutputDestinations(
|
|
64
|
+
resolvedPlatformProjectRoot,
|
|
65
|
+
canonicalPlatformProjectRoot,
|
|
66
|
+
outputPaths,
|
|
67
|
+
);
|
|
68
|
+
validateAndroidResourceCollisions(resolvedPlatformProjectRoot, resourceRoot, preparedIcons);
|
|
69
|
+
validateKeepFileDestination(resourceRoot);
|
|
70
|
+
|
|
71
|
+
const generatedIcons = await generateAllAndroidNotificationIconBuffers(
|
|
72
|
+
projectRoot,
|
|
73
|
+
resourceRoot,
|
|
74
|
+
preparedIcons,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
prepareAndroidOutputDirectories(
|
|
78
|
+
resolvedPlatformProjectRoot,
|
|
79
|
+
canonicalPlatformProjectRoot,
|
|
80
|
+
outputPaths,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
for (const generatedIcon of generatedIcons) {
|
|
84
|
+
fs.writeFileSync(generatedIcon.outputPath, generatedIcon.source);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const keepFilePath = path.join(resourceRoot, 'raw', NOTIFY_KIT_KEEP_FILE_NAME);
|
|
88
|
+
fs.writeFileSync(keepFilePath, renderNotifyKitAndroidNotificationIconsKeepXml(normalizedIcons));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderNotifyKitAndroidNotificationIconsKeepXml(icons) {
|
|
92
|
+
const resources = icons.map(icon => `@drawable/${icon.name}`).join(',');
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
'<?xml version="1.0" encoding="utf-8"?>\n' +
|
|
96
|
+
'<resources xmlns:tools="http://schemas.android.com/tools"\n' +
|
|
97
|
+
` tools:keep="${resources}" />\n`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveAndroidNotificationIconSource(projectRoot, icon) {
|
|
102
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
103
|
+
const resolvedSourcePath = path.resolve(resolvedProjectRoot, icon.path);
|
|
104
|
+
|
|
105
|
+
if (!isPathInside(resolvedProjectRoot, resolvedSourcePath)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`[react-native-notify-kit] android.icons source '${icon.path}' for '${icon.name}' ` +
|
|
108
|
+
'resolves outside the Expo project root.',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(resolvedSourcePath)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`[react-native-notify-kit] android.icons source does not exist for '${icon.name}': ${resolvedSourcePath}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let sourceStats;
|
|
119
|
+
try {
|
|
120
|
+
sourceStats = fs.statSync(resolvedSourcePath);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`[react-native-notify-kit] Unable to inspect android.icons source for '${icon.name}': ` +
|
|
124
|
+
`${resolvedSourcePath}. ${formatError(error)}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!sourceStats.isFile()) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`[react-native-notify-kit] android.icons source for '${icon.name}' must be a file: ${resolvedSourcePath}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
fs.accessSync(resolvedSourcePath, fs.constants.R_OK);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`[react-native-notify-kit] android.icons source for '${icon.name}' is not readable: ` +
|
|
139
|
+
`${resolvedSourcePath}. ${formatError(error)}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const realProjectRoot = fs.realpathSync(resolvedProjectRoot);
|
|
144
|
+
const realSourcePath = fs.realpathSync(resolvedSourcePath);
|
|
145
|
+
if (!isPathInside(realProjectRoot, realSourcePath)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`[react-native-notify-kit] android.icons source '${icon.path}' for '${icon.name}' ` +
|
|
148
|
+
'resolves outside the Expo project root through a symbolic link.',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return resolvedSourcePath;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validateAndroidNotificationIconPngSignature(icon, sourcePath) {
|
|
156
|
+
let sourceHeader;
|
|
157
|
+
try {
|
|
158
|
+
sourceHeader = fs.readFileSync(sourcePath).subarray(0, PNG_SIGNATURE.length);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`[react-native-notify-kit] Unable to read android.icons source content for '${icon.name}' ` +
|
|
162
|
+
`from '${icon.path}' (${sourcePath}): ${formatError(error)}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (sourceHeader.length !== PNG_SIGNATURE.length || !sourceHeader.equals(PNG_SIGNATURE)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`[react-native-notify-kit] android.icons source content for '${icon.name}' from ` +
|
|
169
|
+
`'${icon.path}' (${sourcePath}) is not a valid PNG: the PNG signature is missing.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveAndroidPlatformProjectRoot(platformProjectRoot) {
|
|
175
|
+
const resolvedPlatformProjectRoot = path.resolve(platformProjectRoot);
|
|
176
|
+
let platformProjectRootStats;
|
|
177
|
+
let canonicalPlatformProjectRoot;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
platformProjectRootStats = fs.statSync(resolvedPlatformProjectRoot);
|
|
181
|
+
canonicalPlatformProjectRoot = fs.realpathSync(resolvedPlatformProjectRoot);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`[react-native-notify-kit] Unable to inspect Android platform project root ` +
|
|
185
|
+
`${resolvedPlatformProjectRoot}: ${formatError(error)}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!platformProjectRootStats.isDirectory()) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`[react-native-notify-kit] Android platform project root is not a directory: ` +
|
|
192
|
+
resolvedPlatformProjectRoot,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { resolvedPlatformProjectRoot, canonicalPlatformProjectRoot };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getAndroidNotificationIconOutputPaths(resourceRoot, preparedIcons) {
|
|
200
|
+
const outputPaths = preparedIcons.flatMap(({ icon }) =>
|
|
201
|
+
ANDROID_NOTIFICATION_ICON_DENSITIES.map(({ density }) =>
|
|
202
|
+
path.join(resourceRoot, `drawable-${density}`, `${icon.name}.png`),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
outputPaths.push(path.join(resourceRoot, 'raw', NOTIFY_KIT_KEEP_FILE_NAME));
|
|
207
|
+
return outputPaths;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateAndroidOutputDestinations(
|
|
211
|
+
resolvedPlatformProjectRoot,
|
|
212
|
+
canonicalPlatformProjectRoot,
|
|
213
|
+
outputPaths,
|
|
214
|
+
) {
|
|
215
|
+
for (const outputPath of outputPaths) {
|
|
216
|
+
validateAndroidOutputDestination(
|
|
217
|
+
resolvedPlatformProjectRoot,
|
|
218
|
+
canonicalPlatformProjectRoot,
|
|
219
|
+
outputPath,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function validateAndroidOutputDestination(
|
|
225
|
+
resolvedPlatformProjectRoot,
|
|
226
|
+
canonicalPlatformProjectRoot,
|
|
227
|
+
outputPath,
|
|
228
|
+
) {
|
|
229
|
+
const resolvedOutputPath = path.resolve(outputPath);
|
|
230
|
+
if (!isPathInside(resolvedPlatformProjectRoot, resolvedOutputPath)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`[react-native-notify-kit] Android output destination resolves outside the platform ` +
|
|
233
|
+
`project root: ${resolvedOutputPath}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const relativeOutputPath = path.relative(resolvedPlatformProjectRoot, resolvedOutputPath);
|
|
238
|
+
const pathComponents = relativeOutputPath.split(path.sep).filter(Boolean);
|
|
239
|
+
let currentPath = resolvedPlatformProjectRoot;
|
|
240
|
+
|
|
241
|
+
for (const [index, pathComponent] of pathComponents.entries()) {
|
|
242
|
+
currentPath = path.join(currentPath, pathComponent);
|
|
243
|
+
const currentStats = lstatAndroidOutputPathIfExists(currentPath);
|
|
244
|
+
if (currentStats === undefined) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (currentStats.isSymbolicLink()) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`[react-native-notify-kit] Android output destination must not contain a symbolic link: ` +
|
|
251
|
+
currentPath,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const isFinalComponent = index === pathComponents.length - 1;
|
|
256
|
+
if (!isFinalComponent && !currentStats.isDirectory()) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`[react-native-notify-kit] Android output destination path component is not a directory: ` +
|
|
259
|
+
currentPath,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (currentStats.isDirectory()) {
|
|
264
|
+
let canonicalDirectoryPath;
|
|
265
|
+
try {
|
|
266
|
+
canonicalDirectoryPath = fs.realpathSync(currentPath);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`[react-native-notify-kit] Unable to resolve Android output directory ${currentPath}: ` +
|
|
270
|
+
formatError(error),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!isPathInside(canonicalPlatformProjectRoot, canonicalDirectoryPath)) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`[react-native-notify-kit] Android output directory resolves outside the canonical ` +
|
|
277
|
+
`platform project root: ${currentPath}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function lstatAndroidOutputPathIfExists(outputPath) {
|
|
285
|
+
try {
|
|
286
|
+
return fs.lstatSync(outputPath);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (error.code === 'ENOENT') {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw new Error(
|
|
293
|
+
`[react-native-notify-kit] Unable to inspect Android output destination ${outputPath}: ` +
|
|
294
|
+
formatError(error),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function prepareAndroidOutputDirectories(
|
|
300
|
+
resolvedPlatformProjectRoot,
|
|
301
|
+
canonicalPlatformProjectRoot,
|
|
302
|
+
outputPaths,
|
|
303
|
+
) {
|
|
304
|
+
const outputDirectories = [...new Set(outputPaths.map(outputPath => path.dirname(outputPath)))];
|
|
305
|
+
for (const outputDirectory of outputDirectories) {
|
|
306
|
+
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
validateAndroidOutputDestinations(
|
|
310
|
+
resolvedPlatformProjectRoot,
|
|
311
|
+
canonicalPlatformProjectRoot,
|
|
312
|
+
outputPaths,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function validateAndroidResourceCollisions(platformProjectRoot, resourceRoot, preparedIcons) {
|
|
317
|
+
const sourceSetRoot = path.join(platformProjectRoot, 'app', 'src');
|
|
318
|
+
const mipmapDirectories = findResourceDirectories(sourceSetRoot, 'mipmap');
|
|
319
|
+
|
|
320
|
+
for (const { icon } of preparedIcons) {
|
|
321
|
+
for (const mipmapDirectory of mipmapDirectories) {
|
|
322
|
+
const collision = findLogicalResourceCollision(mipmapDirectory, icon.name);
|
|
323
|
+
if (collision !== undefined) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`[react-native-notify-kit] Android mipmap resource collision for '${icon.name}': ` +
|
|
326
|
+
`${collision}. Remove or rename the mipmap resource before generating a drawable icon.`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const { density } of ANDROID_NOTIFICATION_ICON_DENSITIES) {
|
|
332
|
+
const drawableDirectory = path.join(resourceRoot, `drawable-${density}`);
|
|
333
|
+
validateDrawableDestination(drawableDirectory, icon.name);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function findResourceDirectories(sourceSetRoot, resourceType) {
|
|
339
|
+
if (!fs.existsSync(sourceSetRoot)) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const directories = [];
|
|
344
|
+
for (const sourceSet of fs.readdirSync(sourceSetRoot, { withFileTypes: true })) {
|
|
345
|
+
if (!sourceSet.isDirectory()) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const sourceSetResourceRoot = path.join(sourceSetRoot, sourceSet.name, 'res');
|
|
350
|
+
if (
|
|
351
|
+
!fs.existsSync(sourceSetResourceRoot) ||
|
|
352
|
+
!fs.statSync(sourceSetResourceRoot).isDirectory()
|
|
353
|
+
) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const qualifier of fs.readdirSync(sourceSetResourceRoot, { withFileTypes: true })) {
|
|
358
|
+
if (
|
|
359
|
+
qualifier.isDirectory() &&
|
|
360
|
+
(qualifier.name === resourceType || qualifier.name.startsWith(`${resourceType}-`))
|
|
361
|
+
) {
|
|
362
|
+
directories.push(path.join(sourceSetResourceRoot, qualifier.name));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return directories;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function findLogicalResourceCollision(resourceDirectory, resourceName) {
|
|
371
|
+
for (const entry of fs.readdirSync(resourceDirectory, { withFileTypes: true })) {
|
|
372
|
+
if (
|
|
373
|
+
(entry.isFile() || entry.isSymbolicLink()) &&
|
|
374
|
+
hasLogicalResourceName(entry.name, resourceName)
|
|
375
|
+
) {
|
|
376
|
+
return path.join(resourceDirectory, entry.name);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function validateDrawableDestination(drawableDirectory, resourceName) {
|
|
384
|
+
if (!fs.existsSync(drawableDirectory)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!fs.statSync(drawableDirectory).isDirectory()) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`[react-native-notify-kit] Android drawable destination is not a directory: ${drawableDirectory}`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const exactPngName = `${resourceName}.png`;
|
|
395
|
+
for (const entry of fs.readdirSync(drawableDirectory, { withFileTypes: true })) {
|
|
396
|
+
if (!hasLogicalResourceName(entry.name, resourceName)) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const collisionPath = path.join(drawableDirectory, entry.name);
|
|
401
|
+
if (entry.name === exactPngName && entry.isFile()) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
throw new Error(
|
|
406
|
+
`[react-native-notify-kit] Android drawable resource collision for '${resourceName}': ` +
|
|
407
|
+
`${collisionPath}. Only the exact ${exactPngName} destination may be overwritten.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function validateKeepFileDestination(resourceRoot) {
|
|
413
|
+
const rawDirectory = path.join(resourceRoot, 'raw');
|
|
414
|
+
if (fs.existsSync(rawDirectory) && !fs.statSync(rawDirectory).isDirectory()) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`[react-native-notify-kit] Android raw resource destination is not a directory: ${rawDirectory}`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const keepFilePath = path.join(rawDirectory, NOTIFY_KIT_KEEP_FILE_NAME);
|
|
421
|
+
if (fs.existsSync(keepFilePath) && !fs.statSync(keepFilePath).isFile()) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`[react-native-notify-kit] NotifyKit keep file destination is not a file: ${keepFilePath}`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function generateAllAndroidNotificationIconBuffers(projectRoot, resourceRoot, preparedIcons) {
|
|
429
|
+
const generatedIcons = [];
|
|
430
|
+
|
|
431
|
+
for (const { icon, sourcePath } of preparedIcons) {
|
|
432
|
+
for (const { density, size } of ANDROID_NOTIFICATION_ICON_DENSITIES) {
|
|
433
|
+
try {
|
|
434
|
+
const generatedImage = await generateImageAsync(
|
|
435
|
+
{ projectRoot },
|
|
436
|
+
{
|
|
437
|
+
src: sourcePath,
|
|
438
|
+
name: `${icon.name}.png`,
|
|
439
|
+
width: size,
|
|
440
|
+
height: size,
|
|
441
|
+
resizeMode: 'cover',
|
|
442
|
+
backgroundColor: 'transparent',
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
generatedIcons.push({
|
|
447
|
+
outputPath: path.join(resourceRoot, `drawable-${density}`, `${icon.name}.png`),
|
|
448
|
+
source: generatedImage.source,
|
|
449
|
+
});
|
|
450
|
+
} catch (error) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`[react-native-notify-kit] Unable to generate Android notification icon '${icon.name}' ` +
|
|
453
|
+
`from '${icon.path}' at ${size}x${size}: ${formatError(error)}`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return generatedIcons;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function hasLogicalResourceName(fileName, resourceName) {
|
|
463
|
+
return fileName === resourceName || fileName.startsWith(`${resourceName}.`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function isPathInside(parentPath, childPath) {
|
|
467
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
relativePath === '' ||
|
|
471
|
+
(relativePath !== '..' &&
|
|
472
|
+
!relativePath.startsWith(`..${path.sep}`) &&
|
|
473
|
+
!path.isAbsolute(relativePath))
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function formatError(error) {
|
|
478
|
+
return error instanceof Error ? error.message : String(error);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function requireExpoConfigPlugins() {
|
|
482
|
+
try {
|
|
483
|
+
return require('expo/config-plugins');
|
|
484
|
+
} catch (error) {
|
|
485
|
+
try {
|
|
486
|
+
return require(require.resolve('expo/config-plugins', { paths: [process.cwd()] }));
|
|
487
|
+
} catch {
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
module.exports = {
|
|
494
|
+
ANDROID_NOTIFICATION_ICON_DENSITIES,
|
|
495
|
+
withNotifyKitAndroidNotificationIcons,
|
|
496
|
+
generateNotifyKitAndroidNotificationIconResources,
|
|
497
|
+
renderNotifyKitAndroidNotificationIconsKeepXml,
|
|
498
|
+
};
|
package/plugin/build/index.js
CHANGED
|
@@ -2,33 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
const { createRunOncePlugin } = requireExpoConfigPlugins();
|
|
4
4
|
const { normalizeNotifyKitPluginOptions } = require('./options');
|
|
5
|
+
const { withNotifyKitAndroidManifest } = require('./android/withNotifyKitAndroidManifest');
|
|
5
6
|
const {
|
|
6
|
-
|
|
7
|
-
} = require('./android/
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
} = require('./ios/
|
|
11
|
-
const {
|
|
12
|
-
withNotifyKitIosNseFiles,
|
|
13
|
-
} = require('./ios/withNotifyKitIosNseFiles');
|
|
14
|
-
const {
|
|
15
|
-
withNotifyKitIosNsePodfile,
|
|
16
|
-
} = require('./ios/withNotifyKitIosNsePodfile');
|
|
17
|
-
const {
|
|
18
|
-
withNotifyKitIosNseXcodeProject,
|
|
19
|
-
} = require('./ios/withNotifyKitIosNseXcodeProject');
|
|
7
|
+
withNotifyKitAndroidNotificationIcons,
|
|
8
|
+
} = require('./android/withNotifyKitAndroidNotificationIcons');
|
|
9
|
+
const { withNotifyKitIosNseAppExtension } = require('./ios/withNotifyKitIosNseAppExtension');
|
|
10
|
+
const { withNotifyKitIosNseFiles } = require('./ios/withNotifyKitIosNseFiles');
|
|
11
|
+
const { withNotifyKitIosNsePodfile } = require('./ios/withNotifyKitIosNsePodfile');
|
|
12
|
+
const { withNotifyKitIosNseXcodeProject } = require('./ios/withNotifyKitIosNseXcodeProject');
|
|
20
13
|
const pkg = require('../../package.json');
|
|
21
14
|
|
|
22
15
|
function withNotifyKit(config, props = {}) {
|
|
23
16
|
const options = normalizeNotifyKitPluginOptions(props);
|
|
24
17
|
const foregroundServiceOptions = options.android.foregroundService;
|
|
18
|
+
const notificationIcons = options.android.icons;
|
|
25
19
|
const nseOptions = options.ios.notificationServiceExtension;
|
|
26
|
-
const configWithAndroidManifest = withNotifyKitAndroidManifest(
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
const configWithAndroidManifest = withNotifyKitAndroidManifest(config, foregroundServiceOptions);
|
|
21
|
+
const configWithAndroidNotificationIcons = withNotifyKitAndroidNotificationIcons(
|
|
22
|
+
configWithAndroidManifest,
|
|
23
|
+
notificationIcons,
|
|
29
24
|
);
|
|
30
25
|
const configWithAppExtension = withNotifyKitIosNseAppExtension(
|
|
31
|
-
|
|
26
|
+
configWithAndroidNotificationIcons,
|
|
32
27
|
nseOptions,
|
|
33
28
|
);
|
|
34
29
|
const configWithFiles = withNotifyKitIosNseFiles(configWithAppExtension, nseOptions);
|
package/plugin/build/options.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
3
5
|
const DEFAULT_IOS_NSE_TARGET_NAME = 'NotifyKitNSE';
|
|
4
6
|
const DEFAULT_IOS_NSE_BUNDLE_SUFFIX = '.NotifyKitNSE';
|
|
5
7
|
|
|
@@ -21,6 +23,7 @@ const ANDROID_FOREGROUND_SERVICE_TYPES = [
|
|
|
21
23
|
|
|
22
24
|
const TARGET_NAME_PATTERN = /^[A-Za-z0-9_\-.]+$/;
|
|
23
25
|
const BUNDLE_SUFFIX_PATTERN = /^\.[A-Za-z0-9\-.]+$/;
|
|
26
|
+
const ANDROID_RESOURCE_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
24
27
|
|
|
25
28
|
function normalizeNotifyKitPluginOptions(options = {}) {
|
|
26
29
|
return {
|
|
@@ -33,6 +36,7 @@ function normalizeNotifyKitPluginOptions(options = {}) {
|
|
|
33
36
|
foregroundService: normalizeAndroidForegroundServiceOptions(
|
|
34
37
|
options.android && options.android.foregroundService,
|
|
35
38
|
),
|
|
39
|
+
icons: normalizeAndroidNotificationIcons(options.android && options.android.icons),
|
|
36
40
|
},
|
|
37
41
|
};
|
|
38
42
|
}
|
|
@@ -152,6 +156,85 @@ function disabledAndroidForegroundServiceOptions() {
|
|
|
152
156
|
};
|
|
153
157
|
}
|
|
154
158
|
|
|
159
|
+
function normalizeAndroidNotificationIcons(input) {
|
|
160
|
+
if (input === undefined) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!Array.isArray(input)) {
|
|
165
|
+
throw new Error('[react-native-notify-kit] android.icons must be an array.');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const names = new Set();
|
|
169
|
+
const icons = [];
|
|
170
|
+
|
|
171
|
+
for (const [index, value] of input.entries()) {
|
|
172
|
+
if (!isPlainObject(value)) {
|
|
173
|
+
throw new Error(`[react-native-notify-kit] android.icons[${index}] must be an object.`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (typeof value.name !== 'string' || value.name.trim().length === 0) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`[react-native-notify-kit] android.icons[${index}].name must be a non-empty string.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!ANDROID_RESOURCE_NAME_PATTERN.test(value.name)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`[react-native-notify-kit] Invalid android.icons[${index}].name '${value.name}'. ` +
|
|
185
|
+
'It must start with a lowercase letter and contain only lowercase letters, digits, and underscores.',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (typeof value.path !== 'string' || value.path.trim().length === 0) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`[react-native-notify-kit] android.icons[${index}].path must be a non-empty string.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (path.isAbsolute(value.path) || path.win32.isAbsolute(value.path)) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`[react-native-notify-kit] android.icons[${index}].path must be project-relative.`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (path.extname(value.path) !== '.png') {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`[react-native-notify-kit] android.icons[${index}].path must reference a lowercase .png file.`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (typeof value.type !== 'string') {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`[react-native-notify-kit] android.icons[${index}].type must be the string 'small'.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (value.type !== 'small') {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`[react-native-notify-kit] android.icons[${index}].type '${value.type}' is unsupported. ` +
|
|
216
|
+
"Only 'small' is supported.",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (names.has(value.name)) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`[react-native-notify-kit] Duplicate android.icons name '${value.name}'. ` +
|
|
223
|
+
'Each Android notification icon name must be unique.',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
names.add(value.name);
|
|
228
|
+
icons.push({
|
|
229
|
+
name: value.name,
|
|
230
|
+
path: value.path,
|
|
231
|
+
type: value.type,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return icons;
|
|
236
|
+
}
|
|
237
|
+
|
|
155
238
|
function normalizeAndroidForegroundServiceTypes(input) {
|
|
156
239
|
if (!Array.isArray(input)) {
|
|
157
240
|
throw new Error(
|
|
@@ -221,4 +304,5 @@ module.exports = {
|
|
|
221
304
|
normalizeNotifyKitPluginOptions,
|
|
222
305
|
normalizeIosNotificationServiceExtensionOptions,
|
|
223
306
|
normalizeAndroidForegroundServiceOptions,
|
|
307
|
+
normalizeAndroidNotificationIcons,
|
|
224
308
|
};
|
package/server/README.md
CHANGED
|
@@ -24,6 +24,8 @@ const message = buildNotifyKitPayload({
|
|
|
24
24
|
android: {
|
|
25
25
|
channelId: 'orders',
|
|
26
26
|
smallIcon: 'ic_notification',
|
|
27
|
+
showTimestamp: true,
|
|
28
|
+
timestamp: Date.now(),
|
|
27
29
|
pressAction: { id: 'open-order' },
|
|
28
30
|
},
|
|
29
31
|
ios: {
|
|
@@ -44,6 +46,8 @@ await admin.messaging().send(message);
|
|
|
44
46
|
|
|
45
47
|
`options.ttl` is expressed in seconds. Pass the message directly to Firebase Admin without an adapter or a manual unit conversion.
|
|
46
48
|
|
|
49
|
+
Android `showTimestamp` and `timestamp` stay inside the `_v: 1` `notifee_options` blob and are reconstructed client-side using the existing Android notification semantics. They are not Firebase Admin `android.notification` fields; the Android message remains data-only.
|
|
50
|
+
|
|
47
51
|
## API
|
|
48
52
|
|
|
49
53
|
### `buildNotifyKitPayload(input: NotifyKitPayloadInput): NotifyKitPayloadOutput`
|
|
@@ -7,6 +7,7 @@ import { Notification } from '../types/Notification';
|
|
|
7
7
|
import { NotificationAndroid, AndroidStyle } from '../types/NotificationAndroid';
|
|
8
8
|
import { NotificationIOS } from '../types/NotificationIOS';
|
|
9
9
|
import { isAndroid, isIOS } from '../utils';
|
|
10
|
+
import { isValidTimestamp } from '../validators/validate';
|
|
10
11
|
import type { FcmConfig, FcmRemoteMessage } from './types';
|
|
11
12
|
import type { ParsedPayload } from './parseFcmPayload';
|
|
12
13
|
|
|
@@ -126,6 +127,12 @@ function buildAndroidConfig(
|
|
|
126
127
|
if (typeof raw?.largeIcon === 'string') android.largeIcon = raw.largeIcon;
|
|
127
128
|
if (typeof raw?.color === 'string') android.color = raw.color;
|
|
128
129
|
|
|
130
|
+
// Timestamp fields — preserve only values accepted by the canonical Android validator
|
|
131
|
+
if (typeof raw?.showTimestamp === 'boolean') android.showTimestamp = raw.showTimestamp;
|
|
132
|
+
if (typeof raw?.timestamp === 'number' && isValidTimestamp(raw.timestamp)) {
|
|
133
|
+
android.timestamp = raw.timestamp;
|
|
134
|
+
}
|
|
135
|
+
|
|
129
136
|
// Actions array — pass through (trust server validation)
|
|
130
137
|
if (Array.isArray(raw?.actions)) {
|
|
131
138
|
android.actions = raw.actions as NotificationAndroid['actions'];
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '10.
|
|
2
|
+
export const version = '10.7.1';
|