@msgera/push 0.0.0-stage → 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 +71 -2
- package/package.json +25 -4
- package/src/index.d.ts +100 -0
- package/src/index.js +596 -0
package/README.md
CHANGED
|
@@ -1,3 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Msgera Push SDK for React Native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Push notifications for React Native apps on Android and iOS using `@react-native-firebase/messaging` and Msgera.
|
|
4
|
+
|
|
5
|
+
## 1. Install
|
|
6
|
+
|
|
7
|
+
Install `@msgera/push` alongside `@react-native-firebase/app` and `@react-native-firebase/messaging`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @msgera/push @react-native-firebase/app @react-native-firebase/messaging
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Optional for foreground banners on Android:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @notifee/react-native
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 2. Initialize
|
|
20
|
+
|
|
21
|
+
Register the background handler in `index.js` before `AppRegistry.registerComponent`, and call `init` when your app mounts:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
// index.js
|
|
25
|
+
import { registerBackgroundHandler } from '@msgera/push';
|
|
26
|
+
registerBackgroundHandler();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
// App.tsx
|
|
31
|
+
import { useEffect } from 'react';
|
|
32
|
+
import { init, setLogLevel } from '@msgera/push';
|
|
33
|
+
|
|
34
|
+
export default function App() {
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setLogLevel('verbose'); // optional for debugging
|
|
37
|
+
init('YOUR_APP_PUBLIC_ID');
|
|
38
|
+
}, []);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 3. Request Permission
|
|
43
|
+
|
|
44
|
+
Call `requestPermission` from a user action:
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import { requestPermission } from '@msgera/push';
|
|
48
|
+
|
|
49
|
+
const granted = await requestPermission({ fallbackToSettings: true });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 4. Login & Logout
|
|
53
|
+
|
|
54
|
+
Link the device to your user ID (pass `userHash` when identity verification is enabled):
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
import { login, logout } from '@msgera/push';
|
|
58
|
+
|
|
59
|
+
await login('user_123', userHash);
|
|
60
|
+
|
|
61
|
+
// On sign-out:
|
|
62
|
+
await logout();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 5. Tags, consent, and notification events
|
|
66
|
+
|
|
67
|
+
`addTag`, `addTags`, `removeTag`, `removeTags`, and `getTags` debounce for 2 seconds. Also: `setLanguage`, `trackEvent`, `optOut`, `optIn`, `pushSubscription`, `addClickListener`, `addForegroundListener`, `addPermissionObserver`, `addSubscriptionObserver`, `canRequestPermission`, `setConsentRequired`, `setConsentGiven`, `deleteDevice`, `clearAllNotifications`, and `removeNotification`. Clicks open with `Linking.openURL` unless `preventDefault()` is called. Call `setConsentRequired(true)` before `init`. `groupKey` on the payload sets the Android group and iOS thread when Notifee is installed.
|
|
68
|
+
|
|
69
|
+
## 6. Test
|
|
70
|
+
|
|
71
|
+
1. Launch your app on a device and grant notification permission.
|
|
72
|
+
2. In the Msgera Dashboard, open **Subscribers**, mark your device as a **Test device**, and use **Send test** in the composer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msgera/push",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Msgera push notifications SDK for React Native",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/msgera/push-sdks.git",
|
|
14
|
+
"directory": "react-native"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@react-native-firebase/app": ">=21.0.0",
|
|
19
|
+
"@react-native-firebase/messaging": ">=21.0.0",
|
|
20
|
+
"react-native": ">=0.76.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependenciesMeta": {
|
|
23
|
+
"@notifee/react-native": {
|
|
24
|
+
"optional": true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export type Done = (error?: Error) => void;
|
|
2
|
+
export type LogLevel = 'none' | 'error' | 'verbose';
|
|
3
|
+
|
|
4
|
+
export interface RequestPermissionOptions {
|
|
5
|
+
fallbackToSettings?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface InitOptions {
|
|
9
|
+
appId: string;
|
|
10
|
+
apiBase?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SDK_VERSION: string;
|
|
14
|
+
export const DEFAULT_API_BASE: string;
|
|
15
|
+
|
|
16
|
+
export function init(appId: string, apiBase?: string): Promise<void>;
|
|
17
|
+
export function init(options: InitOptions): Promise<void>;
|
|
18
|
+
export function requestPermission(fallbackToSettings?: boolean | RequestPermissionOptions): Promise<boolean>;
|
|
19
|
+
export function login(externalUserId: string, userHash?: string): Promise<void>;
|
|
20
|
+
export function logout(): Promise<void>;
|
|
21
|
+
export function setLogLevel(level: LogLevel): void;
|
|
22
|
+
|
|
23
|
+
export interface PushSubscriptionState {
|
|
24
|
+
id: string | null;
|
|
25
|
+
token: string | null;
|
|
26
|
+
optedIn: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ClickEvent {
|
|
30
|
+
messageId: string | null;
|
|
31
|
+
title: string | null;
|
|
32
|
+
body: string | null;
|
|
33
|
+
data: Record<string, string>;
|
|
34
|
+
launchUrl: string | null;
|
|
35
|
+
actionId: string | null;
|
|
36
|
+
preventDefault(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ForegroundEvent {
|
|
40
|
+
messageId: string | null;
|
|
41
|
+
title: string | null;
|
|
42
|
+
body: string | null;
|
|
43
|
+
data: Record<string, string>;
|
|
44
|
+
preventDefault(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function pushSubscription(): PushSubscriptionState;
|
|
48
|
+
export function addClickListener(listener: (event: ClickEvent) => void): void;
|
|
49
|
+
export function addForegroundListener(listener: (event: ForegroundEvent) => void): void;
|
|
50
|
+
export function addPermissionObserver(listener: (granted: boolean) => void): void;
|
|
51
|
+
export function addSubscriptionObserver(listener: (subscription: PushSubscriptionState) => void): void;
|
|
52
|
+
export function canRequestPermission(): Promise<boolean>;
|
|
53
|
+
export function optOut(): Promise<void>;
|
|
54
|
+
export function optIn(): Promise<void>;
|
|
55
|
+
export function addTag(key: string, value: string): void;
|
|
56
|
+
export function addTags(tags: Record<string, string>): void;
|
|
57
|
+
export function removeTag(key: string): void;
|
|
58
|
+
export function removeTags(keys: string[]): void;
|
|
59
|
+
export function getTags(): Record<string, string>;
|
|
60
|
+
export function setLanguage(language: string): Promise<void>;
|
|
61
|
+
export function trackEvent(name: string, properties?: Record<string, unknown>): Promise<void>;
|
|
62
|
+
export function setConsentRequired(required: boolean): void;
|
|
63
|
+
export function setConsentGiven(given: boolean): void;
|
|
64
|
+
export function deleteDevice(): Promise<void>;
|
|
65
|
+
export function clearAllNotifications(): Promise<void>;
|
|
66
|
+
export function removeNotification(id: number): Promise<void>;
|
|
67
|
+
|
|
68
|
+
/** Call from index.js, before AppRegistry. A handler registered later misses a tap that launched a killed app. */
|
|
69
|
+
export function registerBackgroundHandler(): void;
|
|
70
|
+
|
|
71
|
+
export const MsgeraPush: {
|
|
72
|
+
SDK_VERSION: string;
|
|
73
|
+
DEFAULT_API_BASE: string;
|
|
74
|
+
init: typeof init;
|
|
75
|
+
requestPermission: typeof requestPermission;
|
|
76
|
+
login: typeof login;
|
|
77
|
+
logout: typeof logout;
|
|
78
|
+
setLogLevel: typeof setLogLevel;
|
|
79
|
+
registerBackgroundHandler: typeof registerBackgroundHandler;
|
|
80
|
+
pushSubscription: typeof pushSubscription;
|
|
81
|
+
addClickListener: typeof addClickListener;
|
|
82
|
+
addForegroundListener: typeof addForegroundListener;
|
|
83
|
+
addPermissionObserver: typeof addPermissionObserver;
|
|
84
|
+
addSubscriptionObserver: typeof addSubscriptionObserver;
|
|
85
|
+
canRequestPermission: typeof canRequestPermission;
|
|
86
|
+
optOut: typeof optOut;
|
|
87
|
+
optIn: typeof optIn;
|
|
88
|
+
addTag: typeof addTag;
|
|
89
|
+
addTags: typeof addTags;
|
|
90
|
+
removeTag: typeof removeTag;
|
|
91
|
+
removeTags: typeof removeTags;
|
|
92
|
+
getTags: typeof getTags;
|
|
93
|
+
setLanguage: typeof setLanguage;
|
|
94
|
+
trackEvent: typeof trackEvent;
|
|
95
|
+
setConsentRequired: typeof setConsentRequired;
|
|
96
|
+
setConsentGiven: typeof setConsentGiven;
|
|
97
|
+
deleteDevice: typeof deleteDevice;
|
|
98
|
+
clearAllNotifications: typeof clearAllNotifications;
|
|
99
|
+
removeNotification: typeof removeNotification;
|
|
100
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
const { Platform, PermissionsAndroid, Linking } = require('react-native');
|
|
2
|
+
const {
|
|
3
|
+
getMessaging,
|
|
4
|
+
getToken,
|
|
5
|
+
onTokenRefresh,
|
|
6
|
+
onMessage,
|
|
7
|
+
onNotificationOpenedApp,
|
|
8
|
+
getInitialNotification,
|
|
9
|
+
setBackgroundMessageHandler,
|
|
10
|
+
requestPermission: requestMessagingPermission,
|
|
11
|
+
AuthorizationStatus,
|
|
12
|
+
getAPNSToken,
|
|
13
|
+
setAutoInitEnabled,
|
|
14
|
+
} = require('@react-native-firebase/messaging');
|
|
15
|
+
|
|
16
|
+
const SDK_VERSION = '1.1.0';
|
|
17
|
+
const DEFAULT_API_BASE = 'https://api.msgera.io';
|
|
18
|
+
const seenClicks = new Set();
|
|
19
|
+
let config = null;
|
|
20
|
+
let logLevel = 'error';
|
|
21
|
+
const clickListeners = [];
|
|
22
|
+
const foregroundListeners = [];
|
|
23
|
+
const permissionObservers = [];
|
|
24
|
+
const subscriptionObservers = [];
|
|
25
|
+
const queuedClicks = [];
|
|
26
|
+
let pendingTags = {};
|
|
27
|
+
let tagTimer = null;
|
|
28
|
+
let consentRequired = false;
|
|
29
|
+
let consentGiven = false;
|
|
30
|
+
let optedIn = true;
|
|
31
|
+
|
|
32
|
+
function networkAllowed() {
|
|
33
|
+
return !consentRequired || consentGiven;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function notifySubscription() {
|
|
37
|
+
const state = pushSubscription();
|
|
38
|
+
subscriptionObservers.forEach((listener) => listener(state));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function notifyPermission(granted) {
|
|
42
|
+
permissionObservers.forEach((listener) => listener(granted));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function javaHash(value) {
|
|
46
|
+
let hash = 0;
|
|
47
|
+
const text = value || '';
|
|
48
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
49
|
+
hash = (Math.imul(hash, 31) + text.charCodeAt(index)) | 0;
|
|
50
|
+
}
|
|
51
|
+
return hash;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function logVerbose(message) {
|
|
55
|
+
if (logLevel === 'verbose') {
|
|
56
|
+
console.log(`[MsgeraPush] ${message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function logError(message, error) {
|
|
61
|
+
if (logLevel === 'error' || logLevel === 'verbose') {
|
|
62
|
+
console.error(`[MsgeraPush] ${message}`, error || '');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function setLogLevel(level) {
|
|
67
|
+
const normalized = String(level || '').toLowerCase();
|
|
68
|
+
if (normalized === 'none' || normalized === 'error' || normalized === 'verbose') {
|
|
69
|
+
logLevel = normalized;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function base() {
|
|
74
|
+
if (!config) throw new Error('Call init before using Msgera push');
|
|
75
|
+
const raw = (config.apiBase || DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
76
|
+
return raw || DEFAULT_API_BASE;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function post(path, body) {
|
|
80
|
+
return request('POST', path, body);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function request(method, path, body) {
|
|
84
|
+
if (!networkAllowed()) throw new Error('Consent is required before Msgera push can send data');
|
|
85
|
+
const response = await fetch(`${base()}/v1/public/push/${config.appId}${path}`, {
|
|
86
|
+
method,
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: method === 'GET' || method === 'DELETE' ? undefined : JSON.stringify(body || {}),
|
|
89
|
+
});
|
|
90
|
+
const text = await response.text();
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const errText = text || `Push request failed (${response.status})`;
|
|
93
|
+
logError(`Request ${path} failed: ${errText}`);
|
|
94
|
+
throw new Error(errText);
|
|
95
|
+
}
|
|
96
|
+
return text ? JSON.parse(text) : {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function register(token) {
|
|
100
|
+
if (!networkAllowed() || !optedIn) return;
|
|
101
|
+
if (config.token === token && config.subscriberId) return;
|
|
102
|
+
const json = await post('/subscribers', {
|
|
103
|
+
token,
|
|
104
|
+
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
105
|
+
transport: 'fcm',
|
|
106
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
107
|
+
sdkVersion: SDK_VERSION,
|
|
108
|
+
});
|
|
109
|
+
config.subscriberId = json.subscriberId;
|
|
110
|
+
config.token = token;
|
|
111
|
+
logVerbose(`Registered subscriber ${json.subscriberId}`);
|
|
112
|
+
notifySubscription();
|
|
113
|
+
if (Object.keys(pendingTags).length) scheduleTagFlush();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function currentToken() {
|
|
117
|
+
if (Platform.OS === 'ios') {
|
|
118
|
+
let apns = null;
|
|
119
|
+
for (let attempt = 0; attempt < 10 && !apns; attempt += 1) {
|
|
120
|
+
apns = await getAPNSToken(getMessaging());
|
|
121
|
+
if (!apns) await new Promise((resolve) => setTimeout(resolve, 300));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const token = await getToken(getMessaging());
|
|
125
|
+
if (!token) throw new Error('Firebase did not return a token. Add the Firebase config files.');
|
|
126
|
+
await register(token);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function report(messageId, type) {
|
|
130
|
+
if (!messageId || !config?.subscriberId) return;
|
|
131
|
+
await post(`/subscribers/${config.subscriberId}/events`, { messageId, type }).catch((err) => {
|
|
132
|
+
logError(`Event report (${type}) failed`, err);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pushSubscription() {
|
|
137
|
+
return {
|
|
138
|
+
id: config?.subscriberId || null,
|
|
139
|
+
token: config?.token || null,
|
|
140
|
+
optedIn,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function dispatchClick(event) {
|
|
145
|
+
clickListeners.forEach((listener) => listener(event));
|
|
146
|
+
if (!event.prevented && event.launchUrl) {
|
|
147
|
+
Linking.openURL(event.launchUrl).catch(() => undefined);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function openClick(message, actionId) {
|
|
152
|
+
const data = message?.data || {};
|
|
153
|
+
const messageId = data.msgeraMessageId;
|
|
154
|
+
const resolvedAction = actionId && actionId !== 'default' ? actionId : data.actionId || null;
|
|
155
|
+
const seenKey = `${messageId || ''}:${resolvedAction || ''}`;
|
|
156
|
+
if (!messageId || seenClicks.has(seenKey)) return;
|
|
157
|
+
seenClicks.add(seenKey);
|
|
158
|
+
await report(messageId, 'clicked');
|
|
159
|
+
let launchUrl = data.clickUrl || null;
|
|
160
|
+
if (resolvedAction) {
|
|
161
|
+
const match = parseButtons(data.msgeraButtons, 4).find((button) => button.id === resolvedAction);
|
|
162
|
+
if (match && match.url) launchUrl = match.url;
|
|
163
|
+
}
|
|
164
|
+
const event = {
|
|
165
|
+
messageId,
|
|
166
|
+
title: message.notification?.title || data.title || null,
|
|
167
|
+
body: message.notification?.body || data.body || null,
|
|
168
|
+
data,
|
|
169
|
+
launchUrl,
|
|
170
|
+
actionId: resolvedAction,
|
|
171
|
+
prevented: false,
|
|
172
|
+
preventDefault() {
|
|
173
|
+
this.prevented = true;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
if (!clickListeners.length) {
|
|
177
|
+
queuedClicks.push(event);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
dispatchClick(event);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseButtons(raw, limit = 2) {
|
|
184
|
+
if (!raw) return [];
|
|
185
|
+
try {
|
|
186
|
+
const rows = JSON.parse(raw);
|
|
187
|
+
if (!Array.isArray(rows)) return [];
|
|
188
|
+
return rows.slice(0, limit).flatMap((row) => {
|
|
189
|
+
if (!row || typeof row.id !== 'string') return [];
|
|
190
|
+
return [{ id: row.id, text: row.text || row.title || row.id, url: row.url || '' }];
|
|
191
|
+
});
|
|
192
|
+
} catch {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function syncChannels() {
|
|
198
|
+
let notifee;
|
|
199
|
+
try {
|
|
200
|
+
notifee = require('@notifee/react-native').default;
|
|
201
|
+
} catch {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const config = await request('GET', '/config');
|
|
205
|
+
const rows = Array.isArray(config.channels) ? config.channels : [];
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
if (!row || !row.channelId || !row.name) continue;
|
|
208
|
+
if (row.key && row.version > 1) {
|
|
209
|
+
for (let older = 1; older < row.version; older += 1) {
|
|
210
|
+
await notifee.deleteChannel(`${row.key}_v${older}`).catch(() => undefined);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const importance = row.importance === 'low' ? 2 : row.importance === 'default' ? 3 : 4;
|
|
214
|
+
await notifee.createChannel({
|
|
215
|
+
id: row.channelId,
|
|
216
|
+
name: row.name,
|
|
217
|
+
description: row.description || undefined,
|
|
218
|
+
importance,
|
|
219
|
+
sound: row.sound && row.sound !== 'default' && row.sound !== 'none' ? row.sound : undefined,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function showForeground(message) {
|
|
225
|
+
const data = message.data || {};
|
|
226
|
+
const event = {
|
|
227
|
+
messageId: data.msgeraMessageId || null,
|
|
228
|
+
title: message.notification?.title || data.title || null,
|
|
229
|
+
body: message.notification?.body || data.body || null,
|
|
230
|
+
data,
|
|
231
|
+
prevented: false,
|
|
232
|
+
preventDefault() {
|
|
233
|
+
this.prevented = true;
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
foregroundListeners.forEach((listener) => listener(event));
|
|
237
|
+
if (event.prevented) return;
|
|
238
|
+
let notifee;
|
|
239
|
+
try {
|
|
240
|
+
notifee = require('@notifee/react-native').default;
|
|
241
|
+
} catch {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const channelId = data.channelId || (await notifee.createChannel({
|
|
245
|
+
id: 'msgera_default',
|
|
246
|
+
name: 'Notifications',
|
|
247
|
+
importance: 4,
|
|
248
|
+
}));
|
|
249
|
+
const buttons = parseButtons(data.msgeraButtons, Platform.OS === 'ios' ? 4 : 2);
|
|
250
|
+
let iosCategoryId;
|
|
251
|
+
if (Platform.OS === 'ios' && buttons.length && typeof notifee.setNotificationCategories === 'function') {
|
|
252
|
+
iosCategoryId = `msgera.${buttons.map((button) => button.id).join('.')}`;
|
|
253
|
+
const existing = typeof notifee.getNotificationCategories === 'function'
|
|
254
|
+
? await notifee.getNotificationCategories().catch(() => [])
|
|
255
|
+
: [];
|
|
256
|
+
const next = (existing || []).filter((row) => row && row.id !== iosCategoryId);
|
|
257
|
+
next.push({
|
|
258
|
+
id: iosCategoryId,
|
|
259
|
+
actions: buttons.map((button) => ({ id: button.id, title: button.text })),
|
|
260
|
+
});
|
|
261
|
+
await notifee.setNotificationCategories(next);
|
|
262
|
+
}
|
|
263
|
+
await notifee.displayNotification({
|
|
264
|
+
id: String(javaHash(data.msgeraMessageId || event.title || 'msgera')),
|
|
265
|
+
title: event.title,
|
|
266
|
+
body: event.body,
|
|
267
|
+
data: message.data,
|
|
268
|
+
android: {
|
|
269
|
+
channelId,
|
|
270
|
+
smallIcon: data.smallIcon || 'ic_stat_msgera',
|
|
271
|
+
pressAction: { id: 'default' },
|
|
272
|
+
...(data.groupKey ? { groupId: data.groupKey } : {}),
|
|
273
|
+
...(data.largeIconUrl ? { largeIcon: data.largeIconUrl } : {}),
|
|
274
|
+
...(data.accentColor ? { color: data.accentColor } : {}),
|
|
275
|
+
...(data.imageUrl ? { style: { type: 0, picture: data.imageUrl } } : {}),
|
|
276
|
+
...(buttons.length
|
|
277
|
+
? {
|
|
278
|
+
actions: buttons.slice(0, 2).map((button) => ({
|
|
279
|
+
title: button.text,
|
|
280
|
+
pressAction: { id: button.id, launchActivity: 'default' },
|
|
281
|
+
})),
|
|
282
|
+
}
|
|
283
|
+
: {}),
|
|
284
|
+
},
|
|
285
|
+
ios: {
|
|
286
|
+
...(data.groupKey ? { threadId: data.groupKey } : {}),
|
|
287
|
+
...(iosCategoryId ? { categoryId: iosCategoryId } : {}),
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function notifeeClick(type, detail) {
|
|
293
|
+
if (type !== 1 && type !== 2) return;
|
|
294
|
+
const notification = detail?.notification || {};
|
|
295
|
+
const actionId = type === 2 ? detail?.pressAction?.id : null;
|
|
296
|
+
openClick({ data: notification.data || {}, notification }, actionId).catch(() => undefined);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function attachNotifeeClicks() {
|
|
300
|
+
let notifee;
|
|
301
|
+
try {
|
|
302
|
+
notifee = require('@notifee/react-native').default;
|
|
303
|
+
} catch {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (attachNotifeeClicks.done) return;
|
|
307
|
+
attachNotifeeClicks.done = true;
|
|
308
|
+
notifee.onForegroundEvent(({ type, detail }) => notifeeClick(type, detail));
|
|
309
|
+
notifee.getInitialNotification().then((initial) => {
|
|
310
|
+
if (!initial?.notification) return;
|
|
311
|
+
const actionId = initial.pressAction?.id;
|
|
312
|
+
openClick(
|
|
313
|
+
{ data: initial.notification.data || {}, notification: initial.notification },
|
|
314
|
+
actionId,
|
|
315
|
+
).catch(() => undefined);
|
|
316
|
+
}).catch(() => undefined);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function registerBackgroundHandler() {
|
|
320
|
+
setBackgroundMessageHandler(getMessaging(), async (message) => {
|
|
321
|
+
await report(message?.data?.msgeraMessageId, 'received');
|
|
322
|
+
});
|
|
323
|
+
try {
|
|
324
|
+
const notifee = require('@notifee/react-native').default;
|
|
325
|
+
notifee.onBackgroundEvent(async ({ type, detail }) => {
|
|
326
|
+
notifeeClick(type, detail);
|
|
327
|
+
});
|
|
328
|
+
} catch {
|
|
329
|
+
/* notifee is optional */
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function init(appIdOrOptions, apiBase = DEFAULT_API_BASE) {
|
|
334
|
+
let resolvedAppId = appIdOrOptions;
|
|
335
|
+
let resolvedApiBase = apiBase || DEFAULT_API_BASE;
|
|
336
|
+
if (appIdOrOptions && typeof appIdOrOptions === 'object') {
|
|
337
|
+
resolvedAppId = appIdOrOptions.appId;
|
|
338
|
+
resolvedApiBase = appIdOrOptions.apiBase || DEFAULT_API_BASE;
|
|
339
|
+
}
|
|
340
|
+
config = {
|
|
341
|
+
appId: resolvedAppId,
|
|
342
|
+
apiBase: resolvedApiBase,
|
|
343
|
+
subscriberId: config?.subscriberId || null,
|
|
344
|
+
token: config?.token || null,
|
|
345
|
+
tags: config?.tags || {},
|
|
346
|
+
userHash: config?.userHash,
|
|
347
|
+
};
|
|
348
|
+
logVerbose(`Initialized MsgeraPush v${SDK_VERSION} for app ${resolvedAppId}`);
|
|
349
|
+
if (networkAllowed()) syncChannels().catch((err) => logError('Channel sync failed', err));
|
|
350
|
+
// Fetching a token asks for permission on its own. Init must not do that.
|
|
351
|
+
await setAutoInitEnabled(getMessaging(), true);
|
|
352
|
+
onTokenRefresh(getMessaging(), (token) => {
|
|
353
|
+
register(token).catch((err) => logError('Token refresh failed', err));
|
|
354
|
+
});
|
|
355
|
+
onMessage(getMessaging(), async (message) => {
|
|
356
|
+
await showForeground(message);
|
|
357
|
+
await report(message?.data?.msgeraMessageId, 'received');
|
|
358
|
+
});
|
|
359
|
+
onNotificationOpenedApp(getMessaging(), (message) => {
|
|
360
|
+
openClick(message).catch(() => undefined);
|
|
361
|
+
});
|
|
362
|
+
attachNotifeeClicks();
|
|
363
|
+
const initial = await getInitialNotification(getMessaging());
|
|
364
|
+
if (initial) await openClick(initial);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function requestPermission(options) {
|
|
368
|
+
const fallbackToSettings =
|
|
369
|
+
typeof options === 'boolean' ? options : Boolean(options && options.fallbackToSettings);
|
|
370
|
+
|
|
371
|
+
if (Platform.OS === 'android' && Platform.Version >= 33) {
|
|
372
|
+
const result = await PermissionsAndroid.request(
|
|
373
|
+
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
|
|
374
|
+
);
|
|
375
|
+
if (result !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
376
|
+
notifyPermission(false);
|
|
377
|
+
if (fallbackToSettings && result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN && Linking.openSettings) {
|
|
378
|
+
await Linking.openSettings().catch(() => undefined);
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const status = await requestMessagingPermission(getMessaging());
|
|
384
|
+
const allowed =
|
|
385
|
+
status === AuthorizationStatus.AUTHORIZED ||
|
|
386
|
+
status === AuthorizationStatus.PROVISIONAL;
|
|
387
|
+
notifyPermission(Platform.OS === 'android' ? true : allowed);
|
|
388
|
+
if (!allowed && Platform.OS === 'ios') {
|
|
389
|
+
if (fallbackToSettings && Linking.openSettings) {
|
|
390
|
+
await Linking.openSettings().catch(() => undefined);
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
await currentToken();
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function login(externalUserId, userHash) {
|
|
399
|
+
if (!config?.subscriberId) throw new Error('Call requestPermission before login');
|
|
400
|
+
await post(`/subscribers/${config.subscriberId}/login`, {
|
|
401
|
+
externalUserId,
|
|
402
|
+
...(userHash ? { userHash } : {}),
|
|
403
|
+
});
|
|
404
|
+
if (userHash) config.userHash = userHash;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function queueTag(key, value) {
|
|
408
|
+
config.tags = config.tags || {};
|
|
409
|
+
if (value == null) delete config.tags[key];
|
|
410
|
+
else config.tags[key] = value;
|
|
411
|
+
pendingTags[key] = value;
|
|
412
|
+
scheduleTagFlush();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function scheduleTagFlush() {
|
|
416
|
+
if (tagTimer) clearTimeout(tagTimer);
|
|
417
|
+
tagTimer = setTimeout(() => {
|
|
418
|
+
tagTimer = null;
|
|
419
|
+
flushTags().catch((error) => logError('Tag update failed', error));
|
|
420
|
+
}, 2000);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function flushTags() {
|
|
424
|
+
if (!networkAllowed() || !config?.subscriberId || !Object.keys(pendingTags).length) return;
|
|
425
|
+
const tags = pendingTags;
|
|
426
|
+
pendingTags = {};
|
|
427
|
+
await request('PATCH', `/subscribers/${config.subscriberId}/tags`, {
|
|
428
|
+
tags,
|
|
429
|
+
...(config.userHash ? { userHash: config.userHash } : {}),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function addClickListener(listener) {
|
|
434
|
+
clickListeners.push(listener);
|
|
435
|
+
const pending = queuedClicks.splice(0, queuedClicks.length);
|
|
436
|
+
pending.forEach((event) => dispatchClick(event));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function addForegroundListener(listener) {
|
|
440
|
+
foregroundListeners.push(listener);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function addPermissionObserver(listener) {
|
|
444
|
+
permissionObservers.push(listener);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function addSubscriptionObserver(listener) {
|
|
448
|
+
subscriptionObservers.push(listener);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function canRequestPermission() {
|
|
452
|
+
try {
|
|
453
|
+
const messaging = require('@react-native-firebase/messaging');
|
|
454
|
+
if (typeof messaging.hasPermission === 'function') {
|
|
455
|
+
const status = await messaging.hasPermission(getMessaging());
|
|
456
|
+
return status !== AuthorizationStatus.DENIED;
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
/* older messaging builds only expose the request call */
|
|
460
|
+
}
|
|
461
|
+
if (Platform.OS === 'android' && Platform.Version >= 33) {
|
|
462
|
+
return PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);
|
|
463
|
+
}
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function optOut() {
|
|
468
|
+
optedIn = false;
|
|
469
|
+
notifySubscription();
|
|
470
|
+
if (config?.subscriberId) {
|
|
471
|
+
await request('PATCH', `/subscribers/${config.subscriberId}/subscription`, { optedIn: false });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function optIn() {
|
|
476
|
+
optedIn = true;
|
|
477
|
+
notifySubscription();
|
|
478
|
+
if (config?.subscriberId) {
|
|
479
|
+
await request('PATCH', `/subscribers/${config.subscriberId}/subscription`, { optedIn: true });
|
|
480
|
+
}
|
|
481
|
+
await currentToken();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function addTag(key, value) {
|
|
485
|
+
queueTag(key, value);
|
|
486
|
+
}
|
|
487
|
+
function addTags(tags) {
|
|
488
|
+
Object.keys(tags || {}).forEach((key) => queueTag(key, tags[key]));
|
|
489
|
+
}
|
|
490
|
+
function removeTag(key) {
|
|
491
|
+
queueTag(key, null);
|
|
492
|
+
}
|
|
493
|
+
function removeTags(keys) {
|
|
494
|
+
(keys || []).forEach((key) => queueTag(key, null));
|
|
495
|
+
}
|
|
496
|
+
function getTags() {
|
|
497
|
+
return { ...(config?.tags || {}) };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function setLanguage(language) {
|
|
501
|
+
config.language = language;
|
|
502
|
+
if (!config?.subscriberId) return;
|
|
503
|
+
await request('PATCH', `/subscribers/${config.subscriberId}`, { language });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function trackEvent(name, properties) {
|
|
507
|
+
if (!config?.subscriberId) throw new Error('Call requestPermission before trackEvent');
|
|
508
|
+
await post(`/subscribers/${config.subscriberId}/events`, {
|
|
509
|
+
name,
|
|
510
|
+
...(properties ? { properties } : {}),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function setConsentRequired(required) {
|
|
515
|
+
consentRequired = !!required;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function setConsentGiven(given) {
|
|
519
|
+
consentGiven = !!given;
|
|
520
|
+
if (consentGiven) currentToken().catch((error) => logError('Consent flush failed', error));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function deleteDevice() {
|
|
524
|
+
if (config?.subscriberId && networkAllowed()) {
|
|
525
|
+
await request('DELETE', `/subscribers/${config.subscriberId}`, {});
|
|
526
|
+
}
|
|
527
|
+
if (config) {
|
|
528
|
+
config.subscriberId = null;
|
|
529
|
+
config.token = null;
|
|
530
|
+
config.tags = {};
|
|
531
|
+
config.userHash = null;
|
|
532
|
+
}
|
|
533
|
+
pendingTags = {};
|
|
534
|
+
optedIn = true;
|
|
535
|
+
notifySubscription();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function clearAllNotifications() {
|
|
539
|
+
try {
|
|
540
|
+
const notifee = require('@notifee/react-native').default;
|
|
541
|
+
await notifee.cancelAllNotifications();
|
|
542
|
+
} catch {
|
|
543
|
+
/* notifee is optional */
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function removeNotification(id) {
|
|
548
|
+
if (Platform.OS !== 'android') return;
|
|
549
|
+
try {
|
|
550
|
+
const notifee = require('@notifee/react-native').default;
|
|
551
|
+
await notifee.cancelNotification(String(id));
|
|
552
|
+
} catch {
|
|
553
|
+
/* notifee is optional */
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function logout() {
|
|
558
|
+
if (!config?.subscriberId) return;
|
|
559
|
+
await post(`/subscribers/${config.subscriberId}/logout`, {});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const MsgeraPush = {
|
|
563
|
+
SDK_VERSION,
|
|
564
|
+
DEFAULT_API_BASE,
|
|
565
|
+
init,
|
|
566
|
+
requestPermission,
|
|
567
|
+
login,
|
|
568
|
+
logout,
|
|
569
|
+
setLogLevel,
|
|
570
|
+
registerBackgroundHandler,
|
|
571
|
+
pushSubscription,
|
|
572
|
+
addClickListener,
|
|
573
|
+
addForegroundListener,
|
|
574
|
+
addPermissionObserver,
|
|
575
|
+
addSubscriptionObserver,
|
|
576
|
+
canRequestPermission,
|
|
577
|
+
optOut,
|
|
578
|
+
optIn,
|
|
579
|
+
addTag,
|
|
580
|
+
addTags,
|
|
581
|
+
removeTag,
|
|
582
|
+
removeTags,
|
|
583
|
+
getTags,
|
|
584
|
+
setLanguage,
|
|
585
|
+
trackEvent,
|
|
586
|
+
setConsentRequired,
|
|
587
|
+
setConsentGiven,
|
|
588
|
+
deleteDevice,
|
|
589
|
+
clearAllNotifications,
|
|
590
|
+
removeNotification,
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
module.exports = {
|
|
594
|
+
...MsgeraPush,
|
|
595
|
+
MsgeraPush,
|
|
596
|
+
};
|