react-native-notify-kit 9.7.0 → 10.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 (101) hide show
  1. package/README.md +71 -2
  2. package/RNNotifeeCore.podspec +2 -0
  3. package/cli/bin/react-native-notify-kit +2 -0
  4. package/cli/dist/cli.bundle.js +20834 -0
  5. package/cli/dist/cli.d.ts +2 -0
  6. package/cli/dist/cli.js +87 -0
  7. package/cli/dist/cli.js.map +1 -0
  8. package/cli/dist/commands/initNse.d.ts +12 -0
  9. package/cli/dist/commands/initNse.js +212 -0
  10. package/cli/dist/commands/initNse.js.map +1 -0
  11. package/cli/dist/lib/detectProject.d.ts +15 -0
  12. package/cli/dist/lib/detectProject.js +156 -0
  13. package/cli/dist/lib/detectProject.js.map +1 -0
  14. package/cli/dist/lib/logger.d.ts +4 -0
  15. package/cli/dist/lib/logger.js +24 -0
  16. package/cli/dist/lib/logger.js.map +1 -0
  17. package/cli/dist/lib/patchPodfile.d.ts +12 -0
  18. package/cli/dist/lib/patchPodfile.js +143 -0
  19. package/cli/dist/lib/patchPodfile.js.map +1 -0
  20. package/cli/dist/lib/patchXcodeProject.d.ts +25 -0
  21. package/cli/dist/lib/patchXcodeProject.js +239 -0
  22. package/cli/dist/lib/patchXcodeProject.js.map +1 -0
  23. package/cli/dist/lib/writeTemplates.d.ts +11 -0
  24. package/cli/dist/lib/writeTemplates.js +82 -0
  25. package/cli/dist/lib/writeTemplates.js.map +1 -0
  26. package/cli/dist/templates/Info.plist.tmpl +29 -0
  27. package/cli/dist/templates/NotificationService.swift.tmpl +32 -0
  28. package/cli/dist/templates/NotifyKitNSE.entitlements.tmpl +6 -0
  29. package/cli/dist/templates/templates/Info.plist.tmpl +29 -0
  30. package/cli/dist/templates/templates/NotificationService.swift.tmpl +73 -0
  31. package/cli/dist/templates/templates/NotifyKitNSE.entitlements.tmpl +6 -0
  32. package/dist/NotifeeApiModule.d.ts +17 -0
  33. package/dist/NotifeeApiModule.js +66 -1
  34. package/dist/NotifeeApiModule.js.map +1 -1
  35. package/dist/fcm/index.d.ts +4 -0
  36. package/dist/fcm/index.js +3 -0
  37. package/dist/fcm/index.js.map +1 -0
  38. package/dist/fcm/parseFcmPayload.d.ts +16 -0
  39. package/dist/fcm/parseFcmPayload.js +37 -0
  40. package/dist/fcm/parseFcmPayload.js.map +1 -0
  41. package/dist/fcm/reconstructNotification.d.ts +8 -0
  42. package/dist/fcm/reconstructNotification.js +167 -0
  43. package/dist/fcm/reconstructNotification.js.map +1 -0
  44. package/dist/fcm/types.d.ts +51 -0
  45. package/dist/fcm/types.js +6 -0
  46. package/dist/fcm/types.js.map +1 -0
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +1 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/types/Module.d.ts +21 -0
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/dist/version.js.map +1 -1
  54. package/package.json +38 -3
  55. package/server/README.md +129 -0
  56. package/server/dist/android.d.ts +8 -0
  57. package/server/dist/android.d.ts.map +1 -0
  58. package/server/dist/android.js +20 -0
  59. package/server/dist/android.js.map +1 -0
  60. package/server/dist/buildPayload.d.ts +3 -0
  61. package/server/dist/buildPayload.d.ts.map +1 -0
  62. package/server/dist/buildPayload.js +82 -0
  63. package/server/dist/buildPayload.js.map +1 -0
  64. package/server/dist/index.d.ts +6 -0
  65. package/server/dist/index.d.ts.map +1 -0
  66. package/server/dist/index.js +12 -0
  67. package/server/dist/index.js.map +1 -0
  68. package/server/dist/ios.d.ts +11 -0
  69. package/server/dist/ios.d.ts.map +1 -0
  70. package/server/dist/ios.js +61 -0
  71. package/server/dist/ios.js.map +1 -0
  72. package/server/dist/serialize.d.ts +11 -0
  73. package/server/dist/serialize.d.ts.map +1 -0
  74. package/server/dist/serialize.js +32 -0
  75. package/server/dist/serialize.js.map +1 -0
  76. package/server/dist/types.d.ts +2 -0
  77. package/server/dist/types.d.ts.map +1 -0
  78. package/server/dist/types.js +3 -0
  79. package/server/dist/types.js.map +1 -0
  80. package/server/dist/validation.d.ts +3 -0
  81. package/server/dist/validation.d.ts.map +1 -0
  82. package/server/dist/validation.js +101 -0
  83. package/server/dist/validation.js.map +1 -0
  84. package/server/package.json +5 -0
  85. package/server/src/android.ts +27 -0
  86. package/server/src/buildPayload.ts +91 -0
  87. package/server/src/index.ts +12 -0
  88. package/server/src/ios.ts +88 -0
  89. package/server/src/serialize.ts +39 -0
  90. package/server/src/types.ts +24 -0
  91. package/server/src/validation.ts +134 -0
  92. package/server/tsconfig.json +25 -0
  93. package/src/NotifeeApiModule.ts +80 -1
  94. package/src/fcm/index.ts +4 -0
  95. package/src/fcm/parseFcmPayload.ts +56 -0
  96. package/src/fcm/reconstructNotification.ts +201 -0
  97. package/src/fcm/types.ts +57 -0
  98. package/src/index.ts +2 -0
  99. package/src/internal/fcmContract.d.ts +150 -0
  100. package/src/types/Module.ts +23 -0
  101. package/src/version.ts +1 -1
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateInput = validateInput;
4
+ const PREFIX = '[react-native-notify-kit/server]';
5
+ const RESERVED_DATA_KEYS = ['notifee_options', 'notifee_data'];
6
+ function err(category, message) {
7
+ return new Error(`${PREFIX} ${category}: ${message}`);
8
+ }
9
+ function isNonEmptyString(value) {
10
+ return typeof value === 'string' && value.length > 0;
11
+ }
12
+ function validateInput(input) {
13
+ if (input === null || typeof input !== 'object') {
14
+ throw err('Validation', 'input must be an object');
15
+ }
16
+ const routingCount = (input.token !== undefined ? 1 : 0) +
17
+ (input.topic !== undefined ? 1 : 0) +
18
+ (input.condition !== undefined ? 1 : 0);
19
+ if (routingCount !== 1) {
20
+ throw err('Routing', `exactly one of 'token', 'topic', or 'condition' must be provided. Got: ${routingCount}`);
21
+ }
22
+ if (input.token !== undefined && !isNonEmptyString(input.token)) {
23
+ throw err('Routing', "'token' must be a non-empty string");
24
+ }
25
+ if (input.topic !== undefined && !isNonEmptyString(input.topic)) {
26
+ throw err('Routing', "'topic' must be a non-empty string");
27
+ }
28
+ if (input.condition !== undefined && !isNonEmptyString(input.condition)) {
29
+ throw err('Routing', "'condition' must be a non-empty string");
30
+ }
31
+ const { notification } = input;
32
+ if (notification === null || typeof notification !== 'object') {
33
+ throw err('Validation', "'notification' is required and must be an object");
34
+ }
35
+ if (notification.id !== undefined && !isNonEmptyString(notification.id)) {
36
+ throw err('Validation', 'notification.id must be a non-empty string when provided');
37
+ }
38
+ if (!isNonEmptyString(notification.title)) {
39
+ throw err('Validation', 'notification.title is required and must be a non-empty string');
40
+ }
41
+ if (!isNonEmptyString(notification.body)) {
42
+ throw err('Validation', 'notification.body is required and must be a non-empty string');
43
+ }
44
+ if (notification.data !== undefined) {
45
+ if (notification.data === null || typeof notification.data !== 'object') {
46
+ throw err('Validation', "'notification.data' must be an object");
47
+ }
48
+ for (const reserved of RESERVED_DATA_KEYS) {
49
+ if (Object.prototype.hasOwnProperty.call(notification.data, reserved)) {
50
+ throw err('Validation', "'notifee_options' and 'notifee_data' are reserved keys and cannot be used in notification.data");
51
+ }
52
+ }
53
+ for (const [key, value] of Object.entries(notification.data)) {
54
+ if (typeof value !== 'string') {
55
+ throw err('Validation', `FCM data values must be strings. Got ${typeof value} for key '${key}'. Use JSON.stringify() if you need to pass complex values.`);
56
+ }
57
+ }
58
+ }
59
+ const attachments = notification.ios?.attachments;
60
+ if (attachments !== undefined) {
61
+ if (!Array.isArray(attachments)) {
62
+ throw err('iOS', "'notification.ios.attachments' must be an array");
63
+ }
64
+ for (const attachment of attachments) {
65
+ if (attachment === null ||
66
+ typeof attachment !== 'object' ||
67
+ typeof attachment.url !== 'string') {
68
+ throw err('iOS', "each attachment must be an object with a string 'url' field");
69
+ }
70
+ if (!attachment.url.startsWith('https://')) {
71
+ throw err('iOS', `iOS attachments require https:// URLs. Got: ${attachment.url}`);
72
+ }
73
+ }
74
+ }
75
+ if (input.options !== undefined) {
76
+ const { options } = input;
77
+ if (options === null || typeof options !== 'object') {
78
+ throw err('Validation', "'options' must be an object");
79
+ }
80
+ if (options.androidPriority !== undefined &&
81
+ options.androidPriority !== 'high' &&
82
+ options.androidPriority !== 'normal') {
83
+ throw err('Validation', `'options.androidPriority' must be 'high' or 'normal'. Got: ${String(options.androidPriority)}`);
84
+ }
85
+ if (options.iosBadgeCount !== undefined &&
86
+ (typeof options.iosBadgeCount !== 'number' ||
87
+ !Number.isFinite(options.iosBadgeCount) ||
88
+ options.iosBadgeCount < 0 ||
89
+ !Number.isInteger(options.iosBadgeCount))) {
90
+ throw err('Validation', "'options.iosBadgeCount' must be a non-negative integer");
91
+ }
92
+ if (options.ttl !== undefined &&
93
+ (typeof options.ttl !== 'number' || !Number.isInteger(options.ttl) || options.ttl <= 0)) {
94
+ throw err('Validation', `options.ttl must be a positive integer (seconds). Got: ${String(options.ttl)}`);
95
+ }
96
+ if (options.collapseKey !== undefined && !isNonEmptyString(options.collapseKey)) {
97
+ throw err('Validation', "'options.collapseKey' must be a non-empty string");
98
+ }
99
+ }
100
+ }
101
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ {
2
+ "private": true,
3
+ "main": "./dist/index.js",
4
+ "types": "./dist/index.d.ts"
5
+ }
@@ -0,0 +1,27 @@
1
+ import type { NotifyKitAndroidOutput, NotifyKitOptions, NotifyKitPayloadInput } from './types';
2
+
3
+ type BuildAndroidContext = {
4
+ collapseKey?: string;
5
+ ttlSeconds?: number;
6
+ };
7
+
8
+ function toAndroidPriority(priority: NotifyKitOptions['androidPriority']): 'HIGH' | 'NORMAL' {
9
+ return priority === 'normal' ? 'NORMAL' : 'HIGH';
10
+ }
11
+
12
+ export function buildAndroidPayload(
13
+ input: NotifyKitPayloadInput,
14
+ context: BuildAndroidContext,
15
+ ): NotifyKitAndroidOutput {
16
+ const options: NotifyKitOptions = input.options ?? {};
17
+ const output: NotifyKitAndroidOutput = {
18
+ priority: toAndroidPriority(options.androidPriority),
19
+ };
20
+ if (context.collapseKey !== undefined) {
21
+ output.collapse_key = context.collapseKey;
22
+ }
23
+ if (context.ttlSeconds !== undefined) {
24
+ output.ttl = `${context.ttlSeconds}s`;
25
+ }
26
+ return output;
27
+ }
@@ -0,0 +1,91 @@
1
+ import { buildAndroidPayload } from './android';
2
+ import { buildIosApnsPayload } from './ios';
3
+ import { serializeData, serializeNotifeeOptions } from './serialize';
4
+ import type { NotifyKitPayloadInput, NotifyKitPayloadOutput } from './types';
5
+ import { validateInput } from './validation';
6
+
7
+ const SIZE_WARN_THRESHOLD_BYTES = 3500;
8
+ const PREFIX = '[react-native-notify-kit/server]';
9
+
10
+ export function buildNotifyKitPayload(input: NotifyKitPayloadInput): NotifyKitPayloadOutput {
11
+ validateInput(input);
12
+
13
+ const { notification } = input;
14
+ const options = input.options ?? {};
15
+
16
+ const collapseKey: string | undefined = options.collapseKey ?? notification.id ?? undefined;
17
+
18
+ let expiration: string | undefined;
19
+ if (options.ttl !== undefined) {
20
+ expiration = String(Math.floor(Date.now() / 1000) + options.ttl);
21
+ }
22
+
23
+ const notifeeOptions = serializeNotifeeOptions({
24
+ title: notification.title,
25
+ body: notification.body,
26
+ ...(notification.android !== undefined ? { android: notification.android } : {}),
27
+ ...(notification.ios !== undefined ? { ios: notification.ios } : {}),
28
+ });
29
+ const notifeeData = serializeData(notification.data);
30
+
31
+ // Android path: top-level `data` carries `notification.data` keys verbatim
32
+ // plus `notifee_options`. `notifee_data` is intentionally NOT duplicated here —
33
+ // the client can read the original keys directly from `data`. iOS NSE still
34
+ // receives `notifee_data` inside `apns.payload` because APNs does not expose
35
+ // arbitrary top-level keys to the extension.
36
+ const data: Record<string, string> = {
37
+ ...(notification.data ?? {}),
38
+ notifee_options: notifeeOptions,
39
+ };
40
+
41
+ const android = buildAndroidPayload(input, {
42
+ ...(collapseKey !== undefined ? { collapseKey } : {}),
43
+ ...(options.ttl !== undefined ? { ttlSeconds: options.ttl } : {}),
44
+ });
45
+
46
+ const apns = buildIosApnsPayload(input, {
47
+ notifeeOptions,
48
+ ...(notifeeData !== undefined ? { notifeeData } : {}),
49
+ ...(collapseKey !== undefined ? { collapseKey } : {}),
50
+ ...(expiration !== undefined ? { expiration } : {}),
51
+ });
52
+
53
+ // Build the FCM-bound payload first (without sizeBytes — that's metadata for
54
+ // the caller, not part of the wire format).
55
+ const fcmPayload: Omit<NotifyKitPayloadOutput, 'sizeBytes'> = {
56
+ data,
57
+ android,
58
+ apns,
59
+ };
60
+ if (input.token !== undefined) {
61
+ (fcmPayload as NotifyKitPayloadOutput).token = input.token;
62
+ } else if (input.topic !== undefined) {
63
+ (fcmPayload as NotifyKitPayloadOutput).topic = input.topic;
64
+ } else if (input.condition !== undefined) {
65
+ (fcmPayload as NotifyKitPayloadOutput).condition = input.condition;
66
+ }
67
+
68
+ // FCM's 4 KB limit is measured in UTF-8 bytes, not JS code units, so emoji
69
+ // and non-ASCII characters consume more than one byte each. Deviation from
70
+ // the spec's literal `JSON.stringify(output).length` to avoid underestimating
71
+ // payload size under such characters. sizeBytes measures the FCM-relevant
72
+ // fields only (excludes the sizeBytes field itself).
73
+ const sizeBytes = Buffer.byteLength(JSON.stringify(fcmPayload), 'utf8');
74
+ if (sizeBytes > SIZE_WARN_THRESHOLD_BYTES) {
75
+ console.warn(
76
+ `${PREFIX} Payload size ${sizeBytes} bytes approaches FCM 4KB limit. Consider reducing notifee_options.`,
77
+ );
78
+ }
79
+
80
+ // Attach sizeBytes as non-enumerable so JSON.stringify and object spread
81
+ // never include it — consumers can safely pass the output directly to
82
+ // admin.messaging().send() without stripping metadata fields.
83
+ const output = { ...fcmPayload } as NotifyKitPayloadOutput;
84
+ Object.defineProperty(output, 'sizeBytes', {
85
+ value: sizeBytes,
86
+ enumerable: false,
87
+ writable: false,
88
+ configurable: false,
89
+ });
90
+ return output;
91
+ }
@@ -0,0 +1,12 @@
1
+ export { buildNotifyKitPayload } from './buildPayload';
2
+ export { buildIosApnsPayload } from './ios';
3
+ export { buildAndroidPayload } from './android';
4
+ export { serializeNotifeeOptions } from './serialize';
5
+ export type {
6
+ NotifyKitPayloadInput,
7
+ NotifyKitPayloadOutput,
8
+ NotifyKitNotification,
9
+ NotifyKitOptions,
10
+ NotifyKitAndroidConfig,
11
+ NotifyKitIosConfig,
12
+ } from './types';
@@ -0,0 +1,88 @@
1
+ import type {
2
+ ApnsInterruptionLevel,
3
+ NotifyKitApnsAps,
4
+ NotifyKitApnsHeaders,
5
+ NotifyKitApnsOutput,
6
+ NotifyKitIosInterruptionLevel,
7
+ NotifyKitNotification,
8
+ NotifyKitOptions,
9
+ NotifyKitPayloadInput,
10
+ } from './types';
11
+
12
+ const INTERRUPTION_LEVEL_MAP: Record<NotifyKitIosInterruptionLevel, ApnsInterruptionLevel> = {
13
+ passive: 'passive',
14
+ active: 'active',
15
+ timeSensitive: 'time-sensitive',
16
+ critical: 'critical',
17
+ };
18
+
19
+ const PREFIX = '[react-native-notify-kit/server]';
20
+
21
+ export function toApnsInterruptionLevel(
22
+ level: NotifyKitIosInterruptionLevel,
23
+ ): ApnsInterruptionLevel {
24
+ const mapped = INTERRUPTION_LEVEL_MAP[level as NotifyKitIosInterruptionLevel];
25
+ if (mapped === undefined) {
26
+ throw new Error(
27
+ `${PREFIX} Validation: invalid interruptionLevel '${String(level)}'. Expected one of: passive, active, timeSensitive, critical`,
28
+ );
29
+ }
30
+ return mapped;
31
+ }
32
+
33
+ type BuildIosContext = {
34
+ notifeeOptions: string;
35
+ notifeeData?: string;
36
+ collapseKey?: string;
37
+ expiration?: string;
38
+ };
39
+
40
+ export function buildIosApnsPayload(
41
+ input: NotifyKitPayloadInput,
42
+ context: BuildIosContext,
43
+ ): NotifyKitApnsOutput {
44
+ const notification: NotifyKitNotification = input.notification;
45
+ const options: NotifyKitOptions = input.options ?? {};
46
+ const ios = notification.ios;
47
+
48
+ const headers: NotifyKitApnsHeaders = {
49
+ 'apns-push-type': 'alert',
50
+ 'apns-priority': '10',
51
+ };
52
+ if (context.collapseKey !== undefined) {
53
+ headers['apns-collapse-id'] = context.collapseKey;
54
+ }
55
+ if (context.expiration !== undefined) {
56
+ headers['apns-expiration'] = context.expiration;
57
+ }
58
+
59
+ const aps: NotifyKitApnsAps = {
60
+ alert: { title: notification.title, body: notification.body },
61
+ 'mutable-content': 1,
62
+ };
63
+ if (ios?.sound !== undefined) {
64
+ aps.sound = ios.sound;
65
+ }
66
+ if (ios?.categoryId !== undefined) {
67
+ aps.category = ios.categoryId;
68
+ }
69
+ if (ios?.threadId !== undefined) {
70
+ aps['thread-id'] = ios.threadId;
71
+ }
72
+ if (ios?.interruptionLevel !== undefined) {
73
+ aps['interruption-level'] = toApnsInterruptionLevel(ios.interruptionLevel);
74
+ }
75
+ if (options.iosBadgeCount !== undefined) {
76
+ aps.badge = options.iosBadgeCount;
77
+ }
78
+
79
+ const payload: NotifyKitApnsOutput['payload'] = {
80
+ aps,
81
+ notifee_options: context.notifeeOptions,
82
+ };
83
+ if (context.notifeeData !== undefined) {
84
+ payload.notifee_data = context.notifeeData;
85
+ }
86
+
87
+ return { headers, payload };
88
+ }
@@ -0,0 +1,39 @@
1
+ import type { NotifyKitAndroidConfig, NotifyKitIosConfig, SerializedNotifeeOptions } from './types';
2
+
3
+ type SerializeInput = {
4
+ title: string;
5
+ body: string;
6
+ android?: NotifyKitAndroidConfig;
7
+ ios?: NotifyKitIosConfig;
8
+ };
9
+
10
+ const PREFIX = '[react-native-notify-kit/server]';
11
+
12
+ export function serializeNotifeeOptions(input: SerializeInput): string {
13
+ const payload: SerializedNotifeeOptions = { _v: 1, title: input.title, body: input.body };
14
+ if (input.android !== undefined) {
15
+ payload.android = input.android;
16
+ }
17
+ if (input.ios !== undefined) {
18
+ payload.ios = input.ios;
19
+ }
20
+ try {
21
+ return JSON.stringify(payload);
22
+ } catch (e: unknown) {
23
+ const detail = e instanceof Error ? `: ${e.message}` : `: ${String(e)}`;
24
+ throw new Error(
25
+ `${PREFIX} Serialization: notifee_options contains circular references or non-serializable values. Check for circular object references in android/ios config${detail}`,
26
+ );
27
+ }
28
+ }
29
+
30
+ export function serializeData(data?: Record<string, string>): string | undefined {
31
+ if (!data) {
32
+ return undefined;
33
+ }
34
+ const keys = Object.keys(data);
35
+ if (keys.length === 0) {
36
+ return undefined;
37
+ }
38
+ return JSON.stringify(data);
39
+ }
@@ -0,0 +1,24 @@
1
+ // Source of truth: packages/react-native/src/internal/fcmContract.ts
2
+ // Re-exported here so the server SDK can be consumed via
3
+ // `react-native-notify-kit/server` without a deep import of the internal path.
4
+ // The shared file is included in the server tsconfig via a relative path.
5
+ export type {
6
+ NotifyKitPressAction,
7
+ NotifyKitAndroidAction,
8
+ NotifyKitAndroidStyle,
9
+ NotifyKitAndroidConfig,
10
+ NotifyKitIosAttachment,
11
+ NotifyKitIosInterruptionLevel,
12
+ NotifyKitIosConfig,
13
+ NotifyKitNotification,
14
+ NotifyKitOptions,
15
+ NotifyKitPayloadInput,
16
+ ApnsInterruptionLevel,
17
+ NotifyKitAndroidOutput,
18
+ NotifyKitApnsAps,
19
+ NotifyKitApnsHeaders,
20
+ NotifyKitApnsPayload,
21
+ NotifyKitApnsOutput,
22
+ NotifyKitPayloadOutput,
23
+ SerializedNotifeeOptions,
24
+ } from '../../src/internal/fcmContract';
@@ -0,0 +1,134 @@
1
+ import type { NotifyKitPayloadInput } from './types';
2
+
3
+ const PREFIX = '[react-native-notify-kit/server]';
4
+ const RESERVED_DATA_KEYS = ['notifee_options', 'notifee_data'] as const;
5
+
6
+ function err(category: string, message: string): Error {
7
+ return new Error(`${PREFIX} ${category}: ${message}`);
8
+ }
9
+
10
+ function isNonEmptyString(value: unknown): value is string {
11
+ return typeof value === 'string' && value.length > 0;
12
+ }
13
+
14
+ export function validateInput(input: NotifyKitPayloadInput): void {
15
+ if (input === null || typeof input !== 'object') {
16
+ throw err('Validation', 'input must be an object');
17
+ }
18
+
19
+ const routingCount =
20
+ (input.token !== undefined ? 1 : 0) +
21
+ (input.topic !== undefined ? 1 : 0) +
22
+ (input.condition !== undefined ? 1 : 0);
23
+
24
+ if (routingCount !== 1) {
25
+ throw err(
26
+ 'Routing',
27
+ `exactly one of 'token', 'topic', or 'condition' must be provided. Got: ${routingCount}`,
28
+ );
29
+ }
30
+
31
+ if (input.token !== undefined && !isNonEmptyString(input.token)) {
32
+ throw err('Routing', "'token' must be a non-empty string");
33
+ }
34
+ if (input.topic !== undefined && !isNonEmptyString(input.topic)) {
35
+ throw err('Routing', "'topic' must be a non-empty string");
36
+ }
37
+ if (input.condition !== undefined && !isNonEmptyString(input.condition)) {
38
+ throw err('Routing', "'condition' must be a non-empty string");
39
+ }
40
+
41
+ const { notification } = input;
42
+ if (notification === null || typeof notification !== 'object') {
43
+ throw err('Validation', "'notification' is required and must be an object");
44
+ }
45
+
46
+ if (notification.id !== undefined && !isNonEmptyString(notification.id)) {
47
+ throw err('Validation', 'notification.id must be a non-empty string when provided');
48
+ }
49
+ if (!isNonEmptyString(notification.title)) {
50
+ throw err('Validation', 'notification.title is required and must be a non-empty string');
51
+ }
52
+ if (!isNonEmptyString(notification.body)) {
53
+ throw err('Validation', 'notification.body is required and must be a non-empty string');
54
+ }
55
+
56
+ if (notification.data !== undefined) {
57
+ if (notification.data === null || typeof notification.data !== 'object') {
58
+ throw err('Validation', "'notification.data' must be an object");
59
+ }
60
+ for (const reserved of RESERVED_DATA_KEYS) {
61
+ if (Object.prototype.hasOwnProperty.call(notification.data, reserved)) {
62
+ throw err(
63
+ 'Validation',
64
+ "'notifee_options' and 'notifee_data' are reserved keys and cannot be used in notification.data",
65
+ );
66
+ }
67
+ }
68
+ for (const [key, value] of Object.entries(notification.data)) {
69
+ if (typeof value !== 'string') {
70
+ throw err(
71
+ 'Validation',
72
+ `FCM data values must be strings. Got ${typeof value} for key '${key}'. Use JSON.stringify() if you need to pass complex values.`,
73
+ );
74
+ }
75
+ }
76
+ }
77
+
78
+ const attachments = notification.ios?.attachments;
79
+ if (attachments !== undefined) {
80
+ if (!Array.isArray(attachments)) {
81
+ throw err('iOS', "'notification.ios.attachments' must be an array");
82
+ }
83
+ for (const attachment of attachments) {
84
+ if (
85
+ attachment === null ||
86
+ typeof attachment !== 'object' ||
87
+ typeof attachment.url !== 'string'
88
+ ) {
89
+ throw err('iOS', "each attachment must be an object with a string 'url' field");
90
+ }
91
+ if (!attachment.url.startsWith('https://')) {
92
+ throw err('iOS', `iOS attachments require https:// URLs. Got: ${attachment.url}`);
93
+ }
94
+ }
95
+ }
96
+
97
+ if (input.options !== undefined) {
98
+ const { options } = input;
99
+ if (options === null || typeof options !== 'object') {
100
+ throw err('Validation', "'options' must be an object");
101
+ }
102
+ if (
103
+ options.androidPriority !== undefined &&
104
+ options.androidPriority !== 'high' &&
105
+ options.androidPriority !== 'normal'
106
+ ) {
107
+ throw err(
108
+ 'Validation',
109
+ `'options.androidPriority' must be 'high' or 'normal'. Got: ${String(options.androidPriority)}`,
110
+ );
111
+ }
112
+ if (
113
+ options.iosBadgeCount !== undefined &&
114
+ (typeof options.iosBadgeCount !== 'number' ||
115
+ !Number.isFinite(options.iosBadgeCount) ||
116
+ options.iosBadgeCount < 0 ||
117
+ !Number.isInteger(options.iosBadgeCount))
118
+ ) {
119
+ throw err('Validation', "'options.iosBadgeCount' must be a non-negative integer");
120
+ }
121
+ if (
122
+ options.ttl !== undefined &&
123
+ (typeof options.ttl !== 'number' || !Number.isInteger(options.ttl) || options.ttl <= 0)
124
+ ) {
125
+ throw err(
126
+ 'Validation',
127
+ `options.ttl must be a positive integer (seconds). Got: ${String(options.ttl)}`,
128
+ );
129
+ }
130
+ if (options.collapseKey !== undefined && !isNonEmptyString(options.collapseKey)) {
131
+ throw err('Validation', "'options.collapseKey' must be a non-empty string");
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "module": "NodeNext",
10
+ "moduleResolution": "nodenext",
11
+ "target": "ES2020",
12
+ "lib": ["ES2020"],
13
+ "types": ["node"],
14
+ "allowJs": false,
15
+ "isolatedModules": true,
16
+ "strict": true,
17
+ "noUncheckedIndexedAccess": true,
18
+ "exactOptionalPropertyTypes": true,
19
+ "esModuleInterop": true,
20
+ "skipLibCheck": true,
21
+ "listEmittedFiles": false
22
+ },
23
+ "include": ["src/**/*"],
24
+ "exclude": ["src/__tests__/**/*", "dist", "node_modules"]
25
+ }
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) 2016-present Invertase Limited
3
3
  */
4
4
 
5
- import { AppRegistry, Platform } from 'react-native';
5
+ import { AppRegistry, AppState, Platform } from 'react-native';
6
6
  import { Module } from './types/Module';
7
7
  import {
8
8
  AndroidChannel,
@@ -48,8 +48,12 @@ import {
48
48
  } from './types/NotificationIOS';
49
49
  import validateIOSCategory from './validators/validateIOSCategory';
50
50
  import validateIOSPermissions from './validators/validateIOSPermissions';
51
+ import type { FcmConfig, FcmRemoteMessage } from './fcm/types';
52
+ import { parseFcmPayload } from './fcm/parseFcmPayload';
53
+ import { reconstructNotification } from './fcm/reconstructNotification';
51
54
 
52
55
  let backgroundEventHandler: (event: Event) => Promise<void>;
56
+ let fcmConfig: FcmConfig = {};
53
57
 
54
58
  let registeredForegroundServiceTask: (notification: Notification) => Promise<void>;
55
59
 
@@ -839,4 +843,79 @@ export default class NotifeeApiModule extends NotifeeNativeModule implements Mod
839
843
  }
840
844
  return this.native.hideNotificationDrawer();
841
845
  };
846
+
847
+ /**
848
+ * Processes an FCM remote message produced by the NotifyKit server SDK and
849
+ * displays a Notifee notification according to the embedded `notifee_options`.
850
+ *
851
+ * Safe to call from both `setBackgroundMessageHandler` and `onMessage`.
852
+ */
853
+ public handleFcmMessage = async (remoteMessage: FcmRemoteMessage): Promise<string | null> => {
854
+ if (remoteMessage == null || typeof remoteMessage !== 'object') {
855
+ throw new Error("notifee.handleFcmMessage(*) 'remoteMessage' expected an object.");
856
+ }
857
+
858
+ // Snapshot config at entry — Rule C10 (mid-flight setFcmConfig won't affect this call).
859
+ // Deep-copy nested ios sub-object to prevent caller mutation leaking through.
860
+ const config: FcmConfig = {
861
+ ...fcmConfig,
862
+ ios: fcmConfig.ios ? { ...fcmConfig.ios } : undefined,
863
+ };
864
+
865
+ const parsed = parseFcmPayload(remoteMessage.data);
866
+
867
+ // Fallback path — no notifee_options present
868
+ if (parsed === null && config.fallbackBehavior === 'ignore') {
869
+ return null;
870
+ }
871
+
872
+ const notification = reconstructNotification(parsed, remoteMessage, config);
873
+
874
+ // iOS background/killed no-op — Rule C3 (NSE already displayed)
875
+ if (isIOS) {
876
+ const state = AppState.currentState;
877
+ if (state !== 'active') {
878
+ return null;
879
+ }
880
+ // iOS foreground suppress — Rule C2 exception
881
+ if (config.ios?.suppressForegroundBanner === true) {
882
+ return null;
883
+ }
884
+ }
885
+
886
+ // Diagnostic warnings before display
887
+ if (!notification.title && !notification.body) {
888
+ console.warn(
889
+ '[react-native-notify-kit] handleFcmMessage: displaying notification with empty title and body. Check your FCM payload.',
890
+ );
891
+ }
892
+ if (isAndroid && !notification.android?.channelId) {
893
+ console.warn(
894
+ '[react-native-notify-kit] handleFcmMessage: Android fallback path has no channelId (no payload channelId, no defaultChannelId configured). Notification may be dropped by the OS.',
895
+ );
896
+ }
897
+
898
+ // Android always displays (Rule C1), iOS foreground displays (Rule C2)
899
+ return this.displayNotification(notification);
900
+ };
901
+
902
+ /**
903
+ * Configures defaults for {@link handleFcmMessage}. Call once at app startup,
904
+ * typically in `index.js` before `registerComponent`.
905
+ *
906
+ * Returns Promise for forward compatibility — a future version may persist
907
+ * config across cold starts (AsyncStorage / MMKV), which would be async.
908
+ * Currently resolves synchronously.
909
+ */
910
+ public setFcmConfig = (config: FcmConfig): Promise<void> => {
911
+ if (config == null || typeof config !== 'object' || Array.isArray(config)) {
912
+ const got = config === null ? 'null' : Array.isArray(config) ? 'array' : typeof config;
913
+ throw new Error(`notifee.setFcmConfig(*) config must be a plain object. Got: ${got}`);
914
+ }
915
+ fcmConfig = {
916
+ ...config,
917
+ ios: config.ios ? { ...config.ios } : undefined,
918
+ };
919
+ return Promise.resolve();
920
+ };
842
921
  }
@@ -0,0 +1,4 @@
1
+ export { parseFcmPayload } from './parseFcmPayload';
2
+ export type { ParsedPayload } from './parseFcmPayload';
3
+ export { reconstructNotification } from './reconstructNotification';
4
+ export type { FcmRemoteMessage, FcmConfig } from './types';