@clix-so/react-native-sdk 0.0.1 → 0.0.2-beta.2
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 +51 -5
- package/lib/module/core/Clix.js +9 -7
- package/lib/module/core/Clix.js.map +1 -1
- package/lib/module/core/ClixNotification.js +122 -0
- package/lib/module/core/ClixNotification.js.map +1 -0
- package/lib/module/services/DeviceService.js +27 -19
- package/lib/module/services/DeviceService.js.map +1 -1
- package/lib/module/services/EventAPIService.js +7 -1
- package/lib/module/services/EventAPIService.js.map +1 -1
- package/lib/module/services/EventService.js +8 -5
- package/lib/module/services/EventService.js.map +1 -1
- package/lib/module/services/NotificationService.js +92 -49
- package/lib/module/services/NotificationService.js.map +1 -1
- package/lib/module/services/StorageService.js +52 -15
- package/lib/module/services/StorageService.js.map +1 -1
- package/lib/module/services/TokenService.js +13 -13
- package/lib/module/services/TokenService.js.map +1 -1
- package/lib/module/utils/ClixDateFormatter.js +19 -0
- package/lib/module/utils/ClixDateFormatter.js.map +1 -0
- package/lib/typescript/src/core/Clix.d.ts +11 -11
- package/lib/typescript/src/core/Clix.d.ts.map +1 -1
- package/lib/typescript/src/core/ClixNotification.d.ts +25 -0
- package/lib/typescript/src/core/ClixNotification.d.ts.map +1 -0
- package/lib/typescript/src/services/DeviceService.d.ts +3 -2
- package/lib/typescript/src/services/DeviceService.d.ts.map +1 -1
- package/lib/typescript/src/services/EventAPIService.d.ts +1 -1
- package/lib/typescript/src/services/EventAPIService.d.ts.map +1 -1
- package/lib/typescript/src/services/EventService.d.ts +1 -1
- package/lib/typescript/src/services/EventService.d.ts.map +1 -1
- package/lib/typescript/src/services/NotificationService.d.ts +21 -1
- package/lib/typescript/src/services/NotificationService.d.ts.map +1 -1
- package/lib/typescript/src/services/StorageService.d.ts +11 -6
- package/lib/typescript/src/services/StorageService.d.ts.map +1 -1
- package/lib/typescript/src/services/TokenService.d.ts +5 -5
- package/lib/typescript/src/services/TokenService.d.ts.map +1 -1
- package/lib/typescript/src/utils/ClixDateFormatter.d.ts +4 -0
- package/lib/typescript/src/utils/ClixDateFormatter.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/core/Clix.ts +17 -21
- package/src/core/ClixNotification.ts +157 -0
- package/src/services/DeviceService.ts +35 -20
- package/src/services/EventAPIService.ts +5 -1
- package/src/services/EventService.ts +13 -5
- package/src/services/NotificationService.ts +148 -54
- package/src/services/StorageService.ts +60 -16
- package/src/services/TokenService.ts +13 -15
- package/src/utils/ClixDateFormatter.ts +19 -0
|
@@ -20,8 +20,8 @@ export class DeviceService {
|
|
|
20
20
|
private readonly deviceAPIService: DeviceAPIService
|
|
21
21
|
) {}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
const existingId =
|
|
23
|
+
getCurrentDeviceId(): string {
|
|
24
|
+
const existingId = this.storageService.get<string>(
|
|
25
25
|
DeviceService.DEVICE_ID_KEY
|
|
26
26
|
);
|
|
27
27
|
if (existingId) {
|
|
@@ -29,13 +29,13 @@ export class DeviceService {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const newId = UUID.generate();
|
|
32
|
-
|
|
32
|
+
this.storageService.set(DeviceService.DEVICE_ID_KEY, newId);
|
|
33
33
|
return newId;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
async setProjectUserId(projectUserId: string): Promise<void> {
|
|
37
37
|
try {
|
|
38
|
-
const deviceId =
|
|
38
|
+
const deviceId = this.getCurrentDeviceId();
|
|
39
39
|
await this.deviceAPIService.setProjectUserId(deviceId, projectUserId);
|
|
40
40
|
ClixLogger.debug(`Project user ID set: ${projectUserId}`);
|
|
41
41
|
} catch (error) {
|
|
@@ -49,7 +49,7 @@ export class DeviceService {
|
|
|
49
49
|
|
|
50
50
|
async removeProjectUserId(): Promise<void> {
|
|
51
51
|
try {
|
|
52
|
-
const deviceId =
|
|
52
|
+
const deviceId = this.getCurrentDeviceId();
|
|
53
53
|
await this.deviceAPIService.removeProjectUserId(deviceId);
|
|
54
54
|
ClixLogger.debug('Project user ID removed');
|
|
55
55
|
} catch (error) {
|
|
@@ -67,7 +67,7 @@ export class DeviceService {
|
|
|
67
67
|
ClixUserProperty.of(key, value)
|
|
68
68
|
);
|
|
69
69
|
|
|
70
|
-
const deviceId =
|
|
70
|
+
const deviceId = this.getCurrentDeviceId();
|
|
71
71
|
await this.deviceAPIService.upsertUserProperties(
|
|
72
72
|
deviceId,
|
|
73
73
|
userProperties
|
|
@@ -87,7 +87,7 @@ export class DeviceService {
|
|
|
87
87
|
|
|
88
88
|
async removeUserProperties(names: string[]): Promise<void> {
|
|
89
89
|
try {
|
|
90
|
-
const deviceId =
|
|
90
|
+
const deviceId = this.getCurrentDeviceId();
|
|
91
91
|
await this.deviceAPIService.removeUserProperties(deviceId, names);
|
|
92
92
|
|
|
93
93
|
ClixLogger.debug(`User properties removed: ${names.join(', ')}`);
|
|
@@ -102,9 +102,9 @@ export class DeviceService {
|
|
|
102
102
|
|
|
103
103
|
async upsertToken(token: string, tokenType: string = 'FCM'): Promise<void> {
|
|
104
104
|
try {
|
|
105
|
-
|
|
105
|
+
this.tokenService.saveToken(token);
|
|
106
106
|
|
|
107
|
-
const deviceId =
|
|
107
|
+
const deviceId = this.getCurrentDeviceId();
|
|
108
108
|
const device = await this.createDevice(deviceId, token);
|
|
109
109
|
|
|
110
110
|
await this.deviceAPIService.registerDevice(device);
|
|
@@ -132,17 +132,27 @@ export class DeviceService {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
async upsertIsPushPermissionGranted(isGranted: boolean): Promise<void> {
|
|
136
136
|
try {
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
137
|
+
const deviceId = this.getCurrentDeviceId();
|
|
138
|
+
const currentToken = this.tokenService.getCurrentToken();
|
|
139
|
+
const device = await this.createDevice(deviceId, currentToken, isGranted);
|
|
140
|
+
|
|
141
|
+
await this.deviceAPIService.registerDevice(device);
|
|
142
|
+
ClixLogger.debug(
|
|
143
|
+
`Push permission status upserted: ${isGranted ? 'granted' : 'denied'}`
|
|
140
144
|
);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
} catch (error) {
|
|
146
|
+
ClixLogger.error('Failed to upsert push permission status', error);
|
|
147
|
+
throw ClixError.unknownError({
|
|
148
|
+
reason: `Failed to upsert push permission status: ${error}`,
|
|
149
|
+
cause: error,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
144
153
|
|
|
145
|
-
|
|
154
|
+
private async getPushPermissionStatus(): Promise<boolean> {
|
|
155
|
+
try {
|
|
146
156
|
const authStatus = await messaging().hasPermission();
|
|
147
157
|
const isGranted =
|
|
148
158
|
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
|
|
@@ -161,7 +171,11 @@ export class DeviceService {
|
|
|
161
171
|
}
|
|
162
172
|
}
|
|
163
173
|
|
|
164
|
-
async createDevice(
|
|
174
|
+
async createDevice(
|
|
175
|
+
deviceId: string,
|
|
176
|
+
token?: string,
|
|
177
|
+
isPushPermissionGranted?: boolean
|
|
178
|
+
): Promise<ClixDevice> {
|
|
165
179
|
const platform = DeviceInfo.getSystemName();
|
|
166
180
|
const osName = DeviceInfo.getSystemName();
|
|
167
181
|
const osVersion = DeviceInfo.getSystemVersion();
|
|
@@ -174,7 +188,8 @@ export class DeviceService {
|
|
|
174
188
|
const localeLanguage = locale.split('-')[0] || 'en';
|
|
175
189
|
const localeRegion = locale.split('-')[1] || 'US';
|
|
176
190
|
let adId: string | undefined;
|
|
177
|
-
const
|
|
191
|
+
const pushPermissionGranted =
|
|
192
|
+
isPushPermissionGranted ?? (await this.getPushPermissionStatus());
|
|
178
193
|
const sdkVersion = await ClixVersion.getVersion();
|
|
179
194
|
|
|
180
195
|
return new ClixDevice({
|
|
@@ -192,7 +207,7 @@ export class DeviceService {
|
|
|
192
207
|
sdkType: 'react-native',
|
|
193
208
|
sdkVersion,
|
|
194
209
|
adId,
|
|
195
|
-
isPushPermissionGranted,
|
|
210
|
+
isPushPermissionGranted: pushPermissionGranted,
|
|
196
211
|
pushToken: token,
|
|
197
212
|
pushTokenType: token
|
|
198
213
|
? Platform.OS === 'ios'
|
|
@@ -8,7 +8,9 @@ export class EventAPIService {
|
|
|
8
8
|
deviceId: string,
|
|
9
9
|
name: string,
|
|
10
10
|
properties: Record<string, any>,
|
|
11
|
-
messageId?: string
|
|
11
|
+
messageId?: string,
|
|
12
|
+
userJourneyId?: string,
|
|
13
|
+
userJourneyNodeId?: string
|
|
12
14
|
): Promise<void> {
|
|
13
15
|
try {
|
|
14
16
|
ClixLogger.debug(`Tracking event: ${name} for device: ${deviceId}`);
|
|
@@ -19,6 +21,8 @@ export class EventAPIService {
|
|
|
19
21
|
event_property: {
|
|
20
22
|
custom_properties: properties,
|
|
21
23
|
...(messageId && { message_id: messageId }),
|
|
24
|
+
...(userJourneyId && { user_journey_id: userJourneyId }),
|
|
25
|
+
...(userJourneyNodeId && { user_journey_node_id: userJourneyNodeId }),
|
|
22
26
|
},
|
|
23
27
|
};
|
|
24
28
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ClixDateFormatter } from '../utils/ClixDateFormatter';
|
|
1
2
|
import { ClixLogger } from '../utils/logging/ClixLogger';
|
|
2
3
|
import { DeviceService } from './DeviceService';
|
|
3
4
|
import { EventAPIService } from './EventAPIService';
|
|
@@ -11,19 +12,24 @@ export class EventService {
|
|
|
11
12
|
async trackEvent(
|
|
12
13
|
name: string,
|
|
13
14
|
properties?: Record<string, any>,
|
|
14
|
-
messageId?: string
|
|
15
|
+
messageId?: string,
|
|
16
|
+
userJourneyId?: string,
|
|
17
|
+
userJourneyNodeId?: string
|
|
15
18
|
): Promise<void> {
|
|
16
19
|
try {
|
|
17
20
|
ClixLogger.debug(`Tracking event: ${name}`);
|
|
18
21
|
|
|
19
|
-
const deviceId =
|
|
22
|
+
const deviceId = this.deviceService.getCurrentDeviceId();
|
|
20
23
|
|
|
21
24
|
const cleanProperties: Record<string, any> = {};
|
|
22
25
|
if (properties) {
|
|
23
26
|
Object.entries(properties).forEach(([key, value]) => {
|
|
24
|
-
if (value
|
|
25
|
-
cleanProperties[key] = value;
|
|
27
|
+
if (value instanceof Date) {
|
|
28
|
+
cleanProperties[key] = ClixDateFormatter.format(value);
|
|
29
|
+
return;
|
|
26
30
|
}
|
|
31
|
+
|
|
32
|
+
cleanProperties[key] = value;
|
|
27
33
|
});
|
|
28
34
|
}
|
|
29
35
|
|
|
@@ -31,7 +37,9 @@ export class EventService {
|
|
|
31
37
|
deviceId,
|
|
32
38
|
name,
|
|
33
39
|
cleanProperties,
|
|
34
|
-
messageId
|
|
40
|
+
messageId,
|
|
41
|
+
userJourneyId,
|
|
42
|
+
userJourneyNodeId
|
|
35
43
|
);
|
|
36
44
|
|
|
37
45
|
ClixLogger.debug(`Event tracked successfully: ${name}`);
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import notifee, {
|
|
2
2
|
AndroidImportance,
|
|
3
3
|
AndroidStyle,
|
|
4
|
+
AuthorizationStatus,
|
|
4
5
|
EventType,
|
|
5
6
|
type AndroidChannel,
|
|
6
7
|
type Event,
|
|
8
|
+
type NotificationSettings,
|
|
7
9
|
} from '@notifee/react-native';
|
|
8
10
|
import messaging, {
|
|
9
11
|
FirebaseMessagingTypes,
|
|
10
12
|
} from '@react-native-firebase/messaging';
|
|
11
13
|
import { Linking, Platform } from 'react-native';
|
|
12
|
-
import type { ClixConfig } from '../core/ClixConfig';
|
|
13
14
|
import { ClixPushNotificationPayload } from '../models/ClixPushNotificationPayload';
|
|
14
15
|
import { ClixLogger } from '../utils/logging/ClixLogger';
|
|
15
16
|
import { UUID } from '../utils/UUID';
|
|
16
|
-
import { ClixAPIClient } from './ClixAPIClient';
|
|
17
|
-
import { DeviceAPIService } from './DeviceAPIService';
|
|
18
17
|
import { DeviceService } from './DeviceService';
|
|
19
|
-
import { EventAPIService } from './EventAPIService';
|
|
20
18
|
import { EventService } from './EventService';
|
|
21
19
|
import { StorageService } from './StorageService';
|
|
22
20
|
import { TokenService } from './TokenService';
|
|
@@ -27,6 +25,19 @@ interface NotificationContent {
|
|
|
27
25
|
imageUrl?: string;
|
|
28
26
|
}
|
|
29
27
|
|
|
28
|
+
type NotificationData = Record<string, any>;
|
|
29
|
+
|
|
30
|
+
export type ForegroundMessageHandler = (
|
|
31
|
+
data: NotificationData
|
|
32
|
+
) => Promise<boolean> | boolean;
|
|
33
|
+
export type BackgroundMessageHandler = (
|
|
34
|
+
data: NotificationData
|
|
35
|
+
) => Promise<void> | void;
|
|
36
|
+
export type NotificationOpenedHandler = (
|
|
37
|
+
data: NotificationData
|
|
38
|
+
) => Promise<void> | void;
|
|
39
|
+
export type FcmTokenErrorHandler = (error: Error) => Promise<void> | void;
|
|
40
|
+
|
|
30
41
|
export class NotificationService {
|
|
31
42
|
private static instance: NotificationService | null = null;
|
|
32
43
|
|
|
@@ -50,6 +61,12 @@ export class NotificationService {
|
|
|
50
61
|
private deviceService?: DeviceService;
|
|
51
62
|
private tokenService?: TokenService;
|
|
52
63
|
|
|
64
|
+
private autoHandleLandingUrl = true;
|
|
65
|
+
private messageHandler?: ForegroundMessageHandler;
|
|
66
|
+
private backgroundMessageHandler?: BackgroundMessageHandler;
|
|
67
|
+
private openedHandler?: NotificationOpenedHandler;
|
|
68
|
+
private fcmTokenErrorHandler?: FcmTokenErrorHandler;
|
|
69
|
+
|
|
53
70
|
private unsubscribeForegroundMessage?: () => void;
|
|
54
71
|
private unsubscribeNotificationOpened?: () => void;
|
|
55
72
|
private unsubscribeTokenRefresh?: () => void;
|
|
@@ -110,6 +127,7 @@ export class NotificationService {
|
|
|
110
127
|
return this.currentPushToken;
|
|
111
128
|
} catch (error) {
|
|
112
129
|
ClixLogger.error('Failed to get push token', error);
|
|
130
|
+
await this.handleFcmTokenError(error);
|
|
113
131
|
return null;
|
|
114
132
|
}
|
|
115
133
|
}
|
|
@@ -124,16 +142,27 @@ export class NotificationService {
|
|
|
124
142
|
ClixLogger.debug('Notification service cleaned up');
|
|
125
143
|
}
|
|
126
144
|
|
|
145
|
+
setMessageHandler(handler?: ForegroundMessageHandler): void {
|
|
146
|
+
this.messageHandler = handler;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setBackgroundMessageHandler(handler?: BackgroundMessageHandler): void {
|
|
150
|
+
this.backgroundMessageHandler = handler;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setNotificationOpenedHandler(handler?: NotificationOpenedHandler): void {
|
|
154
|
+
this.openedHandler = handler;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setFcmTokenErrorHandler(handler?: FcmTokenErrorHandler): void {
|
|
158
|
+
this.fcmTokenErrorHandler = handler;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setAutoHandleLandingUrl(enable: boolean): void {
|
|
162
|
+
this.autoHandleLandingUrl = enable;
|
|
163
|
+
}
|
|
164
|
+
|
|
127
165
|
private async initializeNotificationDisplayService(): Promise<void> {
|
|
128
|
-
await notifee.requestPermission({
|
|
129
|
-
alert: true,
|
|
130
|
-
badge: true,
|
|
131
|
-
sound: true,
|
|
132
|
-
criticalAlert: false,
|
|
133
|
-
announcement: false,
|
|
134
|
-
carPlay: false,
|
|
135
|
-
provisional: false,
|
|
136
|
-
});
|
|
137
166
|
if (Platform.OS === 'android') {
|
|
138
167
|
await this.createNotificationChannels();
|
|
139
168
|
}
|
|
@@ -194,13 +223,8 @@ export class NotificationService {
|
|
|
194
223
|
|
|
195
224
|
private async initializeMessageService(): Promise<void> {
|
|
196
225
|
this.setupMessageHandlers();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
await this.getAndUpdateToken();
|
|
200
|
-
this.setupTokenRefreshListener();
|
|
201
|
-
} else {
|
|
202
|
-
ClixLogger.warn('Push notification permission denied');
|
|
203
|
-
}
|
|
226
|
+
await this.getAndUpdateToken();
|
|
227
|
+
this.setupTokenRefreshListener();
|
|
204
228
|
}
|
|
205
229
|
|
|
206
230
|
private setupMessageHandlers(): void {
|
|
@@ -226,16 +250,25 @@ export class NotificationService {
|
|
|
226
250
|
private async handleBackgroundMessage(
|
|
227
251
|
remoteMessage: FirebaseMessagingTypes.RemoteMessage
|
|
228
252
|
): Promise<void> {
|
|
253
|
+
ClixLogger.debug('Handling background message:', remoteMessage.messageId);
|
|
254
|
+
|
|
255
|
+
const data = remoteMessage.data ?? {};
|
|
229
256
|
try {
|
|
230
|
-
|
|
257
|
+
await this.backgroundMessageHandler?.(data);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
ClixLogger.warn('Background message handler failed', error);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const clixPayload = this.parseClixPayload(data);
|
|
231
264
|
if (!clixPayload) {
|
|
232
265
|
ClixLogger.warn('No Clix payload found in background message');
|
|
233
266
|
return;
|
|
234
267
|
}
|
|
235
268
|
|
|
236
|
-
|
|
269
|
+
this.storageService.set('last_background_notification', {
|
|
237
270
|
messageId: remoteMessage.messageId,
|
|
238
|
-
data:
|
|
271
|
+
data: data,
|
|
239
272
|
timestamp: Date.now(),
|
|
240
273
|
clixMessageId: clixPayload.messageId,
|
|
241
274
|
campaignId: clixPayload.campaignId,
|
|
@@ -255,6 +288,8 @@ export class NotificationService {
|
|
|
255
288
|
private async handleForegroundMessage(
|
|
256
289
|
remoteMessage: FirebaseMessagingTypes.RemoteMessage
|
|
257
290
|
): Promise<void> {
|
|
291
|
+
ClixLogger.debug('Handling foreground message:', remoteMessage.messageId);
|
|
292
|
+
|
|
258
293
|
try {
|
|
259
294
|
const messageId = remoteMessage.messageId;
|
|
260
295
|
if (!messageId) {
|
|
@@ -270,12 +305,24 @@ export class NotificationService {
|
|
|
270
305
|
return;
|
|
271
306
|
}
|
|
272
307
|
|
|
273
|
-
const
|
|
308
|
+
const data = remoteMessage.data ?? {};
|
|
309
|
+
const clixPayload = this.parseClixPayload(data);
|
|
274
310
|
if (clixPayload) {
|
|
275
311
|
ClixLogger.debug('Parsed Clix payload:', clixPayload);
|
|
276
312
|
this.processedMessageIds.add(messageId);
|
|
277
|
-
|
|
278
|
-
|
|
313
|
+
if (Platform.OS === 'android') {
|
|
314
|
+
// NOTE(nyanxyz): on iOS, Received event is tracked in NSE
|
|
315
|
+
await this.handlePushReceived(data);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (await this.shouldDisplayForegroundNotification(data)) {
|
|
319
|
+
await this.displayNotification(remoteMessage, clixPayload);
|
|
320
|
+
} else {
|
|
321
|
+
ClixLogger.debug(
|
|
322
|
+
'Foreground message suppressed by user handler:',
|
|
323
|
+
messageId
|
|
324
|
+
);
|
|
325
|
+
}
|
|
279
326
|
} else {
|
|
280
327
|
ClixLogger.warn('No Clix payload found in foreground message');
|
|
281
328
|
}
|
|
@@ -284,8 +331,8 @@ export class NotificationService {
|
|
|
284
331
|
}
|
|
285
332
|
}
|
|
286
333
|
|
|
287
|
-
|
|
288
|
-
const settings = await
|
|
334
|
+
async requestPermission(): Promise<NotificationSettings> {
|
|
335
|
+
const settings = await notifee.requestPermission({
|
|
289
336
|
alert: true,
|
|
290
337
|
badge: true,
|
|
291
338
|
sound: true,
|
|
@@ -295,13 +342,35 @@ export class NotificationService {
|
|
|
295
342
|
criticalAlert: false,
|
|
296
343
|
});
|
|
297
344
|
ClixLogger.debug('Push notification permission status:', settings);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
settings.
|
|
301
|
-
|
|
345
|
+
|
|
346
|
+
const isGranted =
|
|
347
|
+
settings.authorizationStatus === AuthorizationStatus.AUTHORIZED ||
|
|
348
|
+
settings.authorizationStatus === AuthorizationStatus.PROVISIONAL;
|
|
349
|
+
await this.setPermissionGranted(isGranted);
|
|
350
|
+
|
|
302
351
|
return settings;
|
|
303
352
|
}
|
|
304
353
|
|
|
354
|
+
async setPermissionGranted(isGranted: boolean): Promise<void> {
|
|
355
|
+
if (!this.deviceService) {
|
|
356
|
+
ClixLogger.debug(
|
|
357
|
+
'Device service is not initialized, skipping push permission upsert'
|
|
358
|
+
);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
await this.deviceService.upsertIsPushPermissionGranted(isGranted);
|
|
364
|
+
ClixLogger.debug(
|
|
365
|
+
`Push permission status reported to server: ${
|
|
366
|
+
isGranted ? 'granted' : 'denied'
|
|
367
|
+
}`
|
|
368
|
+
);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
ClixLogger.warn('Failed to upsert push permission status', error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
305
374
|
private setupTokenRefreshListener(): void {
|
|
306
375
|
this.unsubscribeTokenRefresh = this.messagingService.onTokenRefresh(
|
|
307
376
|
async (token: string) => {
|
|
@@ -311,6 +380,7 @@ export class NotificationService {
|
|
|
311
380
|
await this.saveAndRegisterToken(token);
|
|
312
381
|
} catch (error) {
|
|
313
382
|
ClixLogger.error('Failed to handle token refresh', error);
|
|
383
|
+
await this.handleFcmTokenError(error);
|
|
314
384
|
}
|
|
315
385
|
}
|
|
316
386
|
);
|
|
@@ -345,20 +415,23 @@ export class NotificationService {
|
|
|
345
415
|
|
|
346
416
|
private async getOrFetchToken(): Promise<string | null> {
|
|
347
417
|
if (this.tokenService) {
|
|
348
|
-
const savedToken =
|
|
418
|
+
const savedToken = this.tokenService.getCurrentToken();
|
|
349
419
|
if (savedToken) return savedToken;
|
|
350
420
|
}
|
|
421
|
+
|
|
422
|
+
await this.messagingService.registerDeviceForRemoteMessages();
|
|
351
423
|
const token = await this.messagingService.getToken();
|
|
424
|
+
|
|
352
425
|
if (token) {
|
|
353
426
|
ClixLogger.debug('Got push token:', token.substring(0, 20) + '...');
|
|
354
|
-
|
|
427
|
+
this.tokenService?.saveToken(token);
|
|
355
428
|
}
|
|
356
429
|
return token;
|
|
357
430
|
}
|
|
358
431
|
|
|
359
432
|
private async saveAndRegisterToken(token: string): Promise<void> {
|
|
360
433
|
if (this.tokenService) {
|
|
361
|
-
|
|
434
|
+
this.tokenService.saveToken(token);
|
|
362
435
|
ClixLogger.debug('New push token saved via TokenService');
|
|
363
436
|
}
|
|
364
437
|
await this.registerTokenWithServer(token);
|
|
@@ -565,7 +638,9 @@ export class NotificationService {
|
|
|
565
638
|
if (clixPayload) {
|
|
566
639
|
await this.trackPushEvent('PUSH_NOTIFICATION_TAPPED', clixPayload);
|
|
567
640
|
}
|
|
568
|
-
|
|
641
|
+
if (this.autoHandleLandingUrl) {
|
|
642
|
+
await this.handleUrlNavigation(data);
|
|
643
|
+
}
|
|
569
644
|
ClixLogger.debug('Push notification tapped and processed');
|
|
570
645
|
} catch (error) {
|
|
571
646
|
ClixLogger.error('Failed to handle push tapped', error);
|
|
@@ -575,6 +650,12 @@ export class NotificationService {
|
|
|
575
650
|
private async handleNotificationTap(
|
|
576
651
|
data: Record<string, any>
|
|
577
652
|
): Promise<void> {
|
|
653
|
+
try {
|
|
654
|
+
await this.openedHandler?.(data);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
ClixLogger.error('Failed to handle notification tap', error);
|
|
657
|
+
}
|
|
658
|
+
|
|
578
659
|
try {
|
|
579
660
|
await this.handlePushTapped(data);
|
|
580
661
|
} catch (error) {
|
|
@@ -687,35 +768,23 @@ export class NotificationService {
|
|
|
687
768
|
return;
|
|
688
769
|
}
|
|
689
770
|
try {
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
'clix_config'
|
|
693
|
-
);
|
|
771
|
+
const configData =
|
|
772
|
+
this.storageService.get<Record<string, any>>('clix_config');
|
|
694
773
|
if (!configData) {
|
|
695
774
|
ClixLogger.error('No Clix config found in storage');
|
|
696
775
|
return;
|
|
697
776
|
}
|
|
698
|
-
let deviceId =
|
|
777
|
+
let deviceId = this.storageService.get<string>('clix_device_id');
|
|
699
778
|
if (!deviceId) {
|
|
700
779
|
ClixLogger.warn(
|
|
701
780
|
'No device ID found in storage, generating new device ID'
|
|
702
781
|
);
|
|
703
782
|
deviceId = UUID.generate();
|
|
704
|
-
|
|
783
|
+
this.storageService.set('clix_device_id', deviceId);
|
|
705
784
|
}
|
|
706
|
-
|
|
707
|
-
const apiClient = new ClixAPIClient(config);
|
|
708
|
-
const deviceAPIService = new DeviceAPIService(apiClient);
|
|
709
|
-
const eventAPIService = new EventAPIService(apiClient);
|
|
710
|
-
const tokenService = new TokenService(storageService);
|
|
711
|
-
const deviceService = new DeviceService(
|
|
712
|
-
storageService,
|
|
713
|
-
tokenService,
|
|
714
|
-
deviceAPIService
|
|
715
|
-
);
|
|
716
|
-
const eventService = new EventService(eventAPIService, deviceService);
|
|
785
|
+
|
|
717
786
|
const properties = this.extractTrackingProperties(clixPayload);
|
|
718
|
-
await eventService.trackEvent(
|
|
787
|
+
await this.eventService.trackEvent(
|
|
719
788
|
'PUSH_NOTIFICATION_RECEIVED',
|
|
720
789
|
properties,
|
|
721
790
|
messageId
|
|
@@ -727,4 +796,29 @@ export class NotificationService {
|
|
|
727
796
|
ClixLogger.error('Error tracking event in background', error);
|
|
728
797
|
}
|
|
729
798
|
}
|
|
799
|
+
|
|
800
|
+
private async shouldDisplayForegroundNotification(
|
|
801
|
+
data: Record<string, any>
|
|
802
|
+
): Promise<boolean> {
|
|
803
|
+
try {
|
|
804
|
+
const result = await this.messageHandler?.(data);
|
|
805
|
+
return result !== false;
|
|
806
|
+
} catch (error) {
|
|
807
|
+
ClixLogger.warn(
|
|
808
|
+
'Foreground message handler failed, displaying notification by default',
|
|
809
|
+
error
|
|
810
|
+
);
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private async handleFcmTokenError(error: any): Promise<void> {
|
|
816
|
+
try {
|
|
817
|
+
const errorInstance =
|
|
818
|
+
error instanceof Error ? error : new Error(String(error));
|
|
819
|
+
await this.fcmTokenErrorHandler?.(errorInstance);
|
|
820
|
+
} catch (handlerError) {
|
|
821
|
+
ClixLogger.warn('FCM token error handler failed', handlerError);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
730
824
|
}
|
|
@@ -1,20 +1,63 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as MMKVModule from 'react-native-mmkv';
|
|
2
2
|
import { ClixLogger } from '../utils/logging/ClixLogger';
|
|
3
3
|
|
|
4
|
+
// Support both v2/v3 (MMKV class) and v4 (createMMKV function)
|
|
5
|
+
type MMKVInstance = {
|
|
6
|
+
set: (key: string, value: string | number | boolean) => void;
|
|
7
|
+
getString: (key: string) => string | undefined;
|
|
8
|
+
delete?: (key: string) => void; // v2/v3
|
|
9
|
+
remove?: (key: string) => void; // v4
|
|
10
|
+
clearAll: () => void;
|
|
11
|
+
getAllKeys: () => string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
4
14
|
export class StorageService {
|
|
5
|
-
private storage:
|
|
15
|
+
private storage: MMKVInstance;
|
|
16
|
+
|
|
17
|
+
constructor(projectId: string) {
|
|
18
|
+
this.storage = this.initializeCompat(projectId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private initializeCompat(projectId: string) {
|
|
22
|
+
const storageId = `clix.${projectId}`;
|
|
23
|
+
|
|
24
|
+
// v4 API (createMMKV function)
|
|
25
|
+
if (typeof MMKVModule.createMMKV === 'function') {
|
|
26
|
+
return MMKVModule.createMMKV({
|
|
27
|
+
id: storageId,
|
|
28
|
+
encryptionKey: undefined, // Add encryption if needed
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// v2/v3 API (MMKV class)
|
|
32
|
+
else if (typeof (MMKVModule as any).MMKV === 'function') {
|
|
33
|
+
const { MMKV } = MMKVModule as any;
|
|
34
|
+
return new MMKV({
|
|
35
|
+
id: storageId,
|
|
36
|
+
encryptionKey: undefined, // Add encryption if needed
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error('No compatible MMKV storage API found');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
6
42
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Delete a key from storage (works with both v2/v3 and v4 APIs)
|
|
45
|
+
*/
|
|
46
|
+
private removeCompat(key: string): void {
|
|
47
|
+
// v4 uses remove(), v2/v3 uses delete()
|
|
48
|
+
if (typeof this.storage.remove === 'function') {
|
|
49
|
+
this.storage.remove(key);
|
|
50
|
+
} else if (typeof this.storage.delete === 'function') {
|
|
51
|
+
this.storage.delete(key);
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error('No compatible delete method found on storage instance');
|
|
54
|
+
}
|
|
12
55
|
}
|
|
13
56
|
|
|
14
|
-
|
|
15
|
-
if (value === undefined) {
|
|
57
|
+
set<T>(key: string, value: T): void {
|
|
58
|
+
if (value === undefined || value === null) {
|
|
16
59
|
try {
|
|
17
|
-
this.
|
|
60
|
+
this.removeCompat(key);
|
|
18
61
|
} catch (error) {
|
|
19
62
|
ClixLogger.error(`Failed to remove value for key: ${key}`, error);
|
|
20
63
|
}
|
|
@@ -31,10 +74,11 @@ export class StorageService {
|
|
|
31
74
|
}
|
|
32
75
|
}
|
|
33
76
|
|
|
34
|
-
|
|
77
|
+
get<T>(key: string): T | undefined {
|
|
35
78
|
try {
|
|
36
79
|
const data = this.storage.getString(key);
|
|
37
80
|
if (data === null || data === undefined) return undefined;
|
|
81
|
+
|
|
38
82
|
try {
|
|
39
83
|
const decoded = JSON.parse(data);
|
|
40
84
|
return decoded as T;
|
|
@@ -43,7 +87,7 @@ export class StorageService {
|
|
|
43
87
|
ClixLogger.debug(
|
|
44
88
|
`Found legacy string value for key: ${key}, migrating to JSON format`
|
|
45
89
|
);
|
|
46
|
-
|
|
90
|
+
this.set(key, data);
|
|
47
91
|
return data as T;
|
|
48
92
|
}
|
|
49
93
|
} catch (error) {
|
|
@@ -53,9 +97,9 @@ export class StorageService {
|
|
|
53
97
|
}
|
|
54
98
|
}
|
|
55
99
|
|
|
56
|
-
|
|
100
|
+
remove(key: string) {
|
|
57
101
|
try {
|
|
58
|
-
this.
|
|
102
|
+
this.removeCompat(key);
|
|
59
103
|
} catch (error) {
|
|
60
104
|
ClixLogger.error(`Failed to remove key: ${key}`, error);
|
|
61
105
|
// Don't throw to prevent initialization failure
|
|
@@ -63,7 +107,7 @@ export class StorageService {
|
|
|
63
107
|
}
|
|
64
108
|
}
|
|
65
109
|
|
|
66
|
-
|
|
110
|
+
clear() {
|
|
67
111
|
try {
|
|
68
112
|
this.storage.clearAll();
|
|
69
113
|
} catch (error) {
|
|
@@ -72,7 +116,7 @@ export class StorageService {
|
|
|
72
116
|
}
|
|
73
117
|
}
|
|
74
118
|
|
|
75
|
-
|
|
119
|
+
getAllKeys() {
|
|
76
120
|
try {
|
|
77
121
|
const keys = this.storage.getAllKeys();
|
|
78
122
|
return Array.from(keys);
|