react-native-notify-kit 10.7.2 → 10.8.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 (44) hide show
  1. package/android/src/main/java/app/notifee/core/ForegroundService.java +363 -66
  2. package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +1194 -32
  3. package/android/src/test/java/app/notifee/core/NotificationManagerActionRoutingTest.java +37 -0
  4. package/dist/fcm/parseFcmPayload.d.ts +1 -0
  5. package/dist/fcm/parseFcmPayload.js.map +1 -1
  6. package/dist/fcm/reconstructNotification.js +96 -8
  7. package/dist/fcm/reconstructNotification.js.map +1 -1
  8. package/dist/validators/validateAndroidPressAction.js +3 -0
  9. package/dist/validators/validateAndroidPressAction.js.map +1 -1
  10. package/dist/validators/validateIOSNotification.js +5 -2
  11. package/dist/validators/validateIOSNotification.js.map +1 -1
  12. package/dist/version.d.ts +1 -1
  13. package/dist/version.js +1 -1
  14. package/ios/NotifeeCore/NotifeeCore.m +36 -3
  15. package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +82 -3
  16. package/ios/NotifeeCore/NotifeeCoreFcmIdentity.h +91 -0
  17. package/package.json +1 -1
  18. package/server/README.md +5 -3
  19. package/server/dist/buildPayload.d.ts.map +1 -1
  20. package/server/dist/buildPayload.js +2 -0
  21. package/server/dist/buildPayload.js.map +1 -1
  22. package/server/dist/ios.d.ts.map +1 -1
  23. package/server/dist/ios.js +8 -1
  24. package/server/dist/ios.js.map +1 -1
  25. package/server/dist/serialize.d.ts +2 -0
  26. package/server/dist/serialize.d.ts.map +1 -1
  27. package/server/dist/serialize.js +6 -0
  28. package/server/dist/serialize.js.map +1 -1
  29. package/server/dist/types.d.ts +1 -1
  30. package/server/dist/types.d.ts.map +1 -1
  31. package/server/dist/validation.d.ts.map +1 -1
  32. package/server/dist/validation.js +160 -6
  33. package/server/dist/validation.js.map +1 -1
  34. package/server/src/buildPayload.ts +2 -0
  35. package/server/src/ios.ts +8 -1
  36. package/server/src/serialize.ts +8 -0
  37. package/server/src/types.ts +1 -0
  38. package/server/src/validation.ts +187 -6
  39. package/src/fcm/parseFcmPayload.ts +1 -0
  40. package/src/fcm/reconstructNotification.ts +105 -10
  41. package/src/internal/fcmContract.d.ts +57 -12
  42. package/src/validators/validateAndroidPressAction.ts +4 -0
  43. package/src/validators/validateIOSNotification.ts +7 -2
  44. package/src/version.ts +1 -1
@@ -2,6 +2,17 @@ import type { NotifyKitPayloadInput } from './types';
2
2
 
3
3
  const PREFIX = '[react-native-notify-kit/server]';
4
4
  const RESERVED_DATA_KEYS = ['notifee_options', 'notifee_data'] as const;
5
+ const BIG_TEXT_STYLE_KEYS = new Set(['type', 'text', 'title', 'summary']);
6
+ const BIG_PICTURE_STYLE_KEYS = new Set(['type', 'picture', 'title', 'summary', 'largeIcon']);
7
+ const PRESS_ACTION_KEYS = new Set(['id', 'launchActivity', 'mainComponent', 'launchActivityFlags']);
8
+ const ACTION_KEYS = new Set(['title', 'pressAction', 'icon', 'input']);
9
+ const INPUT_KEYS = new Set([
10
+ 'allowFreeFormInput',
11
+ 'allowGeneratedReplies',
12
+ 'choices',
13
+ 'editableChoices',
14
+ 'placeholder',
15
+ ]);
5
16
 
6
17
  function err(category: string, message: string): Error {
7
18
  return new Error(`${PREFIX} ${category}: ${message}`);
@@ -11,6 +22,119 @@ function isNonEmptyString(value: unknown): value is string {
11
22
  return typeof value === 'string' && value.length > 0;
12
23
  }
13
24
 
25
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
26
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
27
+ return false;
28
+ }
29
+ const prototype = Object.getPrototypeOf(value);
30
+ return prototype === Object.prototype || prototype === null;
31
+ }
32
+
33
+ function rejectUnsupportedKeys(
34
+ value: Record<string, unknown>,
35
+ supported: Set<string>,
36
+ path: string,
37
+ ): void {
38
+ for (const key of Object.keys(value)) {
39
+ if (!supported.has(key)) {
40
+ throw err('Android', `${path}.${key} is not supported`);
41
+ }
42
+ }
43
+ }
44
+
45
+ function validatePressAction(value: unknown, path: string): void {
46
+ if (!isPlainObject(value)) {
47
+ throw err('Android', `${path} must be an object`);
48
+ }
49
+ rejectUnsupportedKeys(value, PRESS_ACTION_KEYS, path);
50
+ if (!isNonEmptyString(value.id)) {
51
+ throw err('Android', `${path}.id must be a non-empty string`);
52
+ }
53
+ if (value.id === '__NOTIFEE_OPT_OUT__') {
54
+ throw err('Android', `${path}.id is reserved for explicit null`);
55
+ }
56
+ if (value.launchActivity !== undefined && typeof value.launchActivity !== 'string') {
57
+ throw err('Android', `${path}.launchActivity must be a string`);
58
+ }
59
+ if (value.mainComponent !== undefined && typeof value.mainComponent !== 'string') {
60
+ throw err('Android', `${path}.mainComponent must be a string`);
61
+ }
62
+ if (value.launchActivityFlags !== undefined) {
63
+ if (!Array.isArray(value.launchActivityFlags)) {
64
+ throw err(
65
+ 'Android',
66
+ `${path}.launchActivityFlags must be an array of AndroidLaunchActivityFlag values`,
67
+ );
68
+ }
69
+ for (const flag of value.launchActivityFlags) {
70
+ if (typeof flag !== 'number' || !Number.isInteger(flag) || flag < 0 || flag > 20) {
71
+ throw err(
72
+ 'Android',
73
+ `${path}.launchActivityFlags must be an array of AndroidLaunchActivityFlag values`,
74
+ );
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ function validateActionInput(value: unknown, path: string): void {
81
+ if (value === true) {
82
+ return;
83
+ }
84
+ if (!isPlainObject(value)) {
85
+ throw err('Android', `${path} must be true or an AndroidInput object`);
86
+ }
87
+ rejectUnsupportedKeys(value, INPUT_KEYS, path);
88
+
89
+ for (const key of ['allowFreeFormInput', 'allowGeneratedReplies', 'editableChoices'] as const) {
90
+ if (Object.prototype.hasOwnProperty.call(value, key) && typeof value[key] !== 'boolean') {
91
+ throw err('Android', `${path}.${key} must be a boolean`);
92
+ }
93
+ }
94
+ if (Object.prototype.hasOwnProperty.call(value, 'choices')) {
95
+ if (!Array.isArray(value.choices) || value.choices.length === 0) {
96
+ throw err('Android', `${path}.choices must be a non-empty array of strings`);
97
+ }
98
+ for (const choice of value.choices) {
99
+ if (typeof choice !== 'string') {
100
+ throw err('Android', `${path}.choices must be a non-empty array of strings`);
101
+ }
102
+ }
103
+ }
104
+ if (
105
+ Object.prototype.hasOwnProperty.call(value, 'placeholder') &&
106
+ typeof value.placeholder !== 'string'
107
+ ) {
108
+ throw err('Android', `${path}.placeholder must be a string`);
109
+ }
110
+ if (
111
+ value.allowFreeFormInput === false &&
112
+ (!Array.isArray(value.choices) || value.choices.length === 0)
113
+ ) {
114
+ throw err('Android', `${path}.allowFreeFormInput requires choices when false`);
115
+ }
116
+ if (value.editableChoices === true && value.allowFreeFormInput === false) {
117
+ throw err('Android', `${path}.editableChoices requires allowFreeFormInput when true`);
118
+ }
119
+ }
120
+
121
+ function validateAction(value: unknown, path: string): void {
122
+ if (!isPlainObject(value)) {
123
+ throw err('Android', `${path} must be an AndroidAction object`);
124
+ }
125
+ rejectUnsupportedKeys(value, ACTION_KEYS, path);
126
+ if (!isNonEmptyString(value.title)) {
127
+ throw err('Android', `${path}.title must be a non-empty string`);
128
+ }
129
+ validatePressAction(value.pressAction, `${path}.pressAction`);
130
+ if (value.icon !== undefined && !isNonEmptyString(value.icon)) {
131
+ throw err('Android', `${path}.icon must be a non-empty string`);
132
+ }
133
+ if (value.input !== undefined) {
134
+ validateActionInput(value.input, `${path}.input`);
135
+ }
136
+ }
137
+
14
138
  export function validateInput(input: NotifyKitPayloadInput): void {
15
139
  if (input === null || typeof input !== 'object') {
16
140
  throw err('Validation', 'input must be an object');
@@ -52,6 +176,9 @@ export function validateInput(input: NotifyKitPayloadInput): void {
52
176
  if (!isNonEmptyString(notification.body)) {
53
177
  throw err('Validation', 'notification.body is required and must be a non-empty string');
54
178
  }
179
+ if (notification.subtitle !== undefined && typeof notification.subtitle !== 'string') {
180
+ throw err('Validation', 'notification.subtitle must be a string when provided');
181
+ }
55
182
 
56
183
  if (notification.data !== undefined) {
57
184
  if (notification.data === null || typeof notification.data !== 'object') {
@@ -75,6 +202,59 @@ export function validateInput(input: NotifyKitPayloadInput): void {
75
202
  }
76
203
  }
77
204
 
205
+ if (notification.android !== undefined) {
206
+ if (!isPlainObject(notification.android)) {
207
+ throw err('Android', 'notification.android must be an object');
208
+ }
209
+ if (
210
+ notification.android.pressAction !== undefined &&
211
+ notification.android.pressAction !== null
212
+ ) {
213
+ validatePressAction(notification.android.pressAction, 'notification.android.pressAction');
214
+ }
215
+ }
216
+
217
+ const circularLargeIcon = notification.android?.circularLargeIcon;
218
+ if (circularLargeIcon !== undefined && typeof circularLargeIcon !== 'boolean') {
219
+ throw err('Android', 'notification.android.circularLargeIcon must be a boolean when provided');
220
+ }
221
+
222
+ const style = notification.android?.style;
223
+ if (style !== undefined) {
224
+ if (style === null || typeof style !== 'object' || Array.isArray(style)) {
225
+ throw err('Android', 'notification.android.style must be a supported style object');
226
+ }
227
+ if (style.type !== 'BIG_TEXT' && style.type !== 'BIG_PICTURE') {
228
+ throw err('Android', 'notification.android.style type must be BIG_TEXT or BIG_PICTURE');
229
+ }
230
+ const supportedKeys = style.type === 'BIG_TEXT' ? BIG_TEXT_STYLE_KEYS : BIG_PICTURE_STYLE_KEYS;
231
+ for (const key of Object.keys(style)) {
232
+ if (!supportedKeys.has(key)) {
233
+ throw err('Android', `notification.android.style.${key} is not supported`);
234
+ }
235
+ }
236
+ if (style.type === 'BIG_TEXT' && !isNonEmptyString(style.text)) {
237
+ throw err('Android', 'notification.android.style.text must be a non-empty string');
238
+ }
239
+ if (style.type === 'BIG_PICTURE' && !isNonEmptyString(style.picture)) {
240
+ throw err('Android', 'notification.android.style.picture must be a non-empty string');
241
+ }
242
+ if (style.title !== undefined && typeof style.title !== 'string') {
243
+ throw err('Android', 'notification.android.style.title must be a string when provided');
244
+ }
245
+ if (style.summary !== undefined && typeof style.summary !== 'string') {
246
+ throw err('Android', 'notification.android.style.summary must be a string when provided');
247
+ }
248
+ if (
249
+ style.type === 'BIG_PICTURE' &&
250
+ style.largeIcon !== undefined &&
251
+ style.largeIcon !== null &&
252
+ typeof style.largeIcon !== 'string'
253
+ ) {
254
+ throw err('Android', 'notification.android.style.largeIcon must be a string or null');
255
+ }
256
+ }
257
+
78
258
  const attachments = notification.ios?.attachments;
79
259
  if (attachments !== undefined) {
80
260
  if (!Array.isArray(attachments)) {
@@ -133,14 +313,15 @@ export function validateInput(input: NotifyKitPayloadInput): void {
133
313
  }
134
314
 
135
315
  const actions = notification.android?.actions;
136
- if (Array.isArray(actions)) {
316
+ if (actions !== undefined) {
317
+ if (!Array.isArray(actions)) {
318
+ throw err('Android', 'notification.android.actions must be an array');
319
+ }
137
320
  const pressActionIds = new Set<string>();
138
321
 
139
- for (const action of actions) {
140
- const pressActionId = action?.pressAction?.id;
141
- if (typeof pressActionId !== 'string') {
142
- continue;
143
- }
322
+ for (const [index, action] of actions.entries()) {
323
+ validateAction(action, `notification.android.actions[${index}]`);
324
+ const pressActionId = action.pressAction.id;
144
325
 
145
326
  if (pressActionIds.has(pressActionId)) {
146
327
  throw err(
@@ -7,6 +7,7 @@ const PREFIX = '[react-native-notify-kit]';
7
7
  export type ParsedPayload = {
8
8
  _v?: number;
9
9
  title?: string;
10
+ subtitle?: string;
10
11
  body?: string;
11
12
  android?: Record<string, unknown>;
12
13
  ios?: Record<string, unknown>;
@@ -20,6 +20,46 @@ const STYLE_TYPE_MAP: Record<string, AndroidStyle | undefined> = {
20
20
  BIG_TEXT: AndroidStyle.BIGTEXT,
21
21
  BIG_PICTURE: AndroidStyle.BIGPICTURE,
22
22
  };
23
+ const ANDROID_INPUT_KEYS = new Set([
24
+ 'allowFreeFormInput',
25
+ 'allowGeneratedReplies',
26
+ 'choices',
27
+ 'editableChoices',
28
+ 'placeholder',
29
+ ]);
30
+
31
+ function assertFcmPressAction(
32
+ value: unknown,
33
+ path: string,
34
+ ): asserts value is NonNullable<NotificationAndroid['pressAction']> {
35
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
36
+ throw new Error(`${PREFIX} ${path} must be an object`);
37
+ }
38
+
39
+ const pressAction = value as Record<string, unknown>;
40
+ if (typeof pressAction.id !== 'string' || pressAction.id.length === 0) {
41
+ throw new Error(`${PREFIX} ${path}.id must be a non-empty string`);
42
+ }
43
+ if (pressAction.id === '__NOTIFEE_OPT_OUT__') {
44
+ throw new Error(`${PREFIX} ${path}.id is reserved for explicit null`);
45
+ }
46
+ if (pressAction.launchActivity !== undefined && typeof pressAction.launchActivity !== 'string') {
47
+ throw new Error(`${PREFIX} ${path}.launchActivity must be a string`);
48
+ }
49
+ if (pressAction.mainComponent !== undefined && typeof pressAction.mainComponent !== 'string') {
50
+ throw new Error(`${PREFIX} ${path}.mainComponent must be a string`);
51
+ }
52
+ if (pressAction.launchActivityFlags !== undefined) {
53
+ if (!Array.isArray(pressAction.launchActivityFlags)) {
54
+ throw new Error(`${PREFIX} ${path}.launchActivityFlags must be an array`);
55
+ }
56
+ for (const flag of pressAction.launchActivityFlags) {
57
+ if (typeof flag !== 'number' || !Number.isInteger(flag) || flag < 0 || flag > 20) {
58
+ throw new Error(`${PREFIX} ${path}.launchActivityFlags contains an invalid flag`);
59
+ }
60
+ }
61
+ }
62
+ }
23
63
 
24
64
  /**
25
65
  * Reconstructs a {@link Notification} object suitable for
@@ -47,6 +87,10 @@ export function reconstructNotification(
47
87
  body,
48
88
  };
49
89
 
90
+ if (typeof parsed?.subtitle === 'string') {
91
+ notification.subtitle = parsed.subtitle;
92
+ }
93
+
50
94
  if (Object.keys(data).length > 0) {
51
95
  notification.data = data;
52
96
  }
@@ -116,15 +160,21 @@ function buildAndroidConfig(
116
160
  android.channelId = channelId;
117
161
  }
118
162
 
119
- // pressAction: payload > config default
120
- const pressAction = raw?.pressAction ?? config.defaultPressAction;
121
- if (pressAction && typeof pressAction === 'object') {
122
- android.pressAction = pressAction as NotificationAndroid['pressAction'];
163
+ // Explicit null opts out of the body tap; only absence uses the configured default.
164
+ const pressAction = raw?.pressAction === undefined ? config.defaultPressAction : raw.pressAction;
165
+ if (pressAction === null) {
166
+ android.pressAction = null;
167
+ } else if (pressAction !== undefined) {
168
+ assertFcmPressAction(pressAction, 'android.pressAction');
169
+ android.pressAction = pressAction;
123
170
  }
124
171
 
125
172
  // Direct string copies
126
173
  if (typeof raw?.smallIcon === 'string') android.smallIcon = raw.smallIcon;
127
174
  if (typeof raw?.largeIcon === 'string') android.largeIcon = raw.largeIcon;
175
+ if (typeof raw?.circularLargeIcon === 'boolean') {
176
+ android.circularLargeIcon = raw.circularLargeIcon;
177
+ }
128
178
  if (typeof raw?.color === 'string') android.color = raw.color;
129
179
 
130
180
  // Timestamp fields — preserve only values accepted by the canonical Android validator
@@ -133,21 +183,66 @@ function buildAndroidConfig(
133
183
  android.timestamp = raw.timestamp;
134
184
  }
135
185
 
136
- // Actions array — pass through (trust server validation)
137
- if (Array.isArray(raw?.actions)) {
138
- android.actions = raw.actions as NotificationAndroid['actions'];
186
+ // Legacy input:false means no reply. Other malformed inputs must fail here:
187
+ // the normal validator accepts some of them as a default RemoteInput.
188
+ if (raw?.actions !== undefined) {
189
+ if (!Array.isArray(raw.actions)) {
190
+ throw new Error(`${PREFIX} android.actions must be an array`);
191
+ }
192
+ android.actions = raw.actions.map((action, index) => {
193
+ if (action === null || typeof action !== 'object' || Array.isArray(action)) {
194
+ return action;
195
+ }
196
+ if (action.pressAction !== undefined) {
197
+ assertFcmPressAction(action.pressAction, `android.actions[${index}].pressAction`);
198
+ }
199
+ const input = action.input;
200
+ if (input === false) {
201
+ const withoutInput = { ...action };
202
+ delete withoutInput.input;
203
+ return withoutInput;
204
+ }
205
+ if (
206
+ input !== undefined &&
207
+ input !== true &&
208
+ (input === null || typeof input !== 'object' || Array.isArray(input))
209
+ ) {
210
+ throw new Error(`${PREFIX} android.actions[${index}].input is invalid`);
211
+ }
212
+ if (input !== undefined && input !== true) {
213
+ for (const key of Object.keys(input)) {
214
+ if (!ANDROID_INPUT_KEYS.has(key)) {
215
+ throw new Error(`${PREFIX} android.actions[${index}].input.${key} is unsupported`);
216
+ }
217
+ }
218
+ }
219
+ return action;
220
+ }) as NonNullable<NotificationAndroid['actions']>;
139
221
  }
140
222
 
141
223
  // Style — enum mapping with defense-in-depth for unknown types
142
224
  if (raw?.style && typeof raw.style === 'object') {
143
- const style = raw.style as { type?: string; text?: string; picture?: string };
225
+ const style = raw.style as Record<string, unknown>;
144
226
  if (typeof style.type === 'string') {
145
227
  const mappedType = STYLE_TYPE_MAP[style.type];
146
228
  if (mappedType !== undefined) {
147
229
  if (mappedType === AndroidStyle.BIGTEXT && typeof style.text === 'string') {
148
- android.style = { type: AndroidStyle.BIGTEXT, text: style.text };
230
+ android.style = {
231
+ type: AndroidStyle.BIGTEXT,
232
+ text: style.text,
233
+ ...(typeof style.title === 'string' ? { title: style.title } : {}),
234
+ ...(typeof style.summary === 'string' ? { summary: style.summary } : {}),
235
+ };
149
236
  } else if (mappedType === AndroidStyle.BIGPICTURE && typeof style.picture === 'string') {
150
- android.style = { type: AndroidStyle.BIGPICTURE, picture: style.picture };
237
+ android.style = {
238
+ type: AndroidStyle.BIGPICTURE,
239
+ picture: style.picture,
240
+ ...(typeof style.title === 'string' ? { title: style.title } : {}),
241
+ ...(typeof style.summary === 'string' ? { summary: style.summary } : {}),
242
+ ...(style.largeIcon === null || typeof style.largeIcon === 'string'
243
+ ? { largeIcon: style.largeIcon }
244
+ : {}),
245
+ };
151
246
  } else {
152
247
  const field = mappedType === AndroidStyle.BIGTEXT ? 'text' : 'picture';
153
248
  console.warn(
@@ -12,27 +12,69 @@
12
12
  * that are not for end users.
13
13
  */
14
14
 
15
+ // Numeric wire values of the public AndroidLaunchActivityFlag enum (0 through 20).
16
+ export type NotifyKitAndroidLaunchActivityFlag =
17
+ | 0
18
+ | 1
19
+ | 2
20
+ | 3
21
+ | 4
22
+ | 5
23
+ | 6
24
+ | 7
25
+ | 8
26
+ | 9
27
+ | 10
28
+ | 11
29
+ | 12
30
+ | 13
31
+ | 14
32
+ | 15
33
+ | 16
34
+ | 17
35
+ | 18
36
+ | 19
37
+ | 20;
38
+
15
39
  export type NotifyKitPressAction = {
16
40
  id: string;
17
41
  launchActivity?: string;
42
+ mainComponent?: string;
43
+ launchActivityFlags?: NotifyKitAndroidLaunchActivityFlag[];
44
+ };
45
+
46
+ export type NotifyKitAndroidInput = {
47
+ allowFreeFormInput?: boolean;
48
+ allowGeneratedReplies?: boolean;
49
+ choices?: string[];
50
+ editableChoices?: boolean;
51
+ placeholder?: string;
18
52
  };
19
53
 
20
54
  export type NotifyKitAndroidAction = {
21
55
  title: string;
22
56
  pressAction: NotifyKitPressAction;
23
- input?: boolean;
57
+ icon?: string;
58
+ input?: true | NotifyKitAndroidInput;
24
59
  };
25
60
 
26
61
  export type NotifyKitAndroidStyle =
27
- | { type: 'BIG_TEXT'; text: string }
28
- | { type: 'BIG_PICTURE'; picture: string };
62
+ | { type: 'BIG_TEXT'; text: string; title?: string; summary?: string }
63
+ | {
64
+ type: 'BIG_PICTURE';
65
+ picture: string;
66
+ title?: string;
67
+ summary?: string;
68
+ largeIcon?: string | null;
69
+ };
29
70
 
30
71
  export type NotifyKitAndroidConfig = {
31
72
  channelId?: string;
32
73
  smallIcon?: string;
33
74
  largeIcon?: string;
75
+ circularLargeIcon?: boolean;
34
76
  color?: string;
35
- pressAction?: NotifyKitPressAction;
77
+ pressAction?: NotifyKitPressAction | null;
36
78
  actions?: NotifyKitAndroidAction[];
37
79
  style?: NotifyKitAndroidStyle;
38
80
  showTimestamp?: boolean;
@@ -56,20 +98,21 @@ export type NotifyKitIosConfig = {
56
98
 
57
99
  export type NotifyKitNotification = {
58
100
  /**
59
- * Optional notification identifier. When provided, it is also used as the
60
- * FCM collapse key (Android `collapseKey`, iOS `apns-collapse-id`) unless
61
- * `options.collapseKey` is explicitly set. Collapse key precedence (Rule 7):
101
+ * Optional public notification identifier. It is serialized in the _v:1
102
+ * options blob and used by NotifyKit display, events, readback and cancel.
103
+ * It also defaults the FCM transport collapse key (Android `collapseKey`,
104
+ * iOS `apns-collapse-id`) unless `options.collapseKey` is explicitly set.
105
+ * Collapse key precedence (Rule 7):
62
106
  * 1. `options.collapseKey` if set
63
107
  * 2. `notification.id` if set
64
108
  * 3. Omitted (no collapse behaviour)
65
109
  *
66
- * This means setting `id` implicitly enables notification replacement on the
67
- * device: a new notification with the same `id` replaces the previous one.
68
- * If you need `id` for local deduplication without collapse behaviour, set a
69
- * distinct `options.collapseKey` or leave both unset.
110
+ * Collapse controls transport coalescing. It does not guarantee replacement
111
+ * of a notification that has already been delivered.
70
112
  */
71
113
  id?: string;
72
114
  title: string;
115
+ subtitle?: string;
73
116
  body: string;
74
117
  data?: Record<string, string>;
75
118
  android?: NotifyKitAndroidConfig;
@@ -100,7 +143,7 @@ export type NotifyKitAndroidOutput = {
100
143
  };
101
144
 
102
145
  export type NotifyKitApnsAps = {
103
- alert: { title: string; body: string };
146
+ alert: { title: string; body: string; subtitle?: string };
104
147
  sound?: string;
105
148
  category?: string;
106
149
  'mutable-content': 1;
@@ -141,7 +184,9 @@ export type NotifyKitPayloadOutput = NotifyKitPayloadTarget & {
141
184
 
142
185
  export type SerializedNotifeeOptions = {
143
186
  _v: 1;
187
+ id?: string;
144
188
  title: string;
189
+ subtitle?: string;
145
190
  body: string;
146
191
  android?: NotifyKitAndroidConfig;
147
192
  ios?: NotifyKitIosConfig;
@@ -19,6 +19,10 @@ export default function validateAndroidPressAction(
19
19
  throw new Error("'id' expected a non-empty string value.");
20
20
  }
21
21
 
22
+ if (pressAction.id === '__NOTIFEE_OPT_OUT__') {
23
+ throw new Error("'id' is reserved for an explicit null pressAction.");
24
+ }
25
+
22
26
  const out: NotificationPressAction = {
23
27
  id: pressAction.id,
24
28
  };
@@ -133,8 +133,13 @@ export default function validateIOSNotification(ios?: NotificationIOS): Notifica
133
133
  /**
134
134
  * badgeCount
135
135
  */
136
- if (objectHasProperty(ios, 'badgeCount')) {
137
- if (!isNumber(ios.badgeCount) || ios.badgeCount < 0) {
136
+ if (objectHasProperty(ios, 'badgeCount') && ios.badgeCount !== null) {
137
+ if (
138
+ !isNumber(ios.badgeCount) ||
139
+ !Number.isFinite(ios.badgeCount) ||
140
+ !Number.isInteger(ios.badgeCount) ||
141
+ ios.badgeCount < 0
142
+ ) {
138
143
  throw new Error("'notification.ios.badgeCount' expected a number value >=0.");
139
144
  }
140
145
 
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '10.7.2';
2
+ export const version = '10.8.0';