react-native-notify-sphere 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +271 -902
- package/lib/module/index.js +242 -33
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +45 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/index.tsx +287 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-notify-sphere",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Notify sphere npm package",
|
|
5
5
|
"main": "./lib/module/index",
|
|
6
6
|
"types": "lib/typescript/src/index.d.ts",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"react": "19.1.0",
|
|
79
79
|
"react-native": "0.81.1",
|
|
80
80
|
"react-native-builder-bob": "^0.40.13",
|
|
81
|
+
"react-native-device-info": "^15.0.1",
|
|
81
82
|
"release-it": "^19.0.4",
|
|
82
83
|
"turbo": "^2.5.6",
|
|
83
84
|
"typescript": "^5.9.2"
|
|
@@ -91,7 +92,8 @@
|
|
|
91
92
|
"@react-native-firebase/app": "^22.2.1",
|
|
92
93
|
"@react-native-firebase/messaging": "^22.2.1",
|
|
93
94
|
"react": "*",
|
|
94
|
-
"react-native": "*"
|
|
95
|
+
"react-native": "*",
|
|
96
|
+
"react-native-device-info": ">=10.0.0"
|
|
95
97
|
},
|
|
96
98
|
"workspaces": [
|
|
97
99
|
"example"
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import messaging from '@react-native-firebase/messaging';
|
|
2
|
+
import DeviceInfo from 'react-native-device-info';
|
|
2
3
|
import notifee, {
|
|
3
4
|
AndroidImportance,
|
|
5
|
+
AndroidVisibility,
|
|
4
6
|
AndroidStyle,
|
|
5
7
|
EventType,
|
|
6
8
|
type AndroidChannel,
|
|
@@ -9,9 +11,10 @@ import axios from 'axios';
|
|
|
9
11
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
10
12
|
import { PermissionsAndroid, Platform } from 'react-native';
|
|
11
13
|
|
|
12
|
-
const STORAGE_KEY_TOKEN
|
|
13
|
-
const STORAGE_KEY_SUB_ID
|
|
14
|
+
const STORAGE_KEY_TOKEN = '@notifysphere_fcm_token';
|
|
15
|
+
const STORAGE_KEY_SUB_ID = '@notifysphere_subscription_id';
|
|
14
16
|
const STORAGE_KEY_USER_HASH = '@notifysphere_user_hash';
|
|
17
|
+
const STORAGE_KEY_CHANNELS = '@notifysphere_channels';
|
|
15
18
|
|
|
16
19
|
// ─── Type definitions ──────────────────────────────────────────────────────────
|
|
17
20
|
|
|
@@ -52,6 +55,28 @@ export type NotificationCallback = (
|
|
|
52
55
|
type?: string
|
|
53
56
|
) => void;
|
|
54
57
|
|
|
58
|
+
/** Shape of a single notification channel returned by the registration API */
|
|
59
|
+
export interface BackendChannelConfig {
|
|
60
|
+
channel_id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
/** Android importance level sent by backend — mapped to AndroidImportance */
|
|
64
|
+
Importance?: number;
|
|
65
|
+
/** 1 = enable LED, 0 = disable */
|
|
66
|
+
LED?: number;
|
|
67
|
+
led_color?: string | null;
|
|
68
|
+
/** 'Custom' | 'Default' | 'None' */
|
|
69
|
+
sound?: string;
|
|
70
|
+
/** Raw resource name used when sound === 'Custom' (e.g. 'nowait_sound') */
|
|
71
|
+
soundDetail?: string;
|
|
72
|
+
/** 1 = vibration on, 0 = off */
|
|
73
|
+
vibration?: number;
|
|
74
|
+
vibrationDetail?: string | null;
|
|
75
|
+
badge?: number;
|
|
76
|
+
/** 1 = PUBLIC, 0 = PRIVATE, -1 = SECRET */
|
|
77
|
+
lockScreen?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
export type InitializeConfig = {
|
|
56
81
|
/** Your application's user ID for this device */
|
|
57
82
|
applicationUserId: number;
|
|
@@ -113,14 +138,27 @@ const warn = (...args: unknown[]) => {
|
|
|
113
138
|
const logError = (...args: unknown[]) =>
|
|
114
139
|
console.error('[NotifySphere]', ...args);
|
|
115
140
|
|
|
141
|
+
// ─── SDK version ──────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
const SDK_VERSION = '1.2.1';
|
|
144
|
+
|
|
145
|
+
// ─── Device info result shape ─────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
interface DeviceInfoResult {
|
|
148
|
+
sdk: string;
|
|
149
|
+
device_model: string;
|
|
150
|
+
device_os: string;
|
|
151
|
+
device_version: string;
|
|
152
|
+
carrier: string;
|
|
153
|
+
app_version: string;
|
|
154
|
+
timezone_id: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
116
157
|
// ─── Default endpoints ────────────────────────────────────────────────────────
|
|
117
158
|
|
|
118
|
-
const DEFAULT_BASE_URL = 'https://api.notifysphere.in';
|
|
159
|
+
const DEFAULT_BASE_URL = 'https://api.notifysphere.in/client/';
|
|
119
160
|
// const DEFAULT_BASE_URL = 'https://apinotify.dothejob.in';
|
|
120
|
-
const
|
|
121
|
-
DEFAULT_BASE_URL+'/onpress/handle';
|
|
122
|
-
const DEFAULT_DELIVERY_URL =
|
|
123
|
-
DEFAULT_BASE_URL+'/delivery/confirm';;
|
|
161
|
+
//const DEFAULT_BASE_URL = 'https://apinotify.dothejob.in/client/';
|
|
124
162
|
|
|
125
163
|
// ─── NotifySphere class ───────────────────────────────────────────────────────
|
|
126
164
|
|
|
@@ -130,8 +168,8 @@ class NotifySphere {
|
|
|
130
168
|
private static appId: string | null = null;
|
|
131
169
|
private static subscriptionId: string | null = null;
|
|
132
170
|
private static baseUrl: string = DEFAULT_BASE_URL;
|
|
133
|
-
private static trackingUrl: string =
|
|
134
|
-
private static deliveryUrl: string =
|
|
171
|
+
private static trackingUrl: string = '';
|
|
172
|
+
private static deliveryUrl: string = '';
|
|
135
173
|
private static apiKey: string | null = null;
|
|
136
174
|
private static unsubscribers: Array<() => void> = [];
|
|
137
175
|
private static initialized = false;
|
|
@@ -166,7 +204,7 @@ class NotifySphere {
|
|
|
166
204
|
subscription_id: NotifySphere.subscriptionId,
|
|
167
205
|
onPressStatus: 1,
|
|
168
206
|
},
|
|
169
|
-
{ headers, maxBodyLength:
|
|
207
|
+
{ headers, maxBodyLength: 1024 * 50, timeout: 10000 }
|
|
170
208
|
);
|
|
171
209
|
|
|
172
210
|
log('onPress tracking response:', response.data);
|
|
@@ -200,7 +238,7 @@ class NotifySphere {
|
|
|
200
238
|
if (NotifySphere.apiKey) {
|
|
201
239
|
headers['Authorization'] = `Bearer ${NotifySphere.apiKey}`;
|
|
202
240
|
}
|
|
203
|
-
|
|
241
|
+
log('Sending delivery confirmation to:', NotifySphere.deliveryUrl);
|
|
204
242
|
const response = await axios.post(
|
|
205
243
|
NotifySphere.deliveryUrl,
|
|
206
244
|
{
|
|
@@ -208,7 +246,7 @@ class NotifySphere {
|
|
|
208
246
|
subscription_id: NotifySphere.subscriptionId,
|
|
209
247
|
onDeliveredStatus: 1,
|
|
210
248
|
},
|
|
211
|
-
{ headers, maxBodyLength:
|
|
249
|
+
{ headers, maxBodyLength: 1024 * 50, timeout: 10000 }
|
|
212
250
|
);
|
|
213
251
|
|
|
214
252
|
log('Delivery confirmation response:', response.data);
|
|
@@ -240,6 +278,44 @@ class NotifySphere {
|
|
|
240
278
|
}
|
|
241
279
|
}
|
|
242
280
|
|
|
281
|
+
// ─── Device info ──────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Collects device metadata sent to the registration API.
|
|
285
|
+
* Uses react-native-device-info for model, carrier, and app version.
|
|
286
|
+
* Falls back gracefully if any call fails so registration is never blocked.
|
|
287
|
+
*/
|
|
288
|
+
private static async getDeviceInfo(): Promise<DeviceInfoResult> {
|
|
289
|
+
try {
|
|
290
|
+
// getCarrier() is the only async call — run everything in parallel
|
|
291
|
+
const [carrier] = await Promise.all([
|
|
292
|
+
DeviceInfo.getCarrier().catch(() => ''),
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
sdk: SDK_VERSION,
|
|
297
|
+
device_model: DeviceInfo.getModel(),
|
|
298
|
+
device_os: Platform.OS,
|
|
299
|
+
device_version: DeviceInfo.getSystemVersion(),
|
|
300
|
+
carrier: carrier ?? '',
|
|
301
|
+
app_version: DeviceInfo.getVersion(),
|
|
302
|
+
// Intl is available in Hermes/JSC without any extra package
|
|
303
|
+
timezone_id: Intl.DateTimeFormat().resolvedOptions().timeZone ?? '',
|
|
304
|
+
};
|
|
305
|
+
} catch (err) {
|
|
306
|
+
warn('getDeviceInfo failed, using fallback values:', err);
|
|
307
|
+
return {
|
|
308
|
+
sdk: SDK_VERSION,
|
|
309
|
+
device_model: '',
|
|
310
|
+
device_os: Platform.OS,
|
|
311
|
+
device_version: String(Platform.Version),
|
|
312
|
+
carrier: '',
|
|
313
|
+
app_version: '',
|
|
314
|
+
timezone_id: '',
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
243
319
|
// ─── User details fingerprint ─────────────────────────────────────────────
|
|
244
320
|
|
|
245
321
|
/**
|
|
@@ -290,9 +366,13 @@ class NotifySphere {
|
|
|
290
366
|
): Promise<string | undefined> {
|
|
291
367
|
_debug = config.debug ?? false;
|
|
292
368
|
NotifySphere.appId = config.appId;
|
|
293
|
-
NotifySphere.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
294
|
-
NotifySphere.trackingUrl =
|
|
295
|
-
|
|
369
|
+
NotifySphere.baseUrl = NotifySphere.enforceHttps(config.baseUrl ?? DEFAULT_BASE_URL, true);
|
|
370
|
+
NotifySphere.trackingUrl = NotifySphere.enforceHttps(
|
|
371
|
+
config.trackingUrl ?? `${NotifySphere.baseUrl}${config.appId}/onpress/handle`
|
|
372
|
+
);
|
|
373
|
+
NotifySphere.deliveryUrl = NotifySphere.enforceHttps(
|
|
374
|
+
config.deliveryUrl ?? `${NotifySphere.baseUrl}${config.appId}/delivery/confirm`
|
|
375
|
+
);
|
|
296
376
|
NotifySphere.apiKey = config.apiKey ?? null;
|
|
297
377
|
|
|
298
378
|
const hasPermission = await NotifySphere.checkApplicationPermission();
|
|
@@ -315,11 +395,21 @@ class NotifySphere {
|
|
|
315
395
|
// ── Only call the registration API when the FCM token OR any user
|
|
316
396
|
// detail has changed. This avoids a redundant network call on every
|
|
317
397
|
// app open while still syncing updates (email, phone, location, etc.)
|
|
318
|
-
const [cachedToken, cachedUserHash] = await Promise.all([
|
|
398
|
+
const [cachedToken, cachedUserHash, cachedChannels] = await Promise.all([
|
|
319
399
|
AsyncStorage.getItem(STORAGE_KEY_TOKEN),
|
|
320
400
|
AsyncStorage.getItem(STORAGE_KEY_USER_HASH),
|
|
401
|
+
AsyncStorage.getItem(STORAGE_KEY_CHANNELS),
|
|
321
402
|
]);
|
|
322
403
|
|
|
404
|
+
// Re-create all backend-configured channels on every launch so they
|
|
405
|
+
// always exist before a background notification arrives — even after
|
|
406
|
+
// a reinstall or app-data clear. Notifee's createChannel is idempotent.
|
|
407
|
+
if (cachedChannels) {
|
|
408
|
+
const channels = JSON.parse(cachedChannels) as BackendChannelConfig[];
|
|
409
|
+
await NotifySphere.createChannelsFromConfig(channels);
|
|
410
|
+
log('Channels restored from cache:', channels.length);
|
|
411
|
+
}
|
|
412
|
+
|
|
323
413
|
const currentUserHash = NotifySphere.buildUserHash(config);
|
|
324
414
|
let subscriptionId: string | undefined = cachedSubId ?? undefined;
|
|
325
415
|
|
|
@@ -369,8 +459,10 @@ class NotifySphere {
|
|
|
369
459
|
fcmToken: string,
|
|
370
460
|
userHash?: string
|
|
371
461
|
): Promise<string | undefined> {
|
|
462
|
+
|
|
463
|
+
const devInfo = await NotifySphere.getDeviceInfo();
|
|
372
464
|
const res = await axios.post(
|
|
373
|
-
`${NotifySphere.baseUrl}
|
|
465
|
+
`${NotifySphere.baseUrl}${config.appId}/users`,
|
|
374
466
|
{
|
|
375
467
|
applicationUserId: config.applicationUserId,
|
|
376
468
|
token: fcmToken,
|
|
@@ -384,11 +476,24 @@ class NotifySphere {
|
|
|
384
476
|
email: config.email,
|
|
385
477
|
phone: config.phone,
|
|
386
478
|
tags: config.tags,
|
|
479
|
+
sdk: devInfo.sdk,
|
|
480
|
+
device_model: devInfo.device_model,
|
|
481
|
+
device_os: devInfo.device_os,
|
|
482
|
+
device_version: devInfo.device_version,
|
|
483
|
+
carrier: devInfo.carrier,
|
|
484
|
+
app_version: devInfo.app_version,
|
|
485
|
+
timezone_id: devInfo.timezone_id,
|
|
387
486
|
},
|
|
388
487
|
},
|
|
389
488
|
{
|
|
390
|
-
headers: {
|
|
391
|
-
|
|
489
|
+
headers: {
|
|
490
|
+
'Content-Type': 'application/json',
|
|
491
|
+
...(NotifySphere.apiKey
|
|
492
|
+
? { Authorization: `Bearer ${NotifySphere.apiKey}` }
|
|
493
|
+
: {}),
|
|
494
|
+
},
|
|
495
|
+
maxBodyLength: 1024 * 50,
|
|
496
|
+
timeout: 10000,
|
|
392
497
|
}
|
|
393
498
|
);
|
|
394
499
|
|
|
@@ -396,6 +501,17 @@ class NotifySphere {
|
|
|
396
501
|
const subscriptionId: string | undefined = res.data?.subscription_id;
|
|
397
502
|
NotifySphere.subscriptionId = subscriptionId ?? null;
|
|
398
503
|
|
|
504
|
+
// Extract channels from the response and create them immediately.
|
|
505
|
+
// This ensures every backend-configured channel exists on the device
|
|
506
|
+
// before any background / terminated-state notification can arrive.
|
|
507
|
+
const channels: BackendChannelConfig[] | undefined =
|
|
508
|
+
res.data?.notification_channels;
|
|
509
|
+
if (channels && channels.length > 0) {
|
|
510
|
+
await NotifySphere.createChannelsFromConfig(channels);
|
|
511
|
+
await AsyncStorage.setItem(STORAGE_KEY_CHANNELS, JSON.stringify(channels));
|
|
512
|
+
log('Channels created and cached from registration response:', channels.length);
|
|
513
|
+
}
|
|
514
|
+
|
|
399
515
|
// Persist token, subscription_id, and user hash for future launch comparisons
|
|
400
516
|
const hash = userHash ?? NotifySphere.buildUserHash(config);
|
|
401
517
|
await Promise.all([
|
|
@@ -457,7 +573,7 @@ class NotifySphere {
|
|
|
457
573
|
|
|
458
574
|
try {
|
|
459
575
|
const res = await axios.post(
|
|
460
|
-
`${NotifySphere.baseUrl}
|
|
576
|
+
`${NotifySphere.baseUrl}${appId}/users`,
|
|
461
577
|
{
|
|
462
578
|
applicationUserId: params.applicationUserId,
|
|
463
579
|
token: NotifySphere.fcmToken,
|
|
@@ -465,8 +581,14 @@ class NotifySphere {
|
|
|
465
581
|
user: { tags: params.tags },
|
|
466
582
|
},
|
|
467
583
|
{
|
|
468
|
-
headers: {
|
|
469
|
-
|
|
584
|
+
headers: {
|
|
585
|
+
'Content-Type': 'application/json',
|
|
586
|
+
...(NotifySphere.apiKey
|
|
587
|
+
? { Authorization: `Bearer ${NotifySphere.apiKey}` }
|
|
588
|
+
: {}),
|
|
589
|
+
},
|
|
590
|
+
maxBodyLength: 1024 * 50,
|
|
591
|
+
timeout: 10000,
|
|
470
592
|
}
|
|
471
593
|
);
|
|
472
594
|
|
|
@@ -536,7 +658,7 @@ class NotifySphere {
|
|
|
536
658
|
// Apply any config overrides immediately — before the handler fires —
|
|
537
659
|
// so they are in place even when the app starts from a terminated state.
|
|
538
660
|
if (config?.deliveryUrl) {
|
|
539
|
-
NotifySphere.deliveryUrl = config.deliveryUrl;
|
|
661
|
+
NotifySphere.deliveryUrl = NotifySphere.enforceHttps(config.deliveryUrl);
|
|
540
662
|
}
|
|
541
663
|
if (config?.apiKey) {
|
|
542
664
|
NotifySphere.apiKey = config.apiKey;
|
|
@@ -545,9 +667,35 @@ class NotifySphere {
|
|
|
545
667
|
NotifySphere.subscriptionId = config.subscriptionId;
|
|
546
668
|
}
|
|
547
669
|
|
|
670
|
+
// Pre-create the default channel immediately so it is guaranteed to exist
|
|
671
|
+
// (with sound enabled) before any background or terminated-state message
|
|
672
|
+
// arrives. Without this, the channel would be created inside the async
|
|
673
|
+
// background handler — on some devices that races with the notification
|
|
674
|
+
// display and results in a silent notification.
|
|
675
|
+
notifee.createChannel({
|
|
676
|
+
id: 'channel_default',
|
|
677
|
+
name: 'Default Channel',
|
|
678
|
+
importance: AndroidImportance.HIGH,
|
|
679
|
+
lights: true,
|
|
680
|
+
lightColor: '#0000FF',
|
|
681
|
+
vibration: true,
|
|
682
|
+
});
|
|
683
|
+
|
|
548
684
|
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
|
|
549
685
|
const msg = remoteMessage as FirebaseRemoteMessage;
|
|
550
|
-
|
|
686
|
+
|
|
687
|
+
// Always display via Notifee so the backend-configured channel (with its
|
|
688
|
+
// custom sound, importance, and LED settings) is used in every state.
|
|
689
|
+
//
|
|
690
|
+
// On iOS, setBackgroundMessageHandler never fires for messages that
|
|
691
|
+
// contain a `notification` field — APNs intercepts them — so there is
|
|
692
|
+
// no duplicate risk on iOS.
|
|
693
|
+
//
|
|
694
|
+
// On Android, if the FCM payload contains a `notification` field, FCM
|
|
695
|
+
// will also auto-display, which causes a duplicate notification. To avoid
|
|
696
|
+
// this, the backend should send data-only messages to Android tokens
|
|
697
|
+
// (omit the top-level `notification` field). Notifee then has full
|
|
698
|
+
// control over display and sound in all states.
|
|
551
699
|
await Promise.all([
|
|
552
700
|
NotifySphere.displayLocalNotification(msg),
|
|
553
701
|
NotifySphere.callbackOnDelivery(msg),
|
|
@@ -640,6 +788,68 @@ class NotifySphere {
|
|
|
640
788
|
);
|
|
641
789
|
}
|
|
642
790
|
|
|
791
|
+
// ─── Private: backend channel helpers ────────────────────────────────────
|
|
792
|
+
|
|
793
|
+
/** Maps the backend importance value to a Notifee AndroidImportance level. */
|
|
794
|
+
private static mapImportance(value: number | undefined): AndroidImportance {
|
|
795
|
+
const v = value ?? 3;
|
|
796
|
+
if (v >= 4) return AndroidImportance.HIGH;
|
|
797
|
+
if (v === 3) return AndroidImportance.DEFAULT;
|
|
798
|
+
if (v === 2) return AndroidImportance.LOW;
|
|
799
|
+
if (v === 1) return AndroidImportance.MIN;
|
|
800
|
+
return AndroidImportance.DEFAULT;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Maps the backend lockScreen value to a Notifee AndroidVisibility level. */
|
|
804
|
+
private static mapVisibility(lockScreen: number | undefined): AndroidVisibility {
|
|
805
|
+
if (lockScreen === 1) return AndroidVisibility.PUBLIC;
|
|
806
|
+
if (lockScreen === -1) return AndroidVisibility.SECRET;
|
|
807
|
+
return AndroidVisibility.PRIVATE;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Creates Notifee channels from the array returned by the registration API.
|
|
812
|
+
* Called immediately after registration and on every subsequent launch from
|
|
813
|
+
* the AsyncStorage cache — so channels always exist before any notification
|
|
814
|
+
* arrives, including in background / terminated state.
|
|
815
|
+
*/
|
|
816
|
+
private static async createChannelsFromConfig(
|
|
817
|
+
channels: BackendChannelConfig[]
|
|
818
|
+
): Promise<void> {
|
|
819
|
+
for (const ch of channels) {
|
|
820
|
+
// sound === 'Custom' → use soundDetail as the raw resource name
|
|
821
|
+
// sound === 'None' → no sound (don't spread sound key)
|
|
822
|
+
// sound === 'Default' → omit key so Android uses system default
|
|
823
|
+
let soundValue: string | undefined;
|
|
824
|
+
if (ch.sound === 'Custom' && ch.soundDetail) {
|
|
825
|
+
soundValue = ch.soundDetail.replace(/\..+$/, ''); // strip extension
|
|
826
|
+
}
|
|
827
|
+
// 'None' and 'Default' intentionally leave soundValue as undefined
|
|
828
|
+
|
|
829
|
+
console.log('this is sound value',soundValue)
|
|
830
|
+
|
|
831
|
+
const channelDef: AndroidChannel = {
|
|
832
|
+
id: ch.channel_id,
|
|
833
|
+
name: ch.name,
|
|
834
|
+
importance: NotifySphere.mapImportance(ch.Importance),
|
|
835
|
+
lights: (ch.LED ?? 0) === 1,
|
|
836
|
+
vibration: (ch.vibration ?? 0) === 1,
|
|
837
|
+
visibility: NotifySphere.mapVisibility(ch.lockScreen),
|
|
838
|
+
...(ch.description ? { description: ch.description } : {}),
|
|
839
|
+
...(ch.led_color ? { lightColor: ch.led_color } : {}),
|
|
840
|
+
// Only spread sound when it is a real custom resource name.
|
|
841
|
+
// For 'Default', omitting the key tells Android to use the system
|
|
842
|
+
// default sound. For 'None', omitting it silences the channel only
|
|
843
|
+
// if importance is set to LOW/MIN — HIGH importance always plays sound.
|
|
844
|
+
...(soundValue ? { sound: soundValue } : {}),
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
await notifee.createChannel(channelDef);
|
|
848
|
+
NotifySphere.createdChannels.add(ch.channel_id);
|
|
849
|
+
log('Channel created from backend config:', ch.channel_id, `(${ch.name})`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
643
853
|
// ─── Private: channel management ─────────────────────────────────────────
|
|
644
854
|
|
|
645
855
|
/**
|
|
@@ -659,7 +869,11 @@ class NotifySphere {
|
|
|
659
869
|
id,
|
|
660
870
|
name,
|
|
661
871
|
importance: AndroidImportance.HIGH,
|
|
662
|
-
|
|
872
|
+
// For custom sounds, use the raw resource name (without extension).
|
|
873
|
+
// For 'default', omit the field entirely so Android uses the device's
|
|
874
|
+
// default notification sound. Setting it to undefined explicitly can
|
|
875
|
+
// be treated as "no sound" by Notifee on some devices.
|
|
876
|
+
...(soundName !== 'default' ? { sound: soundName } : {}),
|
|
663
877
|
lights: true,
|
|
664
878
|
lightColor: '#0000FF',
|
|
665
879
|
vibration: true,
|
|
@@ -678,7 +892,11 @@ class NotifySphere {
|
|
|
678
892
|
// Use || so empty strings fall back just like null/undefined
|
|
679
893
|
const soundRaw = remoteMessage.data?.sound || 'default';
|
|
680
894
|
const soundName = soundRaw.replace(/\..+$/, '');
|
|
681
|
-
|
|
895
|
+
|
|
896
|
+
// If the backend specifies a channelId (e.g. a UUID created on the dashboard),
|
|
897
|
+
// use it directly — this lets the backend control which channel the notification
|
|
898
|
+
// is assigned to. Otherwise fall back to a channel derived from the sound name.
|
|
899
|
+
const channelId = remoteMessage.data?.channelId || `channel_${soundName}`;
|
|
682
900
|
|
|
683
901
|
await NotifySphere.ensureChannel(channelId, `Channel ${soundName}`, soundRaw);
|
|
684
902
|
|
|
@@ -696,20 +914,34 @@ class NotifySphere {
|
|
|
696
914
|
const body =
|
|
697
915
|
remoteMessage.data?.body || remoteMessage.notification?.body || undefined;
|
|
698
916
|
|
|
699
|
-
// smallIcon
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
917
|
+
// Resolve smallIcon and largeIcon — both fall back to
|
|
918
|
+
// 'ic_stat_notifysphere_default' if not provided in the payload.
|
|
919
|
+
// Add a drawable with this name to your app's res/drawable folder.
|
|
920
|
+
const smallIconResolved =
|
|
921
|
+
NotifySphere.resolveIcon(remoteMessage.data?.smallIcon) ??
|
|
922
|
+
'ic_stat_notifysphere_default';
|
|
923
|
+
const largeIconResolved =
|
|
924
|
+
NotifySphere.resolveIcon(remoteMessage.data?.largeIcon) ??
|
|
925
|
+
'ic_stat_notifysphere_default';
|
|
926
|
+
|
|
927
|
+
// Determine Android expand style:
|
|
928
|
+
// - Image present → BIGPICTURE (shows image when expanded)
|
|
929
|
+
// - Long body (>44 chars) → BIGTEXT (shows full text when expanded)
|
|
930
|
+
// - Otherwise → no style (single-line collapsed notification)
|
|
931
|
+
const androidStyle = image
|
|
932
|
+
? ({ type: AndroidStyle.BIGPICTURE, picture: image } as const)
|
|
933
|
+
: body && body.length > 44
|
|
934
|
+
? ({ type: AndroidStyle.BIGTEXT, text: body } as const)
|
|
935
|
+
: undefined;
|
|
703
936
|
|
|
704
937
|
await notifee.displayNotification({
|
|
705
938
|
title,
|
|
706
939
|
body,
|
|
707
940
|
android: {
|
|
708
941
|
channelId,
|
|
709
|
-
smallIcon,
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
: undefined,
|
|
942
|
+
smallIcon: smallIconResolved,
|
|
943
|
+
largeIcon: largeIconResolved,
|
|
944
|
+
style: androidStyle,
|
|
713
945
|
},
|
|
714
946
|
ios: {
|
|
715
947
|
sound: soundRaw,
|
|
@@ -734,6 +966,27 @@ class NotifySphere {
|
|
|
734
966
|
return value;
|
|
735
967
|
}
|
|
736
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Rejects plain HTTP URLs to prevent user data (FCM token, email, phone,
|
|
971
|
+
* location) being sent over an unencrypted connection.
|
|
972
|
+
* Also ensures the URL ends with a trailing slash so appId is always
|
|
973
|
+
* correctly appended (e.g. baseUrl + appId + '/users').
|
|
974
|
+
*/
|
|
975
|
+
private static enforceHttps(url: string, ensureTrailingSlash = false): string {
|
|
976
|
+
if (url.startsWith('http://')) {
|
|
977
|
+
logError(
|
|
978
|
+
`Insecure URL rejected: "${url}". NotifySphere only allows HTTPS endpoints.`
|
|
979
|
+
);
|
|
980
|
+
throw new Error(
|
|
981
|
+
`NotifySphere: insecure HTTP URL is not allowed — use HTTPS instead.`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
if (ensureTrailingSlash && !url.endsWith('/')) {
|
|
985
|
+
return url + '/';
|
|
986
|
+
}
|
|
987
|
+
return url;
|
|
988
|
+
}
|
|
989
|
+
|
|
737
990
|
// ─── Private: fire consumer callback ─────────────────────────────────────
|
|
738
991
|
|
|
739
992
|
/**
|