react-native-notify-kit 9.7.0 → 10.1.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.
- package/README.md +140 -6
- package/RNNotifeeCore.podspec +2 -0
- package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +94 -2
- package/android/src/main/AndroidManifest.xml +8 -7
- package/android/src/main/java/app/notifee/core/NotificationManager.java +2 -0
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +8 -3
- package/android/src/main/java/app/notifee/core/utility/ResourceUtils.java +28 -0
- package/android/src/test/java/app/notifee/core/model/NotificationAndroidModelSmallIconFallbackTest.java +74 -0
- package/android/src/test/java/app/notifee/core/utility/ResourceUtilsFallbackIconTest.java +116 -0
- package/cli/bin/react-native-notify-kit +2 -0
- package/cli/dist/cli.bundle.js +20834 -0
- package/cli/dist/cli.d.ts +2 -0
- package/cli/dist/cli.js +87 -0
- package/cli/dist/cli.js.map +1 -0
- package/cli/dist/commands/initNse.d.ts +12 -0
- package/cli/dist/commands/initNse.js +212 -0
- package/cli/dist/commands/initNse.js.map +1 -0
- package/cli/dist/lib/detectProject.d.ts +15 -0
- package/cli/dist/lib/detectProject.js +156 -0
- package/cli/dist/lib/detectProject.js.map +1 -0
- package/cli/dist/lib/logger.d.ts +4 -0
- package/cli/dist/lib/logger.js +24 -0
- package/cli/dist/lib/logger.js.map +1 -0
- package/cli/dist/lib/patchPodfile.d.ts +12 -0
- package/cli/dist/lib/patchPodfile.js +143 -0
- package/cli/dist/lib/patchPodfile.js.map +1 -0
- package/cli/dist/lib/patchXcodeProject.d.ts +25 -0
- package/cli/dist/lib/patchXcodeProject.js +239 -0
- package/cli/dist/lib/patchXcodeProject.js.map +1 -0
- package/cli/dist/lib/writeTemplates.d.ts +11 -0
- package/cli/dist/lib/writeTemplates.js +82 -0
- package/cli/dist/lib/writeTemplates.js.map +1 -0
- package/cli/dist/templates/Info.plist.tmpl +29 -0
- package/cli/dist/templates/NotificationService.swift.tmpl +32 -0
- package/cli/dist/templates/NotifyKitNSE.entitlements.tmpl +6 -0
- package/cli/dist/templates/templates/Info.plist.tmpl +29 -0
- package/cli/dist/templates/templates/NotificationService.swift.tmpl +73 -0
- package/cli/dist/templates/templates/NotifyKitNSE.entitlements.tmpl +6 -0
- package/dist/NotifeeApiModule.d.ts +17 -0
- package/dist/NotifeeApiModule.js +66 -1
- package/dist/NotifeeApiModule.js.map +1 -1
- package/dist/fcm/index.d.ts +4 -0
- package/dist/fcm/index.js +3 -0
- package/dist/fcm/index.js.map +1 -0
- package/dist/fcm/parseFcmPayload.d.ts +16 -0
- package/dist/fcm/parseFcmPayload.js +37 -0
- package/dist/fcm/parseFcmPayload.js.map +1 -0
- package/dist/fcm/reconstructNotification.d.ts +8 -0
- package/dist/fcm/reconstructNotification.js +167 -0
- package/dist/fcm/reconstructNotification.js.map +1 -0
- package/dist/fcm/types.d.ts +51 -0
- package/dist/fcm/types.js +6 -0
- package/dist/fcm/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/Module.d.ts +21 -0
- package/dist/validators/validateTrigger.js +16 -0
- package/dist/validators/validateTrigger.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +38 -3
- package/server/README.md +129 -0
- package/server/dist/android.d.ts +8 -0
- package/server/dist/android.d.ts.map +1 -0
- package/server/dist/android.js +20 -0
- package/server/dist/android.js.map +1 -0
- package/server/dist/buildPayload.d.ts +3 -0
- package/server/dist/buildPayload.d.ts.map +1 -0
- package/server/dist/buildPayload.js +82 -0
- package/server/dist/buildPayload.js.map +1 -0
- package/server/dist/index.d.ts +6 -0
- package/server/dist/index.d.ts.map +1 -0
- package/server/dist/index.js +12 -0
- package/server/dist/index.js.map +1 -0
- package/server/dist/ios.d.ts +11 -0
- package/server/dist/ios.d.ts.map +1 -0
- package/server/dist/ios.js +61 -0
- package/server/dist/ios.js.map +1 -0
- package/server/dist/serialize.d.ts +11 -0
- package/server/dist/serialize.d.ts.map +1 -0
- package/server/dist/serialize.js +32 -0
- package/server/dist/serialize.js.map +1 -0
- package/server/dist/types.d.ts +2 -0
- package/server/dist/types.d.ts.map +1 -0
- package/server/dist/types.js +3 -0
- package/server/dist/types.js.map +1 -0
- package/server/dist/validation.d.ts +3 -0
- package/server/dist/validation.d.ts.map +1 -0
- package/server/dist/validation.js +101 -0
- package/server/dist/validation.js.map +1 -0
- package/server/package.json +5 -0
- package/server/src/android.ts +27 -0
- package/server/src/buildPayload.ts +91 -0
- package/server/src/index.ts +12 -0
- package/server/src/ios.ts +88 -0
- package/server/src/serialize.ts +39 -0
- package/server/src/types.ts +24 -0
- package/server/src/validation.ts +134 -0
- package/server/tsconfig.json +25 -0
- package/src/NotifeeApiModule.ts +80 -1
- package/src/fcm/index.ts +4 -0
- package/src/fcm/parseFcmPayload.ts +56 -0
- package/src/fcm/reconstructNotification.ts +201 -0
- package/src/fcm/types.ts +57 -0
- package/src/index.ts +2 -0
- package/src/internal/fcmContract.d.ts +150 -0
- package/src/types/Module.ts +23 -0
- package/src/validators/validateTrigger.ts +24 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pure function — parses the `notifee_options` blob from an FCM data payload.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const PREFIX = '[react-native-notify-kit]';
|
|
6
|
+
|
|
7
|
+
export type ParsedPayload = {
|
|
8
|
+
_v?: number;
|
|
9
|
+
title?: string;
|
|
10
|
+
body?: string;
|
|
11
|
+
android?: Record<string, unknown>;
|
|
12
|
+
ios?: Record<string, unknown>;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts and JSON-parses the `notifee_options` value from the FCM data map.
|
|
18
|
+
* Returns the parsed object on success, or `null` when:
|
|
19
|
+
* - `data` is undefined/null
|
|
20
|
+
* - `data.notifee_options` is missing
|
|
21
|
+
* - JSON parsing fails (a `console.warn` is emitted)
|
|
22
|
+
*/
|
|
23
|
+
export function parseFcmPayload(data: Record<string, string> | undefined): ParsedPayload | null {
|
|
24
|
+
if (!data || typeof data.notifee_options !== 'string') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let parsed: ParsedPayload;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(data.notifee_options);
|
|
31
|
+
} catch (e: unknown) {
|
|
32
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
33
|
+
console.warn(
|
|
34
|
+
`${PREFIX} Failed to parse notifee_options: ${detail}. Falling back to raw title/body.`,
|
|
35
|
+
);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
40
|
+
console.warn(
|
|
41
|
+
`${PREFIX} notifee_options parsed to a non-object value. Falling back to raw title/body.`,
|
|
42
|
+
);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Version check (Rule C5) — coerce to number to handle string _v from
|
|
47
|
+
// intermediaries that stringify FCM data values.
|
|
48
|
+
const versionNum = Number(parsed._v);
|
|
49
|
+
if (!Number.isNaN(versionNum) && versionNum > 1) {
|
|
50
|
+
console.warn(
|
|
51
|
+
`${PREFIX} notifee_options version ${versionNum} is newer than supported version 1. Display may be incomplete.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pure function — builds a Notification object from a parsed notifee_options
|
|
3
|
+
* blob, an FCM remote message, and the current FcmConfig.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Notification } from '../types/Notification';
|
|
7
|
+
import { NotificationAndroid, AndroidStyle } from '../types/NotificationAndroid';
|
|
8
|
+
import { NotificationIOS } from '../types/NotificationIOS';
|
|
9
|
+
import { isAndroid, isIOS } from '../utils';
|
|
10
|
+
import type { FcmConfig, FcmRemoteMessage } from './types';
|
|
11
|
+
import type { ParsedPayload } from './parseFcmPayload';
|
|
12
|
+
|
|
13
|
+
const PREFIX = '[react-native-notify-kit]';
|
|
14
|
+
const RESERVED_DATA_KEYS = new Set(['notifee_options', 'notifee_data']);
|
|
15
|
+
|
|
16
|
+
const VALID_INTERRUPTION_LEVELS = new Set(['active', 'critical', 'passive', 'timeSensitive']);
|
|
17
|
+
|
|
18
|
+
const STYLE_TYPE_MAP: Record<string, AndroidStyle | undefined> = {
|
|
19
|
+
BIG_TEXT: AndroidStyle.BIGTEXT,
|
|
20
|
+
BIG_PICTURE: AndroidStyle.BIGPICTURE,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reconstructs a {@link Notification} object suitable for
|
|
25
|
+
* `displayNotification` from the parsed FCM payload + config defaults.
|
|
26
|
+
*/
|
|
27
|
+
export function reconstructNotification(
|
|
28
|
+
parsed: ParsedPayload | null,
|
|
29
|
+
remoteMessage: FcmRemoteMessage,
|
|
30
|
+
config: FcmConfig,
|
|
31
|
+
): Notification {
|
|
32
|
+
const data = rebuildData(remoteMessage.data);
|
|
33
|
+
|
|
34
|
+
const title =
|
|
35
|
+
parsed?.title ?? remoteMessage.notification?.title ?? remoteMessage.data?.title ?? '';
|
|
36
|
+
|
|
37
|
+
const body = parsed?.body ?? remoteMessage.notification?.body ?? remoteMessage.data?.body ?? '';
|
|
38
|
+
|
|
39
|
+
const notification: Notification = {
|
|
40
|
+
id:
|
|
41
|
+
((parsed as Record<string, unknown> | null)?.id as string | undefined) ??
|
|
42
|
+
remoteMessage.messageId,
|
|
43
|
+
title,
|
|
44
|
+
body,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (Object.keys(data).length > 0) {
|
|
48
|
+
notification.data = data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isAndroid && (parsed?.android || config.defaultChannelId || config.defaultPressAction)) {
|
|
52
|
+
notification.android = buildAndroidConfig(parsed?.android, config);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isIOS && parsed?.ios) {
|
|
56
|
+
notification.ios = buildIosConfig(parsed.ios);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return notification;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Data reconstruction (Rules C6 / C7)
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function rebuildData(rawData: Record<string, string> | undefined): Record<string, string> {
|
|
67
|
+
if (!rawData) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Start with top-level data keys (excluding reserved)
|
|
72
|
+
const result: Record<string, string> = {};
|
|
73
|
+
for (const [key, value] of Object.entries(rawData)) {
|
|
74
|
+
if (!RESERVED_DATA_KEYS.has(key)) {
|
|
75
|
+
result[key] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Merge notifee_data blob — overrides top-level on conflict (Rule C7)
|
|
80
|
+
const notifeeDataRaw = rawData.notifee_data;
|
|
81
|
+
if (typeof notifeeDataRaw === 'string') {
|
|
82
|
+
try {
|
|
83
|
+
const notifeeData = JSON.parse(notifeeDataRaw) as Record<string, string>;
|
|
84
|
+
if (notifeeData && typeof notifeeData === 'object') {
|
|
85
|
+
Object.assign(result, notifeeData);
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
console.warn(`${PREFIX} Failed to parse notifee_data. Using top-level data keys only.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Post-merge strip: notifee_data blob could contain reserved keys
|
|
93
|
+
for (const key of RESERVED_DATA_KEYS) {
|
|
94
|
+
delete result[key];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Android config mapping
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
function buildAndroidConfig(
|
|
105
|
+
raw: Record<string, unknown> | undefined,
|
|
106
|
+
config: FcmConfig,
|
|
107
|
+
): NotificationAndroid {
|
|
108
|
+
const android: NotificationAndroid = {};
|
|
109
|
+
|
|
110
|
+
// channelId: payload > config default
|
|
111
|
+
const channelId = raw?.channelId ?? config.defaultChannelId;
|
|
112
|
+
if (typeof channelId === 'string') {
|
|
113
|
+
android.channelId = channelId;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// pressAction: payload > config default
|
|
117
|
+
const pressAction = raw?.pressAction ?? config.defaultPressAction;
|
|
118
|
+
if (pressAction && typeof pressAction === 'object') {
|
|
119
|
+
android.pressAction = pressAction as NotificationAndroid['pressAction'];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Direct string copies
|
|
123
|
+
if (typeof raw?.smallIcon === 'string') android.smallIcon = raw.smallIcon;
|
|
124
|
+
if (typeof raw?.largeIcon === 'string') android.largeIcon = raw.largeIcon;
|
|
125
|
+
if (typeof raw?.color === 'string') android.color = raw.color;
|
|
126
|
+
|
|
127
|
+
// Actions array — pass through (trust server validation)
|
|
128
|
+
if (Array.isArray(raw?.actions)) {
|
|
129
|
+
android.actions = raw.actions as NotificationAndroid['actions'];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Style — enum mapping with defense-in-depth for unknown types
|
|
133
|
+
if (raw?.style && typeof raw.style === 'object') {
|
|
134
|
+
const style = raw.style as { type?: string; text?: string; picture?: string };
|
|
135
|
+
if (typeof style.type === 'string') {
|
|
136
|
+
const mappedType = STYLE_TYPE_MAP[style.type];
|
|
137
|
+
if (mappedType !== undefined) {
|
|
138
|
+
if (mappedType === AndroidStyle.BIGTEXT && typeof style.text === 'string') {
|
|
139
|
+
android.style = { type: AndroidStyle.BIGTEXT, text: style.text };
|
|
140
|
+
} else if (mappedType === AndroidStyle.BIGPICTURE && typeof style.picture === 'string') {
|
|
141
|
+
android.style = { type: AndroidStyle.BIGPICTURE, picture: style.picture };
|
|
142
|
+
} else {
|
|
143
|
+
const field = mappedType === AndroidStyle.BIGTEXT ? 'text' : 'picture';
|
|
144
|
+
console.warn(
|
|
145
|
+
`${PREFIX} android.style.type '${style.type}' present but required '${field}' field missing or not a string. Style ignored.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
console.warn(`${PREFIX} Unknown android.style.type '${style.type}'. Style ignored.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return android;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// iOS config mapping
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
function buildIosConfig(raw: Record<string, unknown>): NotificationIOS {
|
|
162
|
+
const ios: NotificationIOS = {};
|
|
163
|
+
|
|
164
|
+
if (typeof raw.sound === 'string') ios.sound = raw.sound;
|
|
165
|
+
if (typeof raw.categoryId === 'string') ios.categoryId = raw.categoryId;
|
|
166
|
+
if (typeof raw.threadId === 'string') ios.threadId = raw.threadId;
|
|
167
|
+
|
|
168
|
+
// interruptionLevel — defense-in-depth for unknown values
|
|
169
|
+
if (typeof raw.interruptionLevel === 'string') {
|
|
170
|
+
if (VALID_INTERRUPTION_LEVELS.has(raw.interruptionLevel)) {
|
|
171
|
+
ios.interruptionLevel = raw.interruptionLevel as NotificationIOS['interruptionLevel'];
|
|
172
|
+
} else {
|
|
173
|
+
console.warn(`${PREFIX} Unknown ios.interruptionLevel '${raw.interruptionLevel}'. Ignored.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Attachments — rename identifier → id, filter out invalid entries
|
|
178
|
+
if (Array.isArray(raw.attachments)) {
|
|
179
|
+
ios.attachments = (raw.attachments as Array<unknown>)
|
|
180
|
+
.filter((att): att is Record<string, unknown> => {
|
|
181
|
+
if (att == null || typeof att !== 'object') return false;
|
|
182
|
+
const a = att as Record<string, unknown>;
|
|
183
|
+
if (typeof a.url !== 'string' || a.url.length === 0) {
|
|
184
|
+
console.warn(`${PREFIX} ios.attachments entry has missing or empty url. Skipped.`);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
})
|
|
189
|
+
.map(att => {
|
|
190
|
+
const mapped: { id?: string; url: string } = {
|
|
191
|
+
url: att.url as string,
|
|
192
|
+
};
|
|
193
|
+
if (typeof att.identifier === 'string') {
|
|
194
|
+
mapped.id = att.identifier;
|
|
195
|
+
}
|
|
196
|
+
return mapped;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return ios;
|
|
201
|
+
}
|
package/src/fcm/types.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Types for the FCM message handler. Declared structurally to avoid a peer
|
|
3
|
+
* dependency on @react-native-firebase/messaging.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Minimal subset of `@react-native-firebase/messaging`'s RemoteMessage that
|
|
8
|
+
* `handleFcmMessage` actually reads.
|
|
9
|
+
*/
|
|
10
|
+
export type FcmRemoteMessage = {
|
|
11
|
+
messageId?: string;
|
|
12
|
+
data?: Record<string, string>;
|
|
13
|
+
notification?: {
|
|
14
|
+
title?: string;
|
|
15
|
+
body?: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for `handleFcmMessage`. Call `setFcmConfig` once
|
|
21
|
+
* at app startup (typically in `index.js` before `registerComponent`).
|
|
22
|
+
*/
|
|
23
|
+
export type FcmConfig = {
|
|
24
|
+
/**
|
|
25
|
+
* Default channelId used when `notifee_options.android.channelId` is absent.
|
|
26
|
+
* If this is also absent and the payload has no channelId,
|
|
27
|
+
* `displayNotification` will throw on Android — same behavior as calling
|
|
28
|
+
* `displayNotification` without a channel.
|
|
29
|
+
*/
|
|
30
|
+
defaultChannelId?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default pressAction when `notifee_options.android.pressAction` is absent.
|
|
34
|
+
* Mirrors the existing behavior: `{ id: 'default', launchActivity: 'default' }`.
|
|
35
|
+
*/
|
|
36
|
+
defaultPressAction?: { id: string; launchActivity?: string };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What to do when `remoteMessage.data.notifee_options` is absent entirely.
|
|
40
|
+
* - `'display'`: build a minimal notification from `remoteMessage.notification`
|
|
41
|
+
* title/body (or `data.title`/`data.body` as a fallback). Uses `defaultChannelId`.
|
|
42
|
+
* - `'ignore'`: return `null` without displaying anything.
|
|
43
|
+
*
|
|
44
|
+
* @default 'display'
|
|
45
|
+
*/
|
|
46
|
+
fallbackBehavior?: 'display' | 'ignore';
|
|
47
|
+
|
|
48
|
+
/** iOS-specific options. */
|
|
49
|
+
ios?: {
|
|
50
|
+
/**
|
|
51
|
+
* When `true`, foreground notifications delivered via `handleFcmMessage`
|
|
52
|
+
* are NOT displayed (only events are emitted).
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
suppressForegroundBanner?: boolean;
|
|
56
|
+
};
|
|
57
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -31,3 +31,5 @@ export * from './types/Notification';
|
|
|
31
31
|
export * from './types/Trigger';
|
|
32
32
|
export * from './types/NotificationIOS';
|
|
33
33
|
export * from './types/NotificationAndroid';
|
|
34
|
+
export * from './types/PowerManagerInfo';
|
|
35
|
+
export type { FcmRemoteMessage, FcmConfig } from './fcm/types';
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FCM wire contract — shared between the server SDK (Node) and the upcoming
|
|
3
|
+
* Phase 2 client `handleFcmMessage`. Types only, no runtime code.
|
|
4
|
+
*
|
|
5
|
+
* This module is the single source of truth for the shape of `notifee_options`
|
|
6
|
+
* embedded in FCM data / APNs payloads. Both sides (server builder, client
|
|
7
|
+
* reader) import from here to guarantee parity.
|
|
8
|
+
*
|
|
9
|
+
* NOT a public API. Consumers should use `react-native-notify-kit/server` for
|
|
10
|
+
* building payloads and the main `react-native-notify-kit` module for client
|
|
11
|
+
* handling. The `internal/` directory exists to hold cross-module contracts
|
|
12
|
+
* that are not for end users.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type NotifyKitPressAction = {
|
|
16
|
+
id: string;
|
|
17
|
+
launchActivity?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type NotifyKitAndroidAction = {
|
|
21
|
+
title: string;
|
|
22
|
+
pressAction: NotifyKitPressAction;
|
|
23
|
+
input?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type NotifyKitAndroidStyle =
|
|
27
|
+
| { type: 'BIG_TEXT'; text: string }
|
|
28
|
+
| { type: 'BIG_PICTURE'; picture: string };
|
|
29
|
+
|
|
30
|
+
export type NotifyKitAndroidConfig = {
|
|
31
|
+
channelId?: string;
|
|
32
|
+
smallIcon?: string;
|
|
33
|
+
largeIcon?: string;
|
|
34
|
+
color?: string;
|
|
35
|
+
pressAction?: NotifyKitPressAction;
|
|
36
|
+
actions?: NotifyKitAndroidAction[];
|
|
37
|
+
style?: NotifyKitAndroidStyle;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type NotifyKitIosAttachment = {
|
|
41
|
+
url: string;
|
|
42
|
+
identifier?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type NotifyKitIosInterruptionLevel = 'passive' | 'active' | 'timeSensitive' | 'critical';
|
|
46
|
+
|
|
47
|
+
export type NotifyKitIosConfig = {
|
|
48
|
+
sound?: string;
|
|
49
|
+
categoryId?: string;
|
|
50
|
+
threadId?: string;
|
|
51
|
+
interruptionLevel?: NotifyKitIosInterruptionLevel;
|
|
52
|
+
attachments?: NotifyKitIosAttachment[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type NotifyKitNotification = {
|
|
56
|
+
/**
|
|
57
|
+
* Optional notification identifier. When provided, it is also used as the
|
|
58
|
+
* FCM collapse key (Android `collapse_key`, iOS `apns-collapse-id`) unless
|
|
59
|
+
* `options.collapseKey` is explicitly set. Collapse key precedence (Rule 7):
|
|
60
|
+
* 1. `options.collapseKey` if set
|
|
61
|
+
* 2. `notification.id` if set
|
|
62
|
+
* 3. Omitted (no collapse behaviour)
|
|
63
|
+
*
|
|
64
|
+
* This means setting `id` implicitly enables notification replacement on the
|
|
65
|
+
* device: a new notification with the same `id` replaces the previous one.
|
|
66
|
+
* If you need `id` for local deduplication without collapse behaviour, set a
|
|
67
|
+
* distinct `options.collapseKey` or leave both unset.
|
|
68
|
+
*/
|
|
69
|
+
id?: string;
|
|
70
|
+
title: string;
|
|
71
|
+
body: string;
|
|
72
|
+
data?: Record<string, string>;
|
|
73
|
+
android?: NotifyKitAndroidConfig;
|
|
74
|
+
ios?: NotifyKitIosConfig;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type NotifyKitOptions = {
|
|
78
|
+
androidPriority?: 'high' | 'normal';
|
|
79
|
+
iosBadgeCount?: number;
|
|
80
|
+
ttl?: number;
|
|
81
|
+
collapseKey?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type NotifyKitPayloadInput = {
|
|
85
|
+
token?: string;
|
|
86
|
+
topic?: string;
|
|
87
|
+
condition?: string;
|
|
88
|
+
notification: NotifyKitNotification;
|
|
89
|
+
options?: NotifyKitOptions;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type ApnsInterruptionLevel = 'passive' | 'active' | 'time-sensitive' | 'critical';
|
|
93
|
+
|
|
94
|
+
export type NotifyKitAndroidOutput = {
|
|
95
|
+
priority: 'HIGH' | 'NORMAL';
|
|
96
|
+
collapse_key?: string;
|
|
97
|
+
ttl?: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type NotifyKitApnsAps = {
|
|
101
|
+
alert: { title: string; body: string };
|
|
102
|
+
sound?: string;
|
|
103
|
+
category?: string;
|
|
104
|
+
'mutable-content': 1;
|
|
105
|
+
'thread-id'?: string;
|
|
106
|
+
'interruption-level'?: ApnsInterruptionLevel;
|
|
107
|
+
badge?: number;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type NotifyKitApnsHeaders = {
|
|
111
|
+
'apns-push-type': 'alert';
|
|
112
|
+
'apns-priority': '10';
|
|
113
|
+
'apns-collapse-id'?: string;
|
|
114
|
+
'apns-expiration'?: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type NotifyKitApnsPayload = {
|
|
118
|
+
aps: NotifyKitApnsAps;
|
|
119
|
+
notifee_options: string;
|
|
120
|
+
notifee_data?: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type NotifyKitApnsOutput = {
|
|
124
|
+
headers: NotifyKitApnsHeaders;
|
|
125
|
+
payload: NotifyKitApnsPayload;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type NotifyKitPayloadOutput = {
|
|
129
|
+
token?: string;
|
|
130
|
+
topic?: string;
|
|
131
|
+
condition?: string;
|
|
132
|
+
data: Record<string, string>;
|
|
133
|
+
android: NotifyKitAndroidOutput;
|
|
134
|
+
apns: NotifyKitApnsOutput;
|
|
135
|
+
sizeBytes: number;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type SerializedNotifeeOptions = {
|
|
139
|
+
_v: 1;
|
|
140
|
+
title: string;
|
|
141
|
+
body: string;
|
|
142
|
+
android?: NotifyKitAndroidConfig;
|
|
143
|
+
ios?: NotifyKitIosConfig;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Android output must never carry a `notification` field (the server enforces
|
|
147
|
+
// data-only delivery for Android to prevent FCM from auto-displaying). This
|
|
148
|
+
// invariant is exercised at runtime by the `never includes a notification
|
|
149
|
+
// field` test in buildPayload.test.ts and is enforced structurally by the
|
|
150
|
+
// shape of NotifyKitAndroidOutput above.
|
package/src/types/Module.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from './NotificationIOS';
|
|
18
18
|
import { PowerManagerInfo } from './PowerManagerInfo';
|
|
19
19
|
import { DisplayedNotification, NotificationSettings, TriggerNotification } from '..';
|
|
20
|
+
import type { FcmConfig, FcmRemoteMessage } from '../fcm/types';
|
|
20
21
|
|
|
21
22
|
export interface Module {
|
|
22
23
|
/**
|
|
@@ -660,6 +661,28 @@ export interface Module {
|
|
|
660
661
|
* @platform android
|
|
661
662
|
*/
|
|
662
663
|
hideNotificationDrawer(): void;
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Processes an FCM remote message produced by the NotifyKit server SDK and
|
|
667
|
+
* displays a Notifee notification according to the embedded `notifee_options`.
|
|
668
|
+
*
|
|
669
|
+
* Safe to call from both `setBackgroundMessageHandler` and `onMessage`.
|
|
670
|
+
*
|
|
671
|
+
* @returns The displayed notification ID, or `null` if the call was an
|
|
672
|
+
* intentional no-op (iOS background, `fallbackBehavior: 'ignore'`,
|
|
673
|
+
* `suppressForegroundBanner`).
|
|
674
|
+
*/
|
|
675
|
+
handleFcmMessage(remoteMessage: FcmRemoteMessage): Promise<string | null>;
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Configures defaults for {@link handleFcmMessage}. Call once at app startup,
|
|
679
|
+
* typically in `index.js` before `registerComponent`.
|
|
680
|
+
*
|
|
681
|
+
* Returns Promise for forward compatibility — a future version may persist
|
|
682
|
+
* config across cold starts, which would be async. Currently resolves
|
|
683
|
+
* synchronously.
|
|
684
|
+
*/
|
|
685
|
+
setFcmConfig(config: FcmConfig): Promise<void>;
|
|
663
686
|
}
|
|
664
687
|
|
|
665
688
|
/**
|
|
@@ -52,11 +52,35 @@ export default function validateTrigger(trigger: Trigger): Trigger {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Smallest plausible epoch-ms value for a future trigger. 1e12 ms ≈ Sep 2001;
|
|
56
|
+
// anything smaller is almost certainly the wrong unit (seconds-since-epoch,
|
|
57
|
+
// or a Date.getDate() day-of-month). We intercept the three common shapes
|
|
58
|
+
// and emit targeted hints — see upstream invertase/notifee#872 for the
|
|
59
|
+
// year-after-year UX pattern this guards against.
|
|
60
|
+
const MIN_PLAUSIBLE_EPOCH_MS = 1e12;
|
|
61
|
+
const MIN_PLAUSIBLE_EPOCH_SECONDS = 1e9;
|
|
62
|
+
|
|
55
63
|
function validateTimestampTrigger(trigger: TimestampTrigger): TimestampTrigger {
|
|
56
64
|
if (!isNumber(trigger.timestamp)) {
|
|
57
65
|
throw new Error("'trigger.timestamp' expected a number value.");
|
|
58
66
|
}
|
|
59
67
|
|
|
68
|
+
if (trigger.timestamp < MIN_PLAUSIBLE_EPOCH_MS) {
|
|
69
|
+
if (trigger.timestamp >= 1 && trigger.timestamp <= 31) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`'trigger.timestamp' looks like a day-of-month (${trigger.timestamp}). Did you mean \`date.getTime()\` instead of \`date.getDate()\`?`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (trigger.timestamp >= MIN_PLAUSIBLE_EPOCH_SECONDS) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`'trigger.timestamp' looks like seconds since epoch (${trigger.timestamp}). Notifee expects milliseconds — multiply by 1000, or use \`Date.now()\` / \`date.getTime()\`.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
throw new Error(
|
|
80
|
+
`'trigger.timestamp' (${trigger.timestamp}) is too small to be a valid epoch millisecond value. Use \`Date.now()\` or \`someDate.getTime()\`.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
60
84
|
const now = Date.now();
|
|
61
85
|
if (trigger.timestamp <= now) {
|
|
62
86
|
throw new Error("'trigger.timestamp' date must be in the future.");
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '
|
|
2
|
+
export const version = '10.1.0';
|