@clix-so/react-native-sdk 0.0.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.
Files changed (100) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +345 -0
  3. package/lib/module/core/Clix.js +217 -0
  4. package/lib/module/core/Clix.js.map +1 -0
  5. package/lib/module/core/ClixConfig.js +4 -0
  6. package/lib/module/core/ClixConfig.js.map +1 -0
  7. package/lib/module/core/ClixInitCoordinator.js +58 -0
  8. package/lib/module/core/ClixInitCoordinator.js.map +1 -0
  9. package/lib/module/core/ClixVersion.js +17 -0
  10. package/lib/module/core/ClixVersion.js.map +1 -0
  11. package/lib/module/index.js +7 -0
  12. package/lib/module/index.js.map +1 -0
  13. package/lib/module/models/ClixDevice.js +51 -0
  14. package/lib/module/models/ClixDevice.js.map +1 -0
  15. package/lib/module/models/ClixPushNotificationPayload.js +21 -0
  16. package/lib/module/models/ClixPushNotificationPayload.js.map +1 -0
  17. package/lib/module/models/ClixUserProperty.js +44 -0
  18. package/lib/module/models/ClixUserProperty.js.map +1 -0
  19. package/lib/module/package.json +1 -0
  20. package/lib/module/services/ClixAPIClient.js +172 -0
  21. package/lib/module/services/ClixAPIClient.js.map +1 -0
  22. package/lib/module/services/DeviceAPIService.js +112 -0
  23. package/lib/module/services/DeviceAPIService.js.map +1 -0
  24. package/lib/module/services/DeviceService.js +157 -0
  25. package/lib/module/services/DeviceService.js.map +1 -0
  26. package/lib/module/services/EventAPIService.js +36 -0
  27. package/lib/module/services/EventAPIService.js.map +1 -0
  28. package/lib/module/services/EventService.js +29 -0
  29. package/lib/module/services/EventService.js.map +1 -0
  30. package/lib/module/services/NotificationService.js +549 -0
  31. package/lib/module/services/NotificationService.js.map +1 -0
  32. package/lib/module/services/StorageService.js +76 -0
  33. package/lib/module/services/StorageService.js.map +1 -0
  34. package/lib/module/services/TokenService.js +71 -0
  35. package/lib/module/services/TokenService.js.map +1 -0
  36. package/lib/module/utils/ClixError.js +63 -0
  37. package/lib/module/utils/ClixError.js.map +1 -0
  38. package/lib/module/utils/UUID.js +28 -0
  39. package/lib/module/utils/UUID.js.map +1 -0
  40. package/lib/module/utils/logging/ClixLogger.js +55 -0
  41. package/lib/module/utils/logging/ClixLogger.js.map +1 -0
  42. package/lib/typescript/package.json +1 -0
  43. package/lib/typescript/src/core/Clix.d.ts +67 -0
  44. package/lib/typescript/src/core/Clix.d.ts.map +1 -0
  45. package/lib/typescript/src/core/ClixConfig.d.ts +9 -0
  46. package/lib/typescript/src/core/ClixConfig.d.ts.map +1 -0
  47. package/lib/typescript/src/core/ClixInitCoordinator.d.ts +16 -0
  48. package/lib/typescript/src/core/ClixInitCoordinator.d.ts.map +1 -0
  49. package/lib/typescript/src/core/ClixVersion.d.ts +6 -0
  50. package/lib/typescript/src/core/ClixVersion.d.ts.map +1 -0
  51. package/lib/typescript/src/index.d.ts +5 -0
  52. package/lib/typescript/src/index.d.ts.map +1 -0
  53. package/lib/typescript/src/models/ClixDevice.d.ts +42 -0
  54. package/lib/typescript/src/models/ClixDevice.d.ts.map +1 -0
  55. package/lib/typescript/src/models/ClixPushNotificationPayload.d.ts +23 -0
  56. package/lib/typescript/src/models/ClixPushNotificationPayload.d.ts.map +1 -0
  57. package/lib/typescript/src/models/ClixUserProperty.d.ts +19 -0
  58. package/lib/typescript/src/models/ClixUserProperty.d.ts.map +1 -0
  59. package/lib/typescript/src/services/ClixAPIClient.d.ts +35 -0
  60. package/lib/typescript/src/services/ClixAPIClient.d.ts.map +1 -0
  61. package/lib/typescript/src/services/DeviceAPIService.d.ts +13 -0
  62. package/lib/typescript/src/services/DeviceAPIService.d.ts.map +1 -0
  63. package/lib/typescript/src/services/DeviceService.d.ts +20 -0
  64. package/lib/typescript/src/services/DeviceService.d.ts.map +1 -0
  65. package/lib/typescript/src/services/EventAPIService.d.ts +7 -0
  66. package/lib/typescript/src/services/EventAPIService.d.ts.map +1 -0
  67. package/lib/typescript/src/services/EventService.d.ts +9 -0
  68. package/lib/typescript/src/services/EventService.d.ts.map +1 -0
  69. package/lib/typescript/src/services/NotificationService.d.ts +56 -0
  70. package/lib/typescript/src/services/NotificationService.d.ts.map +1 -0
  71. package/lib/typescript/src/services/StorageService.d.ts +10 -0
  72. package/lib/typescript/src/services/StorageService.d.ts.map +1 -0
  73. package/lib/typescript/src/services/TokenService.d.ts +15 -0
  74. package/lib/typescript/src/services/TokenService.d.ts.map +1 -0
  75. package/lib/typescript/src/utils/ClixError.d.ts +41 -0
  76. package/lib/typescript/src/utils/ClixError.d.ts.map +1 -0
  77. package/lib/typescript/src/utils/UUID.d.ts +14 -0
  78. package/lib/typescript/src/utils/UUID.d.ts.map +1 -0
  79. package/lib/typescript/src/utils/logging/ClixLogger.d.ts +18 -0
  80. package/lib/typescript/src/utils/logging/ClixLogger.d.ts.map +1 -0
  81. package/package.json +151 -0
  82. package/src/core/Clix.ts +256 -0
  83. package/src/core/ClixConfig.ts +9 -0
  84. package/src/core/ClixInitCoordinator.ts +65 -0
  85. package/src/core/ClixVersion.ts +17 -0
  86. package/src/index.ts +5 -0
  87. package/src/models/ClixDevice.ts +88 -0
  88. package/src/models/ClixPushNotificationPayload.ts +38 -0
  89. package/src/models/ClixUserProperty.ts +58 -0
  90. package/src/services/ClixAPIClient.ts +248 -0
  91. package/src/services/DeviceAPIService.ts +187 -0
  92. package/src/services/DeviceService.ts +204 -0
  93. package/src/services/EventAPIService.ts +48 -0
  94. package/src/services/EventService.ts +45 -0
  95. package/src/services/NotificationService.ts +730 -0
  96. package/src/services/StorageService.ts +84 -0
  97. package/src/services/TokenService.ts +84 -0
  98. package/src/utils/ClixError.ts +78 -0
  99. package/src/utils/UUID.ts +29 -0
  100. package/src/utils/logging/ClixLogger.ts +61 -0
@@ -0,0 +1,730 @@
1
+ import notifee, {
2
+ AndroidImportance,
3
+ AndroidStyle,
4
+ EventType,
5
+ type AndroidChannel,
6
+ type Event,
7
+ } from '@notifee/react-native';
8
+ import messaging, {
9
+ FirebaseMessagingTypes,
10
+ } from '@react-native-firebase/messaging';
11
+ import { Linking, Platform } from 'react-native';
12
+ import type { ClixConfig } from '../core/ClixConfig';
13
+ import { ClixPushNotificationPayload } from '../models/ClixPushNotificationPayload';
14
+ import { ClixLogger } from '../utils/logging/ClixLogger';
15
+ import { UUID } from '../utils/UUID';
16
+ import { ClixAPIClient } from './ClixAPIClient';
17
+ import { DeviceAPIService } from './DeviceAPIService';
18
+ import { DeviceService } from './DeviceService';
19
+ import { EventAPIService } from './EventAPIService';
20
+ import { EventService } from './EventService';
21
+ import { StorageService } from './StorageService';
22
+ import { TokenService } from './TokenService';
23
+
24
+ interface NotificationContent {
25
+ title: string;
26
+ body: string;
27
+ imageUrl?: string;
28
+ }
29
+
30
+ export class NotificationService {
31
+ private static instance: NotificationService | null = null;
32
+
33
+ private static readonly DEFAULT_CHANNEL: AndroidChannel = {
34
+ id: 'clix_default',
35
+ name: 'Clix Notifications',
36
+ description: 'Default notifications from Clix',
37
+ importance: AndroidImportance.HIGH,
38
+ sound: 'default',
39
+ vibration: true,
40
+ lights: true,
41
+ };
42
+
43
+ private messagingService = messaging();
44
+ private isInitialized = false;
45
+ private currentPushToken: string | null = null;
46
+ private processedMessageIds = new Set<string>();
47
+
48
+ private eventService!: EventService;
49
+ private storageService!: StorageService;
50
+ private deviceService?: DeviceService;
51
+ private tokenService?: TokenService;
52
+
53
+ private unsubscribeForegroundMessage?: () => void;
54
+ private unsubscribeNotificationOpened?: () => void;
55
+ private unsubscribeTokenRefresh?: () => void;
56
+ private unsubscribeNotificationEvents?: () => void;
57
+
58
+ private constructor() {}
59
+
60
+ public static getInstance(): NotificationService {
61
+ if (!NotificationService.instance) {
62
+ NotificationService.instance = new NotificationService();
63
+ }
64
+ return NotificationService.instance;
65
+ }
66
+
67
+ public static resetInstance(): void {
68
+ if (NotificationService.instance) {
69
+ NotificationService.instance.cleanup();
70
+ NotificationService.instance = null;
71
+ }
72
+ }
73
+
74
+ async initialize(
75
+ eventService: EventService,
76
+ storageService: StorageService,
77
+ deviceService?: DeviceService,
78
+ tokenService?: TokenService
79
+ ): Promise<NotificationService> {
80
+ if (this.isInitialized) {
81
+ ClixLogger.debug(
82
+ 'Notification service already initialized, returning existing instance'
83
+ );
84
+ return this;
85
+ }
86
+
87
+ try {
88
+ ClixLogger.debug('Initializing notification service');
89
+
90
+ this.eventService = eventService;
91
+ this.storageService = storageService;
92
+ this.deviceService = deviceService;
93
+ this.tokenService = tokenService;
94
+
95
+ await this.initializeNotificationDisplayService();
96
+ await this.initializeMessageService();
97
+
98
+ this.isInitialized = true;
99
+ ClixLogger.debug('Notification service initialized successfully');
100
+ return this;
101
+ } catch (error) {
102
+ ClixLogger.error('Failed to initialize notification service', error);
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ async getCurrentToken(): Promise<string | null> {
108
+ try {
109
+ this.currentPushToken = await this.getOrFetchToken();
110
+ return this.currentPushToken;
111
+ } catch (error) {
112
+ ClixLogger.error('Failed to get push token', error);
113
+ return null;
114
+ }
115
+ }
116
+
117
+ cleanup(): void {
118
+ this.unsubscribeForegroundMessage?.();
119
+ this.unsubscribeNotificationOpened?.();
120
+ this.unsubscribeTokenRefresh?.();
121
+ this.unsubscribeNotificationEvents?.();
122
+ this.isInitialized = false;
123
+ this.processedMessageIds.clear();
124
+ ClixLogger.debug('Notification service cleaned up');
125
+ }
126
+
127
+ 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
+ if (Platform.OS === 'android') {
138
+ await this.createNotificationChannels();
139
+ }
140
+ this.setupNotificationEventListeners();
141
+ }
142
+
143
+ private async createNotificationChannels(): Promise<void> {
144
+ try {
145
+ await notifee.createChannel(NotificationService.DEFAULT_CHANNEL);
146
+ ClixLogger.debug('Notification channels created successfully');
147
+ } catch (error) {
148
+ ClixLogger.error('Failed to create notification channels', error);
149
+ }
150
+ }
151
+
152
+ private setupNotificationEventListeners(): void {
153
+ this.unsubscribeNotificationEvents = notifee.onForegroundEvent(
154
+ async (event: Event) => {
155
+ await this.handleNotificationEvent(event);
156
+ }
157
+ );
158
+ }
159
+
160
+ private async handleNotificationEvent(event: Event): Promise<void> {
161
+ const { type, detail } = event;
162
+
163
+ switch (type) {
164
+ case EventType.PRESS:
165
+ if (detail.notification?.data) {
166
+ await this.handleNotificationTap(detail.notification.data);
167
+ }
168
+ break;
169
+ case EventType.ACTION_PRESS:
170
+ if (detail.pressAction?.id) {
171
+ await this.handleActionPress(
172
+ detail.pressAction.id,
173
+ detail.notification?.data
174
+ );
175
+ }
176
+ break;
177
+ case EventType.DISMISSED:
178
+ ClixLogger.debug('Notification dismissed');
179
+ break;
180
+ default:
181
+ ClixLogger.debug('Unhandled notification event type:', type);
182
+ }
183
+ }
184
+
185
+ private async handleActionPress(
186
+ actionId: string,
187
+ data?: Record<string, any>
188
+ ): Promise<void> {
189
+ ClixLogger.debug('Action pressed:', actionId);
190
+ if (data) {
191
+ await this.handleNotificationTap(data);
192
+ }
193
+ }
194
+
195
+ private async initializeMessageService(): Promise<void> {
196
+ 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
+ }
204
+ }
205
+
206
+ private setupMessageHandlers(): void {
207
+ this.messagingService.setBackgroundMessageHandler(
208
+ async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
209
+ await this.handleBackgroundMessage(remoteMessage);
210
+ }
211
+ );
212
+ this.unsubscribeForegroundMessage = this.messagingService.onMessage(
213
+ async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
214
+ await this.handleForegroundMessage(remoteMessage);
215
+ }
216
+ );
217
+ this.unsubscribeNotificationOpened =
218
+ this.messagingService.onNotificationOpenedApp(
219
+ async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
220
+ await this.handleNotificationTap(remoteMessage.data ?? {});
221
+ }
222
+ );
223
+ this.checkInitialNotification();
224
+ }
225
+
226
+ private async handleBackgroundMessage(
227
+ remoteMessage: FirebaseMessagingTypes.RemoteMessage
228
+ ): Promise<void> {
229
+ try {
230
+ const clixPayload = this.parseClixPayload(remoteMessage.data ?? {});
231
+ if (!clixPayload) {
232
+ ClixLogger.warn('No Clix payload found in background message');
233
+ return;
234
+ }
235
+
236
+ await this.storageService.set('last_background_notification', {
237
+ messageId: remoteMessage.messageId,
238
+ data: remoteMessage.data ?? {},
239
+ timestamp: Date.now(),
240
+ clixMessageId: clixPayload.messageId,
241
+ campaignId: clixPayload.campaignId,
242
+ trackingId: clixPayload.trackingId,
243
+ });
244
+
245
+ await this.trackEventInBackground(clixPayload);
246
+
247
+ if (!remoteMessage.notification) {
248
+ await this.displayNotification(remoteMessage, clixPayload);
249
+ }
250
+ } catch (error) {
251
+ ClixLogger.error('Background message handler error:', error);
252
+ }
253
+ }
254
+
255
+ private async handleForegroundMessage(
256
+ remoteMessage: FirebaseMessagingTypes.RemoteMessage
257
+ ): Promise<void> {
258
+ try {
259
+ const messageId = remoteMessage.messageId;
260
+ if (!messageId) {
261
+ ClixLogger.warn('No messageId found in foreground message');
262
+ return;
263
+ }
264
+
265
+ if (this.processedMessageIds.has(messageId)) {
266
+ ClixLogger.debug(
267
+ 'Message already processed, skipping duplicate:',
268
+ messageId
269
+ );
270
+ return;
271
+ }
272
+
273
+ const clixPayload = this.parseClixPayload(remoteMessage.data ?? {});
274
+ if (clixPayload) {
275
+ ClixLogger.debug('Parsed Clix payload:', clixPayload);
276
+ this.processedMessageIds.add(messageId);
277
+ await this.handlePushReceived(remoteMessage.data ?? {});
278
+ await this.displayNotification(remoteMessage, clixPayload);
279
+ } else {
280
+ ClixLogger.warn('No Clix payload found in foreground message');
281
+ }
282
+ } catch (error) {
283
+ ClixLogger.error('Failed to handle foreground message', error);
284
+ }
285
+ }
286
+
287
+ private async requestMessagePermission(): Promise<any> {
288
+ const settings = await this.messagingService.requestPermission({
289
+ alert: true,
290
+ badge: true,
291
+ sound: true,
292
+ provisional: false,
293
+ announcement: false,
294
+ carPlay: false,
295
+ criticalAlert: false,
296
+ });
297
+ ClixLogger.debug('Push notification permission status:', settings);
298
+ await this.storageService.set(
299
+ 'notification_permission_status',
300
+ settings.toString()
301
+ );
302
+ return settings;
303
+ }
304
+
305
+ private setupTokenRefreshListener(): void {
306
+ this.unsubscribeTokenRefresh = this.messagingService.onTokenRefresh(
307
+ async (token: string) => {
308
+ try {
309
+ ClixLogger.debug('Push token refreshed');
310
+ this.currentPushToken = token;
311
+ await this.saveAndRegisterToken(token);
312
+ } catch (error) {
313
+ ClixLogger.error('Failed to handle token refresh', error);
314
+ }
315
+ }
316
+ );
317
+ }
318
+
319
+ private async checkInitialNotification(): Promise<void> {
320
+ try {
321
+ const initialNotification =
322
+ await this.messagingService.getInitialNotification();
323
+ if (initialNotification) {
324
+ ClixLogger.debug(
325
+ 'App launched from notification:',
326
+ initialNotification.messageId
327
+ );
328
+ await this.handleNotificationTap(initialNotification.data ?? {});
329
+ }
330
+ } catch (error) {
331
+ ClixLogger.error('Failed to handle initial notification', error);
332
+ }
333
+ }
334
+
335
+ private async getAndUpdateToken(): Promise<void> {
336
+ try {
337
+ const token = await this.getCurrentToken();
338
+ if (token) {
339
+ await this.registerTokenWithServer(token);
340
+ }
341
+ } catch (error) {
342
+ ClixLogger.error('Failed to update push token', error);
343
+ }
344
+ }
345
+
346
+ private async getOrFetchToken(): Promise<string | null> {
347
+ if (this.tokenService) {
348
+ const savedToken = await this.tokenService.getCurrentToken();
349
+ if (savedToken) return savedToken;
350
+ }
351
+ const token = await this.messagingService.getToken();
352
+ if (token) {
353
+ ClixLogger.debug('Got push token:', token.substring(0, 20) + '...');
354
+ await this.tokenService?.saveToken(token);
355
+ }
356
+ return token;
357
+ }
358
+
359
+ private async saveAndRegisterToken(token: string): Promise<void> {
360
+ if (this.tokenService) {
361
+ await this.tokenService.saveToken(token);
362
+ ClixLogger.debug('New push token saved via TokenService');
363
+ }
364
+ await this.registerTokenWithServer(token);
365
+ }
366
+
367
+ private async registerTokenWithServer(token: string): Promise<void> {
368
+ if (this.deviceService) {
369
+ await this.deviceService.upsertToken(token);
370
+ ClixLogger.debug('Push token registered with server');
371
+ }
372
+ }
373
+
374
+ private async displayNotification(
375
+ remoteMessage: FirebaseMessagingTypes.RemoteMessage,
376
+ clixPayload: ClixPushNotificationPayload
377
+ ): Promise<void> {
378
+ try {
379
+ const notificationContent = this.extractNotificationContent(
380
+ remoteMessage.notification,
381
+ clixPayload
382
+ );
383
+
384
+ ClixLogger.debug('Creating notification config with content:', {
385
+ title: notificationContent.title,
386
+ body: notificationContent.body,
387
+ hasImage: !!notificationContent.imageUrl,
388
+ imageUrl: notificationContent.imageUrl,
389
+ });
390
+
391
+ const notificationConfig = await this.createNotificationConfig(
392
+ remoteMessage,
393
+ clixPayload,
394
+ notificationContent,
395
+ NotificationService.DEFAULT_CHANNEL.id
396
+ );
397
+
398
+ await notifee.displayNotification(notificationConfig);
399
+ ClixLogger.debug(
400
+ 'Notification displayed successfully:',
401
+ notificationContent.title
402
+ );
403
+ } catch (error) {
404
+ ClixLogger.error('Failed to display notification', error);
405
+ if (error instanceof Error) {
406
+ ClixLogger.error('Error details:', {
407
+ message: error.message,
408
+ stack: error.stack,
409
+ messageId: remoteMessage.messageId,
410
+ hasImage: !!clixPayload.imageUrl,
411
+ imageUrl: clixPayload.imageUrl,
412
+ });
413
+ }
414
+ }
415
+ }
416
+
417
+ private async createNotificationConfig(
418
+ remoteMessage: FirebaseMessagingTypes.RemoteMessage,
419
+ clixPayload: ClixPushNotificationPayload,
420
+ notificationContent: NotificationContent,
421
+ channelId: string
422
+ ) {
423
+ const config: any = {
424
+ id: remoteMessage.messageId || Date.now().toString(),
425
+ title: notificationContent.title,
426
+ body: notificationContent.body,
427
+ data: remoteMessage.data ?? {},
428
+ android: {
429
+ channelId,
430
+ pressAction: {
431
+ id: 'default',
432
+ },
433
+ actions: this.createNotificationActions(clixPayload),
434
+ },
435
+ ios: {
436
+ foregroundPresentationOptions: {
437
+ badge: true,
438
+ sound: true,
439
+ banner: true,
440
+ list: true,
441
+ },
442
+ },
443
+ };
444
+
445
+ if (notificationContent.imageUrl) {
446
+ if (this.isValidImageUrl(notificationContent.imageUrl)) {
447
+ ClixLogger.debug(
448
+ 'Adding image attachment to notification:',
449
+ notificationContent.imageUrl
450
+ );
451
+ if (Platform.OS === 'ios') {
452
+ try {
453
+ config.ios.attachments = [
454
+ {
455
+ url: notificationContent.imageUrl,
456
+ },
457
+ ];
458
+ } catch (error) {
459
+ ClixLogger.warn('Failed to download image attachment:', error);
460
+ }
461
+ } else {
462
+ config.android.style = {
463
+ type: AndroidStyle.BIGPICTURE,
464
+ picture: notificationContent.imageUrl,
465
+ };
466
+ }
467
+ } else {
468
+ ClixLogger.warn(
469
+ 'Skipping attachment due to invalid URL:',
470
+ notificationContent.imageUrl
471
+ );
472
+ }
473
+ }
474
+ return config;
475
+ }
476
+
477
+ private createNotificationActions(clixPayload: ClixPushNotificationPayload) {
478
+ const actions = [];
479
+
480
+ actions.push({
481
+ title: 'Open',
482
+ pressAction: {
483
+ id: 'default',
484
+ },
485
+ });
486
+
487
+ if (clixPayload.customProperties?.actions) {
488
+ const customActions = clixPayload.customProperties.actions;
489
+ if (Array.isArray(customActions)) {
490
+ customActions.forEach((action) => {
491
+ if (action.title && action.actionId) {
492
+ actions.push({
493
+ title: action.title,
494
+ pressAction: {
495
+ id: action.actionId,
496
+ },
497
+ });
498
+ }
499
+ });
500
+ }
501
+ }
502
+
503
+ return actions;
504
+ }
505
+
506
+ private isValidImageUrl(url: string): boolean {
507
+ return url.trim() !== '' && url.startsWith('http');
508
+ }
509
+
510
+ private extractNotificationContent(
511
+ fcmNotification: FirebaseMessagingTypes.Notification | null | undefined,
512
+ clixPayload: ClixPushNotificationPayload
513
+ ): NotificationContent {
514
+ ClixLogger.debug('Extracting notification content from payload:', {
515
+ fcmTitle: fcmNotification?.title,
516
+ fcmBody: fcmNotification?.body,
517
+ customProperties: clixPayload.customProperties,
518
+ messageId: clixPayload.messageId,
519
+ imageUrl: clixPayload.imageUrl,
520
+ });
521
+
522
+ const title =
523
+ fcmNotification?.title ||
524
+ clixPayload.customProperties?.title ||
525
+ 'New Message';
526
+ const body =
527
+ fcmNotification?.body || clixPayload.customProperties?.body || '';
528
+
529
+ ClixLogger.debug('Extracted notification content:', { title, body });
530
+
531
+ // Validate imageUrl before using it
532
+ let imageUrl: string | undefined;
533
+ if (clixPayload.imageUrl) {
534
+ ClixLogger.debug('Processing image URL:', clixPayload.imageUrl);
535
+ if (this.isValidImageUrl(clixPayload.imageUrl)) {
536
+ imageUrl = clixPayload.imageUrl;
537
+ ClixLogger.debug('Image URL validated successfully');
538
+ } else {
539
+ ClixLogger.warn('Invalid image URL, skipping:', clixPayload.imageUrl);
540
+ }
541
+ }
542
+
543
+ return {
544
+ title,
545
+ body,
546
+ imageUrl,
547
+ };
548
+ }
549
+
550
+ private async handlePushReceived(data: Record<string, any>): Promise<void> {
551
+ try {
552
+ const clixPayload = this.parseClixPayload(data);
553
+ if (clixPayload) {
554
+ await this.trackPushEvent('PUSH_NOTIFICATION_RECEIVED', clixPayload);
555
+ }
556
+ ClixLogger.debug('Push notification received and processed');
557
+ } catch (error) {
558
+ ClixLogger.error('Failed to handle push received', error);
559
+ }
560
+ }
561
+
562
+ private async handlePushTapped(data: Record<string, any>): Promise<void> {
563
+ try {
564
+ const clixPayload = this.parseClixPayload(data);
565
+ if (clixPayload) {
566
+ await this.trackPushEvent('PUSH_NOTIFICATION_TAPPED', clixPayload);
567
+ }
568
+ await this.handleUrlNavigation(data);
569
+ ClixLogger.debug('Push notification tapped and processed');
570
+ } catch (error) {
571
+ ClixLogger.error('Failed to handle push tapped', error);
572
+ }
573
+ }
574
+
575
+ private async handleNotificationTap(
576
+ data: Record<string, any>
577
+ ): Promise<void> {
578
+ try {
579
+ await this.handlePushTapped(data);
580
+ } catch (error) {
581
+ ClixLogger.error('Failed to handle notification tap', error);
582
+ }
583
+ }
584
+
585
+ private async handleUrlNavigation(data: Record<string, any>): Promise<void> {
586
+ try {
587
+ let url: string | undefined;
588
+ const clixPayload = this.parseClixPayload(data);
589
+ if (clixPayload) {
590
+ url = clixPayload.landingUrl;
591
+ }
592
+ url =
593
+ url || data.landing_url || data.url || data.link || data.click_action;
594
+ if (url) {
595
+ ClixLogger.debug('Opening URL from notification:', url);
596
+ try {
597
+ const supported = await Linking.canOpenURL(url);
598
+ ClixLogger.debug('Can open URL:', supported);
599
+ if (supported) {
600
+ await Linking.openURL(url);
601
+ ClixLogger.debug('URL opened successfully:', url);
602
+ } else {
603
+ ClixLogger.warn('Cannot open URL:', url);
604
+ await Linking.openURL(url);
605
+ }
606
+ } catch (error) {
607
+ ClixLogger.error('Error opening URL:', error);
608
+ }
609
+ }
610
+ } catch (error) {
611
+ ClixLogger.error('Failed to handle URL navigation', error);
612
+ }
613
+ }
614
+
615
+ private parseClixPayload(
616
+ userInfo: Record<string, any>
617
+ ): ClixPushNotificationPayload | null {
618
+ try {
619
+ let payload: any = userInfo;
620
+ if (userInfo.clix) {
621
+ if (typeof userInfo.clix === 'object') {
622
+ payload = userInfo.clix;
623
+ } else if (typeof userInfo.clix === 'string') {
624
+ payload = JSON.parse(userInfo.clix);
625
+ }
626
+ }
627
+ const toCamel = (s: string) =>
628
+ s.replace(/_([a-z])/g, (g) => (g[1] ?? '').toUpperCase());
629
+ const result: any = {};
630
+ if (!payload) return null;
631
+ for (const key in payload) {
632
+ result[toCamel(key)] = payload[key];
633
+ }
634
+
635
+ ClixLogger.debug('Parsed Clix payload result:', result);
636
+
637
+ if (!result.messageId) return null;
638
+
639
+ // Extract title and body into customProperties if they exist
640
+ const customProperties: Record<string, any> = {};
641
+ if (result.title) customProperties.title = result.title;
642
+ if (result.body) customProperties.body = result.body;
643
+
644
+ const finalResult = {
645
+ ...result,
646
+ customProperties:
647
+ Object.keys(customProperties).length > 0
648
+ ? customProperties
649
+ : undefined,
650
+ };
651
+
652
+ ClixLogger.debug('Final Clix payload result:', finalResult);
653
+
654
+ return new ClixPushNotificationPayload(finalResult);
655
+ } catch (error) {
656
+ ClixLogger.error('Failed to parse Clix payload', error);
657
+ return null;
658
+ }
659
+ }
660
+
661
+ private async trackPushEvent(
662
+ eventType: string,
663
+ clixPayload: ClixPushNotificationPayload
664
+ ): Promise<void> {
665
+ const properties = this.extractTrackingProperties(clixPayload);
666
+ const messageId = clixPayload.messageId;
667
+ await this.eventService.trackEvent(eventType, properties, messageId);
668
+ ClixLogger.debug(`${eventType} tracked:`, messageId);
669
+ }
670
+
671
+ private extractTrackingProperties(
672
+ clixPayload: ClixPushNotificationPayload
673
+ ): Record<string, any> {
674
+ const properties: Record<string, any> = {};
675
+ if (clixPayload.messageId) properties.messageId = clixPayload.messageId;
676
+ if (clixPayload.campaignId) properties.campaignId = clixPayload.campaignId;
677
+ if (clixPayload.trackingId) properties.trackingId = clixPayload.trackingId;
678
+ return properties;
679
+ }
680
+
681
+ private async trackEventInBackground(
682
+ clixPayload: ClixPushNotificationPayload
683
+ ): Promise<void> {
684
+ const messageId = clixPayload.messageId;
685
+ if (!messageId) {
686
+ ClixLogger.warn('No messageId found in payload, skipping event tracking');
687
+ return;
688
+ }
689
+ try {
690
+ const storageService = new StorageService();
691
+ const configData = await storageService.get<Record<string, any>>(
692
+ 'clix_config'
693
+ );
694
+ if (!configData) {
695
+ ClixLogger.error('No Clix config found in storage');
696
+ return;
697
+ }
698
+ let deviceId = await storageService.get<string>('clix_device_id');
699
+ if (!deviceId) {
700
+ ClixLogger.warn(
701
+ 'No device ID found in storage, generating new device ID'
702
+ );
703
+ deviceId = UUID.generate();
704
+ await storageService.set('clix_device_id', deviceId);
705
+ }
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);
717
+ const properties = this.extractTrackingProperties(clixPayload);
718
+ await eventService.trackEvent(
719
+ 'PUSH_NOTIFICATION_RECEIVED',
720
+ properties,
721
+ messageId
722
+ );
723
+ ClixLogger.debug(
724
+ 'PUSH_NOTIFICATION_RECEIVED event tracked in background'
725
+ );
726
+ } catch (error) {
727
+ ClixLogger.error('Error tracking event in background', error);
728
+ }
729
+ }
730
+ }