@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.
Files changed (47) hide show
  1. package/README.md +51 -5
  2. package/lib/module/core/Clix.js +9 -7
  3. package/lib/module/core/Clix.js.map +1 -1
  4. package/lib/module/core/ClixNotification.js +122 -0
  5. package/lib/module/core/ClixNotification.js.map +1 -0
  6. package/lib/module/services/DeviceService.js +27 -19
  7. package/lib/module/services/DeviceService.js.map +1 -1
  8. package/lib/module/services/EventAPIService.js +7 -1
  9. package/lib/module/services/EventAPIService.js.map +1 -1
  10. package/lib/module/services/EventService.js +8 -5
  11. package/lib/module/services/EventService.js.map +1 -1
  12. package/lib/module/services/NotificationService.js +92 -49
  13. package/lib/module/services/NotificationService.js.map +1 -1
  14. package/lib/module/services/StorageService.js +52 -15
  15. package/lib/module/services/StorageService.js.map +1 -1
  16. package/lib/module/services/TokenService.js +13 -13
  17. package/lib/module/services/TokenService.js.map +1 -1
  18. package/lib/module/utils/ClixDateFormatter.js +19 -0
  19. package/lib/module/utils/ClixDateFormatter.js.map +1 -0
  20. package/lib/typescript/src/core/Clix.d.ts +11 -11
  21. package/lib/typescript/src/core/Clix.d.ts.map +1 -1
  22. package/lib/typescript/src/core/ClixNotification.d.ts +25 -0
  23. package/lib/typescript/src/core/ClixNotification.d.ts.map +1 -0
  24. package/lib/typescript/src/services/DeviceService.d.ts +3 -2
  25. package/lib/typescript/src/services/DeviceService.d.ts.map +1 -1
  26. package/lib/typescript/src/services/EventAPIService.d.ts +1 -1
  27. package/lib/typescript/src/services/EventAPIService.d.ts.map +1 -1
  28. package/lib/typescript/src/services/EventService.d.ts +1 -1
  29. package/lib/typescript/src/services/EventService.d.ts.map +1 -1
  30. package/lib/typescript/src/services/NotificationService.d.ts +21 -1
  31. package/lib/typescript/src/services/NotificationService.d.ts.map +1 -1
  32. package/lib/typescript/src/services/StorageService.d.ts +11 -6
  33. package/lib/typescript/src/services/StorageService.d.ts.map +1 -1
  34. package/lib/typescript/src/services/TokenService.d.ts +5 -5
  35. package/lib/typescript/src/services/TokenService.d.ts.map +1 -1
  36. package/lib/typescript/src/utils/ClixDateFormatter.d.ts +4 -0
  37. package/lib/typescript/src/utils/ClixDateFormatter.d.ts.map +1 -0
  38. package/package.json +2 -2
  39. package/src/core/Clix.ts +17 -21
  40. package/src/core/ClixNotification.ts +157 -0
  41. package/src/services/DeviceService.ts +35 -20
  42. package/src/services/EventAPIService.ts +5 -1
  43. package/src/services/EventService.ts +13 -5
  44. package/src/services/NotificationService.ts +148 -54
  45. package/src/services/StorageService.ts +60 -16
  46. package/src/services/TokenService.ts +13 -15
  47. 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
- async getCurrentDeviceId(): Promise<string> {
24
- const existingId = await this.storageService.get<string>(
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
- await this.storageService.set(DeviceService.DEVICE_ID_KEY, newId);
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 = await this.getCurrentDeviceId();
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 = await this.getCurrentDeviceId();
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 = await this.getCurrentDeviceId();
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 = await this.getCurrentDeviceId();
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
- await this.tokenService.saveToken(token);
105
+ this.tokenService.saveToken(token);
106
106
 
107
- const deviceId = await this.getCurrentDeviceId();
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
- private async getPushPermissionStatus(): Promise<boolean> {
135
+ async upsertIsPushPermissionGranted(isGranted: boolean): Promise<void> {
136
136
  try {
137
- // First check stored permission status
138
- const storedStatus = await this.storageService.get<string>(
139
- 'notification_permission_status'
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
- if (storedStatus === 'authorized' || storedStatus === 'provisional') {
142
- return true;
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
- // If no stored status, check current Firebase messaging permission
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(deviceId: string, token?: string): Promise<ClixDevice> {
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 isPushPermissionGranted = await this.getPushPermissionStatus();
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 = await this.deviceService.getCurrentDeviceId();
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 !== null && value !== undefined) {
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
- const settings = await this.requestMessagePermission();
198
- if (settings !== messaging.AuthorizationStatus.DENIED) {
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
- const clixPayload = this.parseClixPayload(remoteMessage.data ?? {});
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
- await this.storageService.set('last_background_notification', {
269
+ this.storageService.set('last_background_notification', {
237
270
  messageId: remoteMessage.messageId,
238
- data: remoteMessage.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 clixPayload = this.parseClixPayload(remoteMessage.data ?? {});
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
- await this.handlePushReceived(remoteMessage.data ?? {});
278
- await this.displayNotification(remoteMessage, clixPayload);
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
- private async requestMessagePermission(): Promise<any> {
288
- const settings = await this.messagingService.requestPermission({
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
- await this.storageService.set(
299
- 'notification_permission_status',
300
- settings.toString()
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 = await this.tokenService.getCurrentToken();
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
- await this.tokenService?.saveToken(token);
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
- await this.tokenService.saveToken(token);
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
- await this.handleUrlNavigation(data);
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 storageService = new StorageService();
691
- const configData = await storageService.get<Record<string, any>>(
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 = await storageService.get<string>('clix_device_id');
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
- await storageService.set('clix_device_id', deviceId);
783
+ this.storageService.set('clix_device_id', deviceId);
705
784
  }
706
- const config = configData as ClixConfig;
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 { MMKV } from 'react-native-mmkv';
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: MMKV;
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
- constructor() {
8
- this.storage = new MMKV({
9
- id: 'clix-storage',
10
- encryptionKey: undefined, // Add encryption if needed
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
- async set<T>(key: string, value: T): Promise<void> {
15
- if (value === undefined) {
57
+ set<T>(key: string, value: T): void {
58
+ if (value === undefined || value === null) {
16
59
  try {
17
- this.storage.delete(key);
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
- async get<T>(key: string): Promise<T | undefined> {
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
- await this.set(key, data);
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
- async remove(key: string): Promise<void> {
100
+ remove(key: string) {
57
101
  try {
58
- this.storage.delete(key);
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
- async clear(): Promise<void> {
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
- async getAllKeys(): Promise<string[]> {
119
+ getAllKeys() {
76
120
  try {
77
121
  const keys = this.storage.getAllKeys();
78
122
  return Array.from(keys);