react-native-notify-sphere 1.0.0 → 1.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 +1080 -15
- package/android/src/main/AndroidManifest.xml +8 -1
- package/android/src/main/java/com/notifysphere/NotifySphereModule.kt +0 -6
- package/ios/NotifySphere.mm +0 -6
- package/lib/module/NativeNotifySphere.js +11 -0
- package/lib/module/NativeNotifySphere.js.map +1 -1
- package/lib/module/index.js +463 -105
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeNotifySphere.d.ts +11 -1
- package/lib/typescript/src/NativeNotifySphere.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +213 -25
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +9 -11
- package/src/NativeNotifySphere.ts +12 -3
- package/src/index.tsx +670 -154
package/src/index.tsx
CHANGED
|
@@ -2,63 +2,223 @@ import messaging from '@react-native-firebase/messaging';
|
|
|
2
2
|
import notifee, {
|
|
3
3
|
AndroidImportance,
|
|
4
4
|
AndroidStyle,
|
|
5
|
-
EventType,
|
|
5
|
+
EventType,
|
|
6
|
+
type AndroidChannel,
|
|
6
7
|
} from '@notifee/react-native';
|
|
7
8
|
import axios from 'axios';
|
|
9
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
8
10
|
import { PermissionsAndroid, Platform } from 'react-native';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
const STORAGE_KEY_TOKEN = '@notifysphere_fcm_token';
|
|
13
|
+
const STORAGE_KEY_SUB_ID = '@notifysphere_subscription_id';
|
|
14
|
+
const STORAGE_KEY_USER_HASH = '@notifysphere_user_hash';
|
|
15
|
+
|
|
16
|
+
// ─── Type definitions ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/** Shape of a Firebase Cloud Messaging remote message */
|
|
19
|
+
export interface FirebaseRemoteMessage {
|
|
20
|
+
messageId?: string;
|
|
21
|
+
notification?: {
|
|
22
|
+
title?: string;
|
|
23
|
+
body?: string;
|
|
24
|
+
android?: { imageUrl?: string };
|
|
25
|
+
ios?: { attachments?: Array<{ url?: string }> };
|
|
26
|
+
imageUrl?: string;
|
|
27
|
+
};
|
|
28
|
+
data?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Shape of a Notifee notification object (returned from press events) */
|
|
32
|
+
export interface NotifeeNotification {
|
|
33
|
+
id?: string;
|
|
34
|
+
title?: string;
|
|
35
|
+
body?: string;
|
|
36
|
+
data?: Record<string, string>;
|
|
37
|
+
android?: { largeIcon?: string };
|
|
38
|
+
ios?: { attachments?: Array<{ url?: string }> };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Normalised notification data delivered to the consumer callback */
|
|
42
|
+
export type NotificationData = {
|
|
12
43
|
title?: string;
|
|
13
44
|
body?: string;
|
|
14
|
-
data?:
|
|
45
|
+
data?: Record<string, string>;
|
|
15
46
|
image?: string;
|
|
16
47
|
sound?: string;
|
|
17
48
|
};
|
|
18
49
|
|
|
19
|
-
type
|
|
20
|
-
[key: string]: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type NotificationCallback = (
|
|
50
|
+
export type NotificationCallback = (
|
|
24
51
|
notification: NotificationData,
|
|
25
52
|
type?: string
|
|
26
53
|
) => void;
|
|
27
54
|
|
|
55
|
+
export type InitializeConfig = {
|
|
56
|
+
/** Your application's user ID for this device */
|
|
57
|
+
applicationUserId: number;
|
|
58
|
+
/** Push type, e.g. 'AndroidPush' or 'IOSPush' */
|
|
59
|
+
type: string;
|
|
60
|
+
/** NotifySphere app ID (required) */
|
|
61
|
+
appId: string;
|
|
62
|
+
name?: string;
|
|
63
|
+
lat?: string;
|
|
64
|
+
long?: string;
|
|
65
|
+
city?: string;
|
|
66
|
+
state?: string;
|
|
67
|
+
email?: string;
|
|
68
|
+
/** Phone number as a string to preserve leading zeros and country codes */
|
|
69
|
+
phone?: string;
|
|
70
|
+
tags?: Record<string, string>;
|
|
71
|
+
/**
|
|
72
|
+
* Base URL of your NotifySphere API server.
|
|
73
|
+
* Defaults to the hosted service.
|
|
74
|
+
*/
|
|
75
|
+
baseUrl?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Full URL of your onpress-tracking endpoint.
|
|
78
|
+
* Defaults to the hosted service.
|
|
79
|
+
*/
|
|
80
|
+
trackingUrl?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Full URL of your delivery-confirmation endpoint.
|
|
83
|
+
* Called as soon as a notification is received (foreground, background, or
|
|
84
|
+
* terminated — see note below).
|
|
85
|
+
* Defaults to the hosted service.
|
|
86
|
+
*
|
|
87
|
+
* NOTE: delivery confirmation when the app is terminated only works for
|
|
88
|
+
* **data-only** FCM messages (no `notification` field in the payload).
|
|
89
|
+
* If the payload contains a `notification` field, the OS delivers it
|
|
90
|
+
* silently without waking the app, so this callback cannot fire.
|
|
91
|
+
*/
|
|
92
|
+
deliveryUrl?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Optional API key sent as a Bearer token to the tracking endpoints.
|
|
95
|
+
* Never hard-code this — read it from a secure config or environment.
|
|
96
|
+
*/
|
|
97
|
+
apiKey?: string;
|
|
98
|
+
/** Enable verbose logging. Defaults to false. */
|
|
99
|
+
debug?: boolean;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ─── Internal logger ──────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
let _debug = false;
|
|
105
|
+
|
|
106
|
+
const log = (...args: unknown[]) => {
|
|
107
|
+
if (_debug) console.log('[NotifySphere]', ...args);
|
|
108
|
+
};
|
|
109
|
+
const warn = (...args: unknown[]) => {
|
|
110
|
+
if (_debug) console.warn('[NotifySphere]', ...args);
|
|
111
|
+
};
|
|
112
|
+
// Errors are always surfaced regardless of the debug flag
|
|
113
|
+
const logError = (...args: unknown[]) =>
|
|
114
|
+
console.error('[NotifySphere]', ...args);
|
|
115
|
+
|
|
116
|
+
// ─── Default endpoints ────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const DEFAULT_BASE_URL = 'https://api.notifysphere.in';
|
|
119
|
+
// const DEFAULT_BASE_URL = 'https://apinotify.dothejob.in';
|
|
120
|
+
const DEFAULT_TRACKING_URL =
|
|
121
|
+
DEFAULT_BASE_URL+'/onpress/handle';
|
|
122
|
+
const DEFAULT_DELIVERY_URL =
|
|
123
|
+
DEFAULT_BASE_URL+'/delivery/confirm';;
|
|
124
|
+
|
|
125
|
+
// ─── NotifySphere class ───────────────────────────────────────────────────────
|
|
126
|
+
|
|
28
127
|
class NotifySphere {
|
|
29
128
|
private static callback: NotificationCallback | null = null;
|
|
129
|
+
private static fcmToken: string | null = null;
|
|
130
|
+
private static appId: string | null = null;
|
|
131
|
+
private static subscriptionId: string | null = null;
|
|
132
|
+
private static baseUrl: string = DEFAULT_BASE_URL;
|
|
133
|
+
private static trackingUrl: string = DEFAULT_TRACKING_URL;
|
|
134
|
+
private static deliveryUrl: string = DEFAULT_DELIVERY_URL;
|
|
135
|
+
private static apiKey: string | null = null;
|
|
136
|
+
private static unsubscribers: Array<() => void> = [];
|
|
137
|
+
private static initialized = false;
|
|
138
|
+
private static createdChannels = new Set<string>();
|
|
30
139
|
|
|
31
|
-
//
|
|
140
|
+
// ─── Press tracking ──────────────────────────────────────────────────────
|
|
32
141
|
|
|
33
|
-
|
|
34
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Tracks a notification press event and fires the consumer callback.
|
|
144
|
+
* Accepts both Firebase remote messages and Notifee notification objects.
|
|
145
|
+
*/
|
|
146
|
+
static async callbackOnpress(
|
|
147
|
+
notification: FirebaseRemoteMessage | NotifeeNotification,
|
|
148
|
+
status: string
|
|
149
|
+
): Promise<void> {
|
|
35
150
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
151
|
+
const notificationId =
|
|
152
|
+
(notification as FirebaseRemoteMessage).data?.notification_id ??
|
|
153
|
+
(notification as NotifeeNotification).data?.notification_id;
|
|
40
154
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
maxBodyLength: Infinity,
|
|
44
|
-
url: 'https://notifysphere.dothejob.in:3008/onpress/handle',
|
|
45
|
-
headers: {
|
|
46
|
-
'Content-Type': 'application/json',
|
|
47
|
-
'Cookie':
|
|
48
|
-
'token=sYPT8POivlxfFSR54MWd3reaArvODMiIcM4KM39cqkXBLVu%2B8b1zV2csabU9byzo',
|
|
49
|
-
},
|
|
50
|
-
data,
|
|
155
|
+
const headers: Record<string, string> = {
|
|
156
|
+
'Content-Type': 'application/json',
|
|
51
157
|
};
|
|
158
|
+
if (NotifySphere.apiKey) {
|
|
159
|
+
headers['Authorization'] = `Bearer ${NotifySphere.apiKey}`;
|
|
160
|
+
}
|
|
52
161
|
|
|
53
|
-
const response = await axios.
|
|
54
|
-
|
|
162
|
+
const response = await axios.post(
|
|
163
|
+
NotifySphere.trackingUrl,
|
|
164
|
+
{
|
|
165
|
+
notification_id: notificationId,
|
|
166
|
+
subscription_id: NotifySphere.subscriptionId,
|
|
167
|
+
onPressStatus: 1,
|
|
168
|
+
},
|
|
169
|
+
{ headers, maxBodyLength: Infinity }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
log('onPress tracking response:', response.data);
|
|
173
|
+
NotifySphere.sendCallback(notification, status);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
logError('Error in callbackOnpress:', err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Delivery confirmation ───────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Sends a delivery receipt to the server as soon as a notification arrives.
|
|
183
|
+
*
|
|
184
|
+
* This fires in three scenarios:
|
|
185
|
+
* - Foreground : app is open when the message arrives
|
|
186
|
+
* - Background : app is alive in the background (data-only FCM message)
|
|
187
|
+
* - Terminated : app is completely killed (data-only FCM message only —
|
|
188
|
+
* notification messages are shown by the OS without waking
|
|
189
|
+
* the app, so this cannot fire in that case)
|
|
190
|
+
*/
|
|
191
|
+
private static async callbackOnDelivery(
|
|
192
|
+
remoteMessage: FirebaseRemoteMessage
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
const notificationId = remoteMessage.data?.notification_id;
|
|
196
|
+
|
|
197
|
+
const headers: Record<string, string> = {
|
|
198
|
+
'Content-Type': 'application/json',
|
|
199
|
+
};
|
|
200
|
+
if (NotifySphere.apiKey) {
|
|
201
|
+
headers['Authorization'] = `Bearer ${NotifySphere.apiKey}`;
|
|
202
|
+
}
|
|
203
|
+
console.log("NotifySphere.deliveryUrl",NotifySphere.deliveryUrl);
|
|
204
|
+
const response = await axios.post(
|
|
205
|
+
NotifySphere.deliveryUrl,
|
|
206
|
+
{
|
|
207
|
+
notification_id: notificationId,
|
|
208
|
+
subscription_id: NotifySphere.subscriptionId,
|
|
209
|
+
onDeliveredStatus: 1,
|
|
210
|
+
},
|
|
211
|
+
{ headers, maxBodyLength: Infinity }
|
|
212
|
+
);
|
|
55
213
|
|
|
56
|
-
|
|
57
|
-
} catch (
|
|
58
|
-
|
|
214
|
+
log('Delivery confirmation response:', response.data);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
logError('Error sending delivery confirmation:', err);
|
|
59
217
|
}
|
|
60
218
|
}
|
|
61
|
-
|
|
219
|
+
|
|
220
|
+
// ─── Permissions ─────────────────────────────────────────────────────────
|
|
221
|
+
|
|
62
222
|
static async checkApplicationPermission(): Promise<boolean> {
|
|
63
223
|
try {
|
|
64
224
|
if (Platform.OS === 'android' && Platform.Version >= 33) {
|
|
@@ -74,165 +234,486 @@ class NotifySphere {
|
|
|
74
234
|
);
|
|
75
235
|
}
|
|
76
236
|
return true;
|
|
77
|
-
} catch (
|
|
78
|
-
|
|
237
|
+
} catch (err) {
|
|
238
|
+
logError('Error requesting notification permission:', err);
|
|
79
239
|
return false;
|
|
80
240
|
}
|
|
81
241
|
}
|
|
82
242
|
|
|
243
|
+
// ─── User details fingerprint ─────────────────────────────────────────────
|
|
83
244
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Builds a stable string fingerprint of all user-profile fields.
|
|
247
|
+
* Used to detect whether any detail has changed since the last registration,
|
|
248
|
+
* so the API is only called when something actually differs.
|
|
249
|
+
*/
|
|
250
|
+
private static buildUserHash(config: InitializeConfig): string {
|
|
251
|
+
const fields = {
|
|
252
|
+
applicationUserId: config.applicationUserId,
|
|
253
|
+
type: config.type,
|
|
254
|
+
name: config.name ?? '',
|
|
255
|
+
lat: config.lat ?? '',
|
|
256
|
+
long: config.long ?? '',
|
|
257
|
+
city: config.city ?? '',
|
|
258
|
+
state: config.state ?? '',
|
|
259
|
+
email: config.email ?? '',
|
|
260
|
+
phone: config.phone ?? '',
|
|
261
|
+
// Stringify tags with sorted keys so order doesn't affect the hash
|
|
262
|
+
tags: config.tags
|
|
263
|
+
? JSON.stringify(
|
|
264
|
+
Object.keys(config.tags)
|
|
265
|
+
.sort()
|
|
266
|
+
.reduce<Record<string, string>>((acc, k) => {
|
|
267
|
+
acc[k] = config.tags![k]!;
|
|
268
|
+
return acc;
|
|
269
|
+
}, {})
|
|
270
|
+
)
|
|
271
|
+
: '',
|
|
272
|
+
};
|
|
273
|
+
return JSON.stringify(fields);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Initialize ──────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Initialises the SDK and sets up notification listeners.
|
|
280
|
+
*
|
|
281
|
+
* Safe to call every time the app opens — it only hits the registration API
|
|
282
|
+
* when the FCM token OR any user detail (email, phone, location, tags, etc.)
|
|
283
|
+
* has changed since the last successful registration.
|
|
284
|
+
* Listeners are registered only once per process lifetime.
|
|
285
|
+
*
|
|
286
|
+
* Token changes are also handled automatically via `onTokenRefresh`.
|
|
287
|
+
*/
|
|
288
|
+
static async initialize(
|
|
289
|
+
config: InitializeConfig
|
|
290
|
+
): Promise<string | undefined> {
|
|
291
|
+
_debug = config.debug ?? false;
|
|
292
|
+
NotifySphere.appId = config.appId;
|
|
293
|
+
NotifySphere.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
294
|
+
NotifySphere.trackingUrl = config.trackingUrl ?? DEFAULT_TRACKING_URL;
|
|
295
|
+
NotifySphere.deliveryUrl = config.deliveryUrl ?? DEFAULT_DELIVERY_URL;
|
|
296
|
+
NotifySphere.apiKey = config.apiKey ?? null;
|
|
297
|
+
|
|
298
|
+
const hasPermission = await NotifySphere.checkApplicationPermission();
|
|
299
|
+
if (!hasPermission) {
|
|
300
|
+
warn('Notification permission not granted — aborting initialization');
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const fcmToken = await messaging().getToken();
|
|
306
|
+
NotifySphere.fcmToken = fcmToken;
|
|
307
|
+
|
|
308
|
+
// ── Restore cached subscription_id immediately so tracking calls
|
|
309
|
+
// have it available even before the registration API responds.
|
|
310
|
+
const cachedSubId = await AsyncStorage.getItem(STORAGE_KEY_SUB_ID);
|
|
311
|
+
if (cachedSubId) {
|
|
312
|
+
NotifySphere.subscriptionId = cachedSubId;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Only call the registration API when the FCM token OR any user
|
|
316
|
+
// detail has changed. This avoids a redundant network call on every
|
|
317
|
+
// app open while still syncing updates (email, phone, location, etc.)
|
|
318
|
+
const [cachedToken, cachedUserHash] = await Promise.all([
|
|
319
|
+
AsyncStorage.getItem(STORAGE_KEY_TOKEN),
|
|
320
|
+
AsyncStorage.getItem(STORAGE_KEY_USER_HASH),
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
const currentUserHash = NotifySphere.buildUserHash(config);
|
|
324
|
+
let subscriptionId: string | undefined = cachedSubId ?? undefined;
|
|
325
|
+
|
|
326
|
+
const tokenChanged = fcmToken !== cachedToken;
|
|
327
|
+
const userChanged = currentUserHash !== cachedUserHash;
|
|
328
|
+
|
|
329
|
+
if (tokenChanged || userChanged) {
|
|
330
|
+
if (tokenChanged) log('FCM token changed — re-registering device');
|
|
331
|
+
if (userChanged) log('User details changed — re-registering device');
|
|
332
|
+
subscriptionId = await NotifySphere.registerDevice(
|
|
333
|
+
config,
|
|
334
|
+
fcmToken,
|
|
335
|
+
currentUserHash
|
|
336
|
+
);
|
|
337
|
+
} else {
|
|
338
|
+
log('Token and user details unchanged — skipping registration API call');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── Channel and listeners are always set up (idempotent)
|
|
342
|
+
await NotifySphere.ensureChannel(
|
|
343
|
+
'channel_default',
|
|
344
|
+
'Default Channel',
|
|
345
|
+
'default'
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (!NotifySphere.initialized) {
|
|
349
|
+
NotifySphere.setupListeners();
|
|
350
|
+
// Watch for token refreshes (Firebase can rotate the token silently)
|
|
351
|
+
NotifySphere.watchTokenRefresh(config);
|
|
352
|
+
NotifySphere.initialized = true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return subscriptionId;
|
|
356
|
+
} catch (err) {
|
|
357
|
+
logError('Error during initialization:', err);
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
94
360
|
}
|
|
95
361
|
|
|
96
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Calls the registration API and persists the token, subscription_id, and
|
|
364
|
+
* user hash so future launches can detect what has changed.
|
|
365
|
+
* Extracted so it can also be called from the token-refresh watcher.
|
|
366
|
+
*/
|
|
367
|
+
private static async registerDevice(
|
|
368
|
+
config: InitializeConfig,
|
|
369
|
+
fcmToken: string,
|
|
370
|
+
userHash?: string
|
|
371
|
+
): Promise<string | undefined> {
|
|
372
|
+
const res = await axios.post(
|
|
373
|
+
`${NotifySphere.baseUrl}/apps/${config.appId}/users`,
|
|
374
|
+
{
|
|
375
|
+
applicationUserId: config.applicationUserId,
|
|
376
|
+
token: fcmToken,
|
|
377
|
+
type: config.type,
|
|
378
|
+
user: {
|
|
379
|
+
userName: config.name,
|
|
380
|
+
lat: config.lat,
|
|
381
|
+
long: config.long,
|
|
382
|
+
city: config.city,
|
|
383
|
+
state: config.state,
|
|
384
|
+
email: config.email,
|
|
385
|
+
phone: config.phone,
|
|
386
|
+
tags: config.tags,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
headers: { 'Content-Type': 'application/json' },
|
|
391
|
+
maxBodyLength: Infinity,
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
log('Device registration response:', res.data);
|
|
396
|
+
const subscriptionId: string | undefined = res.data?.subscription_id;
|
|
397
|
+
NotifySphere.subscriptionId = subscriptionId ?? null;
|
|
398
|
+
|
|
399
|
+
// Persist token, subscription_id, and user hash for future launch comparisons
|
|
400
|
+
const hash = userHash ?? NotifySphere.buildUserHash(config);
|
|
401
|
+
await Promise.all([
|
|
402
|
+
AsyncStorage.setItem(STORAGE_KEY_TOKEN, fcmToken),
|
|
403
|
+
AsyncStorage.setItem(STORAGE_KEY_USER_HASH, hash),
|
|
404
|
+
subscriptionId
|
|
405
|
+
? AsyncStorage.setItem(STORAGE_KEY_SUB_ID, subscriptionId)
|
|
406
|
+
: Promise.resolve(),
|
|
407
|
+
]);
|
|
408
|
+
|
|
409
|
+
return subscriptionId;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Listens for Firebase token refreshes and automatically re-registers
|
|
414
|
+
* the device when the token rotates, without requiring the consumer to
|
|
415
|
+
* call initialize() again.
|
|
416
|
+
*/
|
|
417
|
+
private static watchTokenRefresh(config: InitializeConfig): void {
|
|
418
|
+
const unsubRefresh = messaging().onTokenRefresh(async (newToken) => {
|
|
419
|
+
log('FCM token refreshed — re-registering device');
|
|
420
|
+
NotifySphere.fcmToken = newToken;
|
|
421
|
+
try {
|
|
422
|
+
// User details haven't changed here, reuse the existing hash
|
|
423
|
+
const existingHash =
|
|
424
|
+
(await AsyncStorage.getItem(STORAGE_KEY_USER_HASH)) ??
|
|
425
|
+
NotifySphere.buildUserHash(config);
|
|
426
|
+
await NotifySphere.registerDevice(config, newToken, existingHash);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
logError('Error re-registering after token refresh:', err);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
NotifySphere.unsubscribers.push(unsubRefresh);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── Update tags ─────────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Updates user tags without re-registering the device.
|
|
438
|
+
* `appId` is optional here — it falls back to the value passed in `initialize`.
|
|
439
|
+
*/
|
|
440
|
+
static async updateTags(params: {
|
|
97
441
|
applicationUserId: number;
|
|
98
442
|
type: string;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
long?: string;
|
|
102
|
-
city?: string;
|
|
103
|
-
state?: string;
|
|
104
|
-
email?: string;
|
|
443
|
+
tags: Record<string, string>;
|
|
444
|
+
/** Falls back to the appId provided in initialize() */
|
|
105
445
|
appId?: string;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}) {
|
|
109
|
-
const hasPermission = await NotifySphere.checkApplicationPermission();
|
|
110
|
-
const devInfo = await NotifySphere.getDeviceInfo();
|
|
111
|
-
if (!hasPermission) return;
|
|
446
|
+
}): Promise<unknown> {
|
|
447
|
+
const appId = params.appId ?? NotifySphere.appId;
|
|
112
448
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
449
|
+
if (!NotifySphere.fcmToken) {
|
|
450
|
+
warn('FCM token missing — cannot update tags');
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
if (!appId) {
|
|
454
|
+
warn('appId missing — cannot update tags');
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
117
457
|
|
|
118
|
-
|
|
119
|
-
.post(
|
|
120
|
-
|
|
458
|
+
try {
|
|
459
|
+
const res = await axios.post(
|
|
460
|
+
`${NotifySphere.baseUrl}/apps/${appId}/users`,
|
|
121
461
|
{
|
|
122
|
-
applicationUserId:
|
|
123
|
-
token: fcmToken,
|
|
124
|
-
type:
|
|
125
|
-
user: {
|
|
126
|
-
userName: config.name,
|
|
127
|
-
lat: config.lat,
|
|
128
|
-
long: config.long,
|
|
129
|
-
city: config.city,
|
|
130
|
-
state: config.state,
|
|
131
|
-
email: config.email,
|
|
132
|
-
phone: config.phone,
|
|
133
|
-
tags: config.tags,
|
|
134
|
-
sdk: devInfo.sdk,
|
|
135
|
-
device_model: devInfo.device_model,
|
|
136
|
-
device_os: devInfo.device_os,
|
|
137
|
-
device_version: devInfo.device_version,
|
|
138
|
-
carrier: devInfo.carrier,
|
|
139
|
-
app_version: devInfo.app_version,
|
|
140
|
-
timezone_id: devInfo.timezone_id
|
|
141
|
-
},
|
|
462
|
+
applicationUserId: params.applicationUserId,
|
|
463
|
+
token: NotifySphere.fcmToken,
|
|
464
|
+
type: params.type,
|
|
465
|
+
user: { tags: params.tags },
|
|
142
466
|
},
|
|
143
467
|
{
|
|
144
468
|
headers: { 'Content-Type': 'application/json' },
|
|
145
469
|
maxBodyLength: Infinity,
|
|
146
470
|
}
|
|
147
|
-
)
|
|
148
|
-
.then(async (res: any) => {
|
|
149
|
-
console.log('res313132', res);
|
|
150
|
-
return res.data.subscription_id;
|
|
151
|
-
})
|
|
152
|
-
.catch((err) => {
|
|
153
|
-
console.log('err31232', err);
|
|
154
|
-
});
|
|
471
|
+
);
|
|
155
472
|
|
|
156
|
-
|
|
157
|
-
|
|
473
|
+
log('Tags update response:', res.data);
|
|
474
|
+
return res.data;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
logError('Error updating tags:', err);
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
158
479
|
}
|
|
159
480
|
|
|
160
|
-
|
|
481
|
+
// ─── Public callback setter ───────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
/** Register a callback to receive notification events (received / opened / initial). */
|
|
484
|
+
static onNotification(callback: NotificationCallback): void {
|
|
161
485
|
NotifySphere.callback = callback;
|
|
162
486
|
}
|
|
163
487
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
488
|
+
// ─── Background handler ───────────────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Registers Firebase and Notifee background handlers.
|
|
492
|
+
*
|
|
493
|
+
* **IMPORTANT:** Call this in your root `index.js` file, before
|
|
494
|
+
* `AppRegistry.registerComponent()`, so it is available when the app wakes
|
|
495
|
+
* in the background — or starts from a terminated state — to handle a
|
|
496
|
+
* notification.
|
|
497
|
+
*
|
|
498
|
+
* When the app is **terminated**, React Native only runs `index.js` (a
|
|
499
|
+
* headless JS task). Your app components and `initialize()` never execute,
|
|
500
|
+
* so any custom `deliveryUrl` / `apiKey` you passed to `initialize()` are
|
|
501
|
+
* not available. Pass them here instead so they are set before the handler
|
|
502
|
+
* fires.
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* // index.js
|
|
506
|
+
* import { AppRegistry } from 'react-native';
|
|
507
|
+
* import NotifySphere from 'react-native-notify-sphere';
|
|
508
|
+
* import App from './App';
|
|
509
|
+
*
|
|
510
|
+
* NotifySphere.setBackgroundHandler({
|
|
511
|
+
* deliveryUrl: 'https://your-server.com/ondelivery/handle', // optional
|
|
512
|
+
* apiKey: 'your-api-key', // optional
|
|
513
|
+
* });
|
|
514
|
+
* AppRegistry.registerComponent('MyApp', () => App);
|
|
515
|
+
*/
|
|
516
|
+
static setBackgroundHandler(config?: {
|
|
517
|
+
/**
|
|
518
|
+
* Override the delivery-confirmation URL for background/terminated state.
|
|
519
|
+
* Required if you use a custom deliveryUrl AND need it to work when the
|
|
520
|
+
* app is terminated (since initialize() won't have run yet).
|
|
521
|
+
*/
|
|
522
|
+
deliveryUrl?: string;
|
|
523
|
+
/**
|
|
524
|
+
* Override the API key for background/terminated state.
|
|
525
|
+
* Required if you use apiKey AND need delivery receipts when terminated.
|
|
526
|
+
*/
|
|
527
|
+
apiKey?: string;
|
|
528
|
+
/**
|
|
529
|
+
* The subscription ID returned by initialize().
|
|
530
|
+
* Required for delivery receipts to include subscription_id when the app
|
|
531
|
+
* is terminated (since initialize() won't have run yet).
|
|
532
|
+
* Persist this value (e.g. in AsyncStorage) and pass it here.
|
|
533
|
+
*/
|
|
534
|
+
subscriptionId?: string;
|
|
535
|
+
}): void {
|
|
536
|
+
// Apply any config overrides immediately — before the handler fires —
|
|
537
|
+
// so they are in place even when the app starts from a terminated state.
|
|
538
|
+
if (config?.deliveryUrl) {
|
|
539
|
+
NotifySphere.deliveryUrl = config.deliveryUrl;
|
|
540
|
+
}
|
|
541
|
+
if (config?.apiKey) {
|
|
542
|
+
NotifySphere.apiKey = config.apiKey;
|
|
543
|
+
}
|
|
544
|
+
if (config?.subscriptionId) {
|
|
545
|
+
NotifySphere.subscriptionId = config.subscriptionId;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
|
|
549
|
+
const msg = remoteMessage as FirebaseRemoteMessage;
|
|
550
|
+
// Run both in parallel — display and delivery receipt
|
|
551
|
+
await Promise.all([
|
|
552
|
+
NotifySphere.displayLocalNotification(msg),
|
|
553
|
+
NotifySphere.callbackOnDelivery(msg),
|
|
554
|
+
]);
|
|
167
555
|
});
|
|
168
556
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
557
|
+
notifee.onBackgroundEvent(async ({ type, detail }) => {
|
|
558
|
+
log('Background notifee event, type:', type);
|
|
559
|
+
if (type === EventType.PRESS && detail.notification) {
|
|
560
|
+
await NotifySphere.callbackOnpress(
|
|
561
|
+
detail.notification as NotifeeNotification,
|
|
562
|
+
'press'
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ─── Cleanup ──────────────────────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Unsubscribes all active listeners and resets internal state.
|
|
572
|
+
* Call this when you want to stop receiving notifications (e.g. on logout).
|
|
573
|
+
*/
|
|
574
|
+
static destroy(): void {
|
|
575
|
+
for (const unsub of NotifySphere.unsubscribers) {
|
|
576
|
+
unsub();
|
|
577
|
+
}
|
|
578
|
+
NotifySphere.unsubscribers = [];
|
|
579
|
+
NotifySphere.initialized = false;
|
|
580
|
+
log('NotifySphere destroyed — all listeners removed');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ─── Private: listeners ───────────────────────────────────────────────────
|
|
584
|
+
|
|
585
|
+
private static setupListeners(): void {
|
|
586
|
+
// Foreground messages — display, send delivery receipt, and fire callback
|
|
587
|
+
const unsubMessage = messaging().onMessage(async (remoteMessage) => {
|
|
588
|
+
const msg = remoteMessage as FirebaseRemoteMessage;
|
|
589
|
+
log('Foreground message received:', msg);
|
|
590
|
+
// Display and delivery receipt run in parallel for speed
|
|
591
|
+
|
|
592
|
+
await Promise.all([
|
|
593
|
+
NotifySphere.displayLocalNotification(msg),
|
|
594
|
+
NotifySphere.callbackOnDelivery(msg),
|
|
595
|
+
]);
|
|
596
|
+
NotifySphere.sendCallback(msg, 'received');
|
|
172
597
|
});
|
|
173
598
|
|
|
599
|
+
// App brought from background via notification tap
|
|
600
|
+
const unsubOpened = messaging().onNotificationOpenedApp(
|
|
601
|
+
async (remoteMessage) => {
|
|
602
|
+
log(
|
|
603
|
+
'App opened from background via notification:',
|
|
604
|
+
remoteMessage.messageId
|
|
605
|
+
);
|
|
606
|
+
await NotifySphere.callbackOnpress(
|
|
607
|
+
remoteMessage as FirebaseRemoteMessage,
|
|
608
|
+
'opened'
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
// App launched from a quit state via notification tap
|
|
174
614
|
messaging()
|
|
175
615
|
.getInitialNotification()
|
|
176
616
|
.then((remoteMessage) => {
|
|
177
617
|
if (remoteMessage) {
|
|
178
|
-
NotifySphere.callbackOnpress(
|
|
618
|
+
NotifySphere.callbackOnpress(
|
|
619
|
+
remoteMessage as FirebaseRemoteMessage,
|
|
620
|
+
'initial'
|
|
621
|
+
);
|
|
179
622
|
}
|
|
180
|
-
})
|
|
623
|
+
})
|
|
624
|
+
.catch((err) => logError('getInitialNotification error:', err));
|
|
181
625
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
NotifySphere.callbackOnpress(
|
|
626
|
+
// Notifee foreground press events (covers in-app notification taps)
|
|
627
|
+
const unsubForeground = notifee.onForegroundEvent(({ type, detail }) => {
|
|
628
|
+
if (type === EventType.PRESS && detail.notification) {
|
|
629
|
+
NotifySphere.callbackOnpress(
|
|
630
|
+
detail.notification as NotifeeNotification,
|
|
631
|
+
'opened'
|
|
632
|
+
);
|
|
186
633
|
}
|
|
187
634
|
});
|
|
188
635
|
|
|
189
|
-
|
|
190
|
-
|
|
636
|
+
NotifySphere.unsubscribers.push(
|
|
637
|
+
unsubMessage,
|
|
638
|
+
unsubOpened,
|
|
639
|
+
unsubForeground
|
|
640
|
+
);
|
|
641
|
+
}
|
|
191
642
|
|
|
192
|
-
|
|
193
|
-
console.log('detail323213', detail);
|
|
643
|
+
// ─── Private: channel management ─────────────────────────────────────────
|
|
194
644
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
645
|
+
/**
|
|
646
|
+
* Creates a notification channel if it hasn't been created yet this session.
|
|
647
|
+
* Notifee handles idempotency across app restarts; this in-memory guard
|
|
648
|
+
* prevents redundant async calls within the same session.
|
|
649
|
+
*/
|
|
650
|
+
private static async ensureChannel(
|
|
651
|
+
id: string,
|
|
652
|
+
name: string,
|
|
653
|
+
sound: string
|
|
654
|
+
): Promise<void> {
|
|
655
|
+
if (NotifySphere.createdChannels.has(id)) return;
|
|
656
|
+
|
|
657
|
+
const soundName = sound.replace(/\..+$/, '');
|
|
658
|
+
const channelDef: AndroidChannel = {
|
|
659
|
+
id,
|
|
660
|
+
name,
|
|
661
|
+
importance: AndroidImportance.HIGH,
|
|
662
|
+
sound: soundName !== 'default' ? soundName : undefined,
|
|
663
|
+
lights: true,
|
|
664
|
+
lightColor: '#0000FF',
|
|
665
|
+
vibration: true,
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
await notifee.createChannel(channelDef);
|
|
669
|
+
NotifySphere.createdChannels.add(id);
|
|
670
|
+
log('Android channel created/verified:', id);
|
|
198
671
|
}
|
|
199
672
|
|
|
200
|
-
|
|
673
|
+
// ─── Private: display notification ───────────────────────────────────────
|
|
674
|
+
|
|
675
|
+
private static async displayLocalNotification(
|
|
676
|
+
remoteMessage: FirebaseRemoteMessage
|
|
677
|
+
): Promise<void> {
|
|
678
|
+
// Use || so empty strings fall back just like null/undefined
|
|
201
679
|
const soundRaw = remoteMessage.data?.sound || 'default';
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
console.log('remoteMessage1312', remoteMessage);
|
|
680
|
+
const soundName = soundRaw.replace(/\..+$/, '');
|
|
681
|
+
const channelId = `channel_${soundName}`;
|
|
205
682
|
|
|
206
|
-
await
|
|
207
|
-
id: androidChannelId,
|
|
208
|
-
name: `Channel ${androidSoundName}`,
|
|
209
|
-
importance: AndroidImportance.HIGH,
|
|
210
|
-
sound: androidSoundName !== 'default' ? androidSoundName : undefined,
|
|
211
|
-
});
|
|
683
|
+
await NotifySphere.ensureChannel(channelId, `Channel ${soundName}`, soundRaw);
|
|
212
684
|
|
|
685
|
+
// Resolve image — empty string treated same as missing
|
|
213
686
|
const image =
|
|
214
|
-
remoteMessage
|
|
687
|
+
remoteMessage.data?.imageUrl ||
|
|
215
688
|
remoteMessage.notification?.android?.imageUrl ||
|
|
216
689
|
remoteMessage.notification?.imageUrl ||
|
|
217
|
-
remoteMessage.notification?.ios?.attachments?.[0]?.url
|
|
690
|
+
remoteMessage.notification?.ios?.attachments?.[0]?.url ||
|
|
691
|
+
undefined;
|
|
692
|
+
|
|
693
|
+
// Resolve title/body — empty string treated as missing
|
|
694
|
+
const title =
|
|
695
|
+
remoteMessage.data?.title || remoteMessage.notification?.title || undefined;
|
|
696
|
+
const body =
|
|
697
|
+
remoteMessage.data?.body || remoteMessage.notification?.body || undefined;
|
|
698
|
+
|
|
699
|
+
// smallIcon falls back to 'ic_stat_onesignal_default' if empty/missing
|
|
700
|
+
// largeIcon is removed entirely
|
|
701
|
+
const smallIcon =
|
|
702
|
+
NotifySphere.resolveIcon(remoteMessage.data?.smallIcon) ?? 'ic_stat_onesignal_default';
|
|
703
|
+
|
|
218
704
|
await notifee.displayNotification({
|
|
219
|
-
title
|
|
220
|
-
body
|
|
705
|
+
title,
|
|
706
|
+
body,
|
|
221
707
|
android: {
|
|
222
|
-
channelId
|
|
223
|
-
|
|
224
|
-
sound: androidSoundName !== 'default' ? androidSoundName : undefined,
|
|
225
|
-
largeIcon: image,
|
|
708
|
+
channelId,
|
|
709
|
+
smallIcon,
|
|
226
710
|
style: image
|
|
227
|
-
? {
|
|
228
|
-
type: AndroidStyle.BIGPICTURE,
|
|
229
|
-
picture: image,
|
|
230
|
-
}
|
|
711
|
+
? { type: AndroidStyle.BIGPICTURE, picture: image }
|
|
231
712
|
: undefined,
|
|
232
713
|
},
|
|
233
714
|
ios: {
|
|
234
|
-
sound: soundRaw,
|
|
235
|
-
attachments: image ? [{ url: image }] : undefined,
|
|
715
|
+
sound: soundRaw,
|
|
716
|
+
attachments: image ? [{ url: image }] : undefined,
|
|
236
717
|
foregroundPresentationOptions: {
|
|
237
718
|
alert: true,
|
|
238
719
|
badge: true,
|
|
@@ -243,24 +724,59 @@ class NotifySphere {
|
|
|
243
724
|
});
|
|
244
725
|
}
|
|
245
726
|
|
|
246
|
-
|
|
727
|
+
/**
|
|
728
|
+
* Returns the icon string if it is a real, usable value.
|
|
729
|
+
* Treats empty string (""), "ic_stat_onesignal_default", and
|
|
730
|
+
* any other placeholder-like value as absent (returns undefined).
|
|
731
|
+
*/
|
|
732
|
+
private static resolveIcon(value: string | undefined): string | undefined {
|
|
733
|
+
if (!value) return undefined; // catches "" / null / undefined
|
|
734
|
+
return value;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ─── Private: fire consumer callback ─────────────────────────────────────
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Normalises both Firebase remote messages and Notifee notification objects
|
|
741
|
+
* into the common NotificationData shape and fires the consumer callback.
|
|
742
|
+
*/
|
|
743
|
+
private static sendCallback(
|
|
744
|
+
notification: FirebaseRemoteMessage | NotifeeNotification,
|
|
745
|
+
type?: string
|
|
746
|
+
): void {
|
|
247
747
|
if (!NotifySphere.callback) return;
|
|
248
748
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
remoteMessage.notification?.ios?.attachments?.[0]?.url;
|
|
749
|
+
// Detect shape: Firebase messages have a `notification` or `messageId` field
|
|
750
|
+
const isFirebase =
|
|
751
|
+
'notification' in notification || 'messageId' in notification;
|
|
253
752
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
753
|
+
let title: string | undefined;
|
|
754
|
+
let body: string | undefined;
|
|
755
|
+
let data: Record<string, string> | undefined;
|
|
756
|
+
let image: string | undefined;
|
|
757
|
+
let sound: string | undefined;
|
|
758
|
+
|
|
759
|
+
if (isFirebase) {
|
|
760
|
+
const msg = notification as FirebaseRemoteMessage;
|
|
761
|
+
title = msg.notification?.title ?? msg.data?.title;
|
|
762
|
+
body = msg.notification?.body ?? msg.data?.body;
|
|
763
|
+
data = msg.data;
|
|
764
|
+
image =
|
|
765
|
+
msg.notification?.android?.imageUrl ||
|
|
766
|
+
msg.notification?.imageUrl ||
|
|
767
|
+
msg.notification?.ios?.attachments?.[0]?.url ||
|
|
768
|
+
msg.data?.imageUrl;
|
|
769
|
+
sound = msg.data?.sound;
|
|
770
|
+
} else {
|
|
771
|
+
const notif = notification as NotifeeNotification;
|
|
772
|
+
title = notif.title;
|
|
773
|
+
body = notif.body;
|
|
774
|
+
data = notif.data;
|
|
775
|
+
image = notif.data?.imageUrl;
|
|
776
|
+
sound = notif.data?.sound;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
NotifySphere.callback({ title, body, data, image, sound }, type);
|
|
264
780
|
}
|
|
265
781
|
}
|
|
266
782
|
|