@clix-so/react-native-sdk 1.0.0 → 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.
Files changed (88) hide show
  1. package/lib/module/core/Clix.js +40 -95
  2. package/lib/module/core/Clix.js.map +1 -1
  3. package/lib/module/core/ClixInitCoordinator.js +3 -14
  4. package/lib/module/core/ClixInitCoordinator.js.map +1 -1
  5. package/lib/module/core/ClixNotification.js +25 -28
  6. package/lib/module/core/ClixNotification.js.map +1 -1
  7. package/lib/module/models/ClixDevice.js +0 -6
  8. package/lib/module/models/ClixDevice.js.map +1 -1
  9. package/lib/module/models/ClixPushNotificationPayload.js +0 -19
  10. package/lib/module/models/ClixPushNotificationPayload.js.map +1 -1
  11. package/lib/module/services/ClixAPIClient.js +50 -99
  12. package/lib/module/services/ClixAPIClient.js.map +1 -1
  13. package/lib/module/services/DeviceAPIService.js +37 -45
  14. package/lib/module/services/DeviceAPIService.js.map +1 -1
  15. package/lib/module/services/DeviceService.js +97 -116
  16. package/lib/module/services/DeviceService.js.map +1 -1
  17. package/lib/module/services/EventAPIService.js +3 -5
  18. package/lib/module/services/EventAPIService.js.map +1 -1
  19. package/lib/module/services/EventService.js +13 -20
  20. package/lib/module/services/EventService.js.map +1 -1
  21. package/lib/module/services/NotificationService.js +252 -402
  22. package/lib/module/services/NotificationService.js.map +1 -1
  23. package/lib/module/services/TokenService.js +3 -59
  24. package/lib/module/services/TokenService.js.map +1 -1
  25. package/lib/module/utils/http/HTTPClient.js +101 -0
  26. package/lib/module/utils/http/HTTPClient.js.map +1 -0
  27. package/lib/module/utils/http/HTTPMethod.js +10 -0
  28. package/lib/module/utils/http/HTTPMethod.js.map +1 -0
  29. package/lib/module/utils/http/HTTPRequest.js +4 -0
  30. package/lib/module/utils/http/HTTPRequest.js.map +1 -0
  31. package/lib/module/utils/http/HTTPResponse.js +2 -0
  32. package/lib/module/utils/http/HTTPResponse.js.map +1 -0
  33. package/lib/module/utils/types.js +2 -0
  34. package/lib/module/utils/types.js.map +1 -0
  35. package/lib/typescript/src/core/Clix.d.ts +13 -15
  36. package/lib/typescript/src/core/Clix.d.ts.map +1 -1
  37. package/lib/typescript/src/core/ClixConfig.d.ts +3 -3
  38. package/lib/typescript/src/core/ClixConfig.d.ts.map +1 -1
  39. package/lib/typescript/src/core/ClixInitCoordinator.d.ts +0 -3
  40. package/lib/typescript/src/core/ClixInitCoordinator.d.ts.map +1 -1
  41. package/lib/typescript/src/core/ClixNotification.d.ts +6 -5
  42. package/lib/typescript/src/core/ClixNotification.d.ts.map +1 -1
  43. package/lib/typescript/src/models/ClixDevice.d.ts +0 -2
  44. package/lib/typescript/src/models/ClixDevice.d.ts.map +1 -1
  45. package/lib/typescript/src/models/ClixPushNotificationPayload.d.ts +8 -21
  46. package/lib/typescript/src/models/ClixPushNotificationPayload.d.ts.map +1 -1
  47. package/lib/typescript/src/services/ClixAPIClient.d.ts +6 -22
  48. package/lib/typescript/src/services/ClixAPIClient.d.ts.map +1 -1
  49. package/lib/typescript/src/services/DeviceAPIService.d.ts +1 -1
  50. package/lib/typescript/src/services/DeviceAPIService.d.ts.map +1 -1
  51. package/lib/typescript/src/services/DeviceService.d.ts +10 -5
  52. package/lib/typescript/src/services/DeviceService.d.ts.map +1 -1
  53. package/lib/typescript/src/services/EventAPIService.d.ts.map +1 -1
  54. package/lib/typescript/src/services/EventService.d.ts +1 -0
  55. package/lib/typescript/src/services/EventService.d.ts.map +1 -1
  56. package/lib/typescript/src/services/NotificationService.d.ts +50 -57
  57. package/lib/typescript/src/services/NotificationService.d.ts.map +1 -1
  58. package/lib/typescript/src/services/TokenService.d.ts +1 -7
  59. package/lib/typescript/src/services/TokenService.d.ts.map +1 -1
  60. package/lib/typescript/src/utils/http/HTTPClient.d.ts +15 -0
  61. package/lib/typescript/src/utils/http/HTTPClient.d.ts.map +1 -0
  62. package/lib/typescript/src/utils/http/HTTPMethod.d.ts +7 -0
  63. package/lib/typescript/src/utils/http/HTTPMethod.d.ts.map +1 -0
  64. package/lib/typescript/src/utils/http/HTTPRequest.d.ts +9 -0
  65. package/lib/typescript/src/utils/http/HTTPRequest.d.ts.map +1 -0
  66. package/lib/typescript/src/utils/http/HTTPResponse.d.ts +6 -0
  67. package/lib/typescript/src/utils/http/HTTPResponse.d.ts.map +1 -0
  68. package/lib/typescript/src/utils/types.d.ts +5 -0
  69. package/lib/typescript/src/utils/types.d.ts.map +1 -0
  70. package/package.json +1 -1
  71. package/src/core/Clix.ts +62 -115
  72. package/src/core/ClixConfig.ts +3 -3
  73. package/src/core/ClixInitCoordinator.ts +5 -17
  74. package/src/core/ClixNotification.ts +36 -37
  75. package/src/models/ClixDevice.ts +17 -25
  76. package/src/models/ClixPushNotificationPayload.ts +8 -37
  77. package/src/services/ClixAPIClient.ts +84 -144
  78. package/src/services/DeviceAPIService.ts +39 -47
  79. package/src/services/DeviceService.ts +122 -156
  80. package/src/services/EventAPIService.ts +3 -5
  81. package/src/services/EventService.ts +26 -33
  82. package/src/services/NotificationService.ts +318 -533
  83. package/src/services/TokenService.ts +4 -71
  84. package/src/utils/http/HTTPClient.ts +141 -0
  85. package/src/utils/http/HTTPMethod.ts +6 -0
  86. package/src/utils/http/HTTPRequest.ts +9 -0
  87. package/src/utils/http/HTTPResponse.ts +5 -0
  88. package/src/utils/types.ts +7 -0
@@ -15,35 +15,40 @@ import messaging, {
15
15
  FirebaseMessagingTypes,
16
16
  } from '@react-native-firebase/messaging';
17
17
  import { Linking, Platform } from 'react-native';
18
- import { ClixPushNotificationPayload } from '../models/ClixPushNotificationPayload';
18
+ import type { ClixPushNotificationPayload } from '../models/ClixPushNotificationPayload';
19
19
  import { ClixLogger } from '../utils/logging/ClixLogger';
20
- import { UUID } from '../utils/UUID';
21
20
  import { DeviceService } from './DeviceService';
22
21
  import { EventService } from './EventService';
23
- import { StorageService } from './StorageService';
24
22
  import { TokenService } from './TokenService';
25
23
 
26
- interface NotificationContent {
27
- title: string;
28
- body: string;
29
- imageUrl?: string;
30
- }
31
-
32
24
  type NotificationData = Record<string, any>;
33
25
 
34
- export type ForegroundMessageHandler = (
26
+ export type MessageHandler = (
35
27
  data: NotificationData
36
28
  ) => Promise<boolean> | boolean;
37
29
  export type BackgroundMessageHandler = (
38
30
  data: NotificationData
39
31
  ) => Promise<void> | void;
40
- export type NotificationOpenedHandler = (
32
+ export type NotificationOpenedAppHandler = (
41
33
  data: NotificationData
42
34
  ) => Promise<void> | void;
43
- export type FcmTokenErrorHandler = (error: Error) => Promise<void> | void;
35
+ export type TokenRefreshHandler = (token: string) => Promise<void> | void;
36
+ export type ForegroundEventHandler = (event: Event) => Promise<void> | void;
44
37
 
45
38
  export class NotificationService {
46
- private static instance: NotificationService | null = null;
39
+ autoHandleLandingUrl = true;
40
+ messageHandler?: MessageHandler;
41
+ backgroundMessageHandler?: BackgroundMessageHandler;
42
+ notificationOpenedAppHandler?: NotificationOpenedAppHandler;
43
+ tokenRefreshHandler?: TokenRefreshHandler;
44
+ foregroundEventHandler?: ForegroundEventHandler;
45
+
46
+ private isInitialized = false;
47
+ private processedMessageIds = new Set<string>();
48
+ private unsubscribeMessage?: () => void;
49
+ private unsubscribeNotificationOpenedApp?: () => void;
50
+ private unsubscribeTokenRefresh?: () => void;
51
+ private unsubscribeForegroundEvent?: () => void;
47
52
 
48
53
  private static readonly DEFAULT_CHANNEL: AndroidChannel = {
49
54
  id: 'clix_channel',
@@ -56,209 +61,154 @@ export class NotificationService {
56
61
  };
57
62
  private static readonly ANDROID_GROUP_ID = 'clix_notification_group';
58
63
 
59
- private messagingService = messaging();
60
- private isInitialized = false;
61
- private currentPushToken: string | null = null;
62
- private processedMessageIds = new Set<string>();
63
-
64
- private eventService!: EventService;
65
- private storageService!: StorageService;
66
- private deviceService?: DeviceService;
67
- private tokenService?: TokenService;
64
+ constructor(
65
+ private readonly deviceService: DeviceService,
66
+ private readonly tokenService: TokenService,
67
+ private readonly eventService: EventService
68
+ ) {}
68
69
 
69
- private autoHandleLandingUrl = true;
70
- private messageHandler?: ForegroundMessageHandler;
71
- private backgroundMessageHandler?: BackgroundMessageHandler;
72
- private openedHandler?: NotificationOpenedHandler;
73
- private fcmTokenErrorHandler?: FcmTokenErrorHandler;
74
-
75
- private unsubscribeForegroundMessage?: () => void;
76
- private unsubscribeNotificationOpened?: () => void;
77
- private unsubscribeTokenRefresh?: () => void;
78
- private unsubscribeNotificationEvents?: () => void;
79
-
80
- private constructor() {}
81
-
82
- public static getInstance(): NotificationService {
83
- if (!NotificationService.instance) {
84
- NotificationService.instance = new NotificationService();
85
- }
86
- return NotificationService.instance;
87
- }
88
-
89
- public static resetInstance(): void {
90
- if (NotificationService.instance) {
91
- NotificationService.instance.cleanup();
92
- NotificationService.instance = null;
93
- }
94
- }
95
-
96
- async initialize(
97
- eventService: EventService,
98
- storageService: StorageService,
99
- deviceService?: DeviceService,
100
- tokenService?: TokenService
101
- ): Promise<NotificationService> {
70
+ async initialize(): Promise<void> {
102
71
  if (this.isInitialized) {
103
- ClixLogger.debug(
104
- 'Notification service already initialized, returning existing instance'
105
- );
106
- return this;
72
+ ClixLogger.debug('Notification service already initialized');
73
+ return;
107
74
  }
108
75
 
109
76
  try {
110
- ClixLogger.debug('Initializing notification service');
77
+ ClixLogger.debug('Initializing notification service...');
111
78
 
112
- this.eventService = eventService;
113
- this.storageService = storageService;
114
- this.deviceService = deviceService;
115
- this.tokenService = tokenService;
79
+ this.setupTokenRefreshListener();
80
+ this.setupPushReceivedHandler(); // NOTE(nyanxyz): must be set up before any await calls
81
+ await this.setupPushTappedHandler();
116
82
 
117
- await this.initializeMessageService();
118
- await this.initializeNotificationDisplayService();
83
+ if (Platform.OS === 'android') {
84
+ await this.createNotificationChannels();
85
+ }
119
86
 
120
87
  this.isInitialized = true;
121
88
  ClixLogger.debug('Notification service initialized successfully');
122
- return this;
123
89
  } catch (error) {
124
90
  ClixLogger.error('Failed to initialize notification service', error);
125
91
  throw error;
126
92
  }
127
93
  }
128
94
 
129
- async getCurrentToken(): Promise<string | null> {
130
- try {
131
- this.currentPushToken = await this.getOrFetchToken();
132
- return this.currentPushToken;
133
- } catch (error) {
134
- ClixLogger.error('Failed to get push token', error);
135
- await this.handleFcmTokenError(error);
136
- return null;
137
- }
138
- }
139
-
140
95
  cleanup(): void {
141
- this.unsubscribeForegroundMessage?.();
142
- this.unsubscribeNotificationOpened?.();
96
+ this.unsubscribeMessage?.();
97
+ this.unsubscribeNotificationOpenedApp?.();
143
98
  this.unsubscribeTokenRefresh?.();
144
- this.unsubscribeNotificationEvents?.();
99
+ this.unsubscribeForegroundEvent?.();
145
100
  this.isInitialized = false;
146
101
  this.processedMessageIds.clear();
147
102
  ClixLogger.debug('Notification service cleaned up');
148
103
  }
149
104
 
150
- setMessageHandler(handler?: ForegroundMessageHandler): void {
151
- this.messageHandler = handler;
152
- }
153
-
154
- setBackgroundMessageHandler(handler?: BackgroundMessageHandler): void {
155
- this.backgroundMessageHandler = handler;
156
- }
157
-
158
- setNotificationOpenedHandler(handler?: NotificationOpenedHandler): void {
159
- this.openedHandler = handler;
160
- }
161
-
162
- setFcmTokenErrorHandler(handler?: FcmTokenErrorHandler): void {
163
- this.fcmTokenErrorHandler = handler;
164
- }
105
+ async requestPermission(): Promise<NotificationSettings> {
106
+ const settings = await notifee.requestPermission({
107
+ alert: true,
108
+ badge: true,
109
+ sound: true,
110
+ provisional: false,
111
+ announcement: false,
112
+ carPlay: false,
113
+ criticalAlert: false,
114
+ });
115
+ ClixLogger.debug('Push notification permission status:', settings);
165
116
 
166
- setAutoHandleLandingUrl(enable: boolean): void {
167
- this.autoHandleLandingUrl = enable;
168
- }
117
+ const isGranted =
118
+ settings.authorizationStatus === AuthorizationStatus.AUTHORIZED ||
119
+ settings.authorizationStatus === AuthorizationStatus.PROVISIONAL;
169
120
 
170
- private async initializeNotificationDisplayService(): Promise<void> {
171
- if (Platform.OS === 'android') {
172
- await this.createNotificationChannels();
173
- }
174
- this.setupNotificationEventListeners();
121
+ await this.setPermissionGranted(isGranted);
122
+ return settings;
175
123
  }
176
124
 
177
- private async createNotificationChannels(): Promise<void> {
125
+ async setPermissionGranted(isGranted: boolean): Promise<void> {
178
126
  try {
179
- await notifee.createChannel(NotificationService.DEFAULT_CHANNEL);
180
- ClixLogger.debug('Notification channels created successfully');
127
+ await this.deviceService.updatePushPermission(isGranted);
128
+ ClixLogger.debug(
129
+ `Push permission status reported to server: ${
130
+ isGranted ? 'granted' : 'denied'
131
+ }`
132
+ );
181
133
  } catch (error) {
182
- ClixLogger.error('Failed to create notification channels', error);
134
+ ClixLogger.warn('Failed to upsert push permission status', error);
183
135
  }
184
136
  }
185
137
 
186
- private setupNotificationEventListeners(): void {
187
- this.unsubscribeNotificationEvents = notifee.onForegroundEvent(
188
- async (event: Event) => {
189
- await this.handleNotificationEvent(event);
190
- }
138
+ private setupPushReceivedHandler(): void {
139
+ /**
140
+ * Android: background message handler
141
+ */
142
+ messaging().setBackgroundMessageHandler(
143
+ this.handleBackgroundMessage.bind(this)
191
144
  );
192
- notifee.onBackgroundEvent(async (event: Event) => {
193
- await this.handleNotificationEvent(event);
194
- });
145
+ /**
146
+ * iOS & Android: foreground message handler
147
+ */
148
+ this.unsubscribeMessage = messaging().onMessage(
149
+ this.handleForegroundMessage.bind(this)
150
+ );
151
+ /**
152
+ * iOS: background messages are handled in the Notification Service Extension
153
+ */
154
+ }
155
+
156
+ private async setupPushTappedHandler(): Promise<void> {
157
+ /**
158
+ * Android: background notification tap handler
159
+ * & app launched from quit state via a notification
160
+ */
161
+ notifee.onBackgroundEvent(this.handleNotificationEvent.bind(this));
162
+ /**
163
+ * iOS & Android: foreground notification tap handler
164
+ */
165
+ this.unsubscribeForegroundEvent = notifee.onForegroundEvent(
166
+ this.handleForegroundNotificationEvent.bind(this)
167
+ );
168
+ /**
169
+ * iOS: background notification tap handler
170
+ */
171
+ this.unsubscribeNotificationOpenedApp = messaging().onNotificationOpenedApp(
172
+ this.handleNotificationOpenedApp.bind(this)
173
+ );
174
+ /**
175
+ * iOS: app launched from a quit state via a notification
176
+ */
177
+ await this.handleInitialNotification();
195
178
  }
196
179
 
197
- private async handleNotificationEvent(event: Event): Promise<void> {
198
- const { type, detail } = event;
199
-
200
- switch (type) {
201
- case EventType.PRESS:
202
- if (detail.notification?.data) {
203
- await this.handleNotificationTap(detail.notification.data);
180
+ private setupTokenRefreshListener(): void {
181
+ this.unsubscribeTokenRefresh = messaging().onTokenRefresh(
182
+ async (token: string) => {
183
+ try {
184
+ await this.tokenRefreshHandler?.(token);
185
+ } catch (error) {
186
+ ClixLogger.error('Token refresh handler failed', error);
204
187
  }
205
- break;
206
- case EventType.ACTION_PRESS:
207
- if (detail.pressAction?.id) {
208
- await this.handleActionPress(
209
- detail.pressAction.id,
210
- detail.notification?.data
211
- );
188
+
189
+ try {
190
+ ClixLogger.debug(`Push token refreshed: ${token}`);
191
+ this.tokenService.saveToken(token);
192
+ await this.deviceService.updatePushToken(token, 'FCM');
193
+ } catch (error) {
194
+ ClixLogger.error('Failed to handle token refresh', error);
212
195
  }
213
- break;
214
- case EventType.DISMISSED:
215
- ClixLogger.debug('Notification dismissed');
216
- break;
217
- default:
218
- ClixLogger.debug('Unhandled notification event type:', type);
219
- }
196
+ }
197
+ );
220
198
  }
221
199
 
222
- private async handleActionPress(
223
- actionId: string,
224
- data?: Record<string, any>
225
- ): Promise<void> {
226
- ClixLogger.debug('Action pressed:', actionId);
227
- if (data) {
228
- await this.handleNotificationTap(data);
200
+ private async createNotificationChannels(): Promise<void> {
201
+ try {
202
+ await notifee.createChannel(NotificationService.DEFAULT_CHANNEL);
203
+ ClixLogger.debug('Notification channels created successfully');
204
+ } catch (error) {
205
+ ClixLogger.error('Failed to create notification channels', error);
229
206
  }
230
207
  }
231
208
 
232
- private async initializeMessageService(): Promise<void> {
233
- this.setupMessageHandlers();
234
- await this.getAndUpdateToken();
235
- this.setupTokenRefreshListener();
236
- }
237
-
238
- private setupMessageHandlers(): void {
239
- this.messagingService.setBackgroundMessageHandler(
240
- async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
241
- await this.handleBackgroundMessage(remoteMessage);
242
- }
243
- );
244
- this.unsubscribeForegroundMessage = this.messagingService.onMessage(
245
- async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
246
- await this.handleForegroundMessage(remoteMessage);
247
- }
248
- );
249
- this.unsubscribeNotificationOpened =
250
- this.messagingService.onNotificationOpenedApp(
251
- async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
252
- ClixLogger.debug('Notification opened from background state:', {
253
- messageId: remoteMessage.messageId,
254
- data: remoteMessage.data,
255
- });
256
- await this.handleNotificationTap(remoteMessage.data ?? {});
257
- }
258
- );
259
- this.checkInitialNotification();
260
- }
261
-
209
+ /**
210
+ * Android: background message handler
211
+ */
262
212
  private async handleBackgroundMessage(
263
213
  remoteMessage: FirebaseMessagingTypes.RemoteMessage
264
214
  ): Promise<void> {
@@ -278,16 +228,7 @@ export class NotificationService {
278
228
  return;
279
229
  }
280
230
 
281
- this.storageService.set('last_background_notification', {
282
- messageId: remoteMessage.messageId,
283
- data: data,
284
- timestamp: Date.now(),
285
- clixMessageId: clixPayload.messageId,
286
- campaignId: clixPayload.campaignId,
287
- trackingId: clixPayload.trackingId,
288
- });
289
-
290
- await this.trackEventInBackground(clixPayload);
231
+ await this.trackPushReceivedEvent(clixPayload);
291
232
 
292
233
  if (!remoteMessage.notification) {
293
234
  await this.displayNotification(remoteMessage, clixPayload);
@@ -297,161 +238,202 @@ export class NotificationService {
297
238
  }
298
239
  }
299
240
 
241
+ /**
242
+ * iOS & Android: foreground message handler
243
+ */
300
244
  private async handleForegroundMessage(
301
245
  remoteMessage: FirebaseMessagingTypes.RemoteMessage
302
246
  ): Promise<void> {
303
247
  ClixLogger.debug('Handling foreground message:', remoteMessage.messageId);
304
248
 
305
- try {
306
- const messageId = remoteMessage.messageId;
307
- if (!messageId) {
308
- ClixLogger.warn('No messageId found in foreground message');
309
- return;
310
- }
249
+ const messageId = remoteMessage.messageId;
250
+ if (!messageId) {
251
+ ClixLogger.warn('No messageId found in foreground message');
252
+ return;
253
+ }
254
+
255
+ if (this.processedMessageIds.has(messageId)) {
256
+ ClixLogger.debug(
257
+ 'Message already processed, skipping duplicate:',
258
+ messageId
259
+ );
260
+ return;
261
+ }
311
262
 
312
- if (this.processedMessageIds.has(messageId)) {
263
+ const data = remoteMessage.data ?? {};
264
+ try {
265
+ const result = await this.messageHandler?.(data);
266
+ if (result === false) {
313
267
  ClixLogger.debug(
314
- 'Message already processed, skipping duplicate:',
315
- messageId
268
+ 'Foreground message suppressed by user handler:',
269
+ remoteMessage.messageId
316
270
  );
317
271
  return;
318
272
  }
273
+ } catch (error) {
274
+ ClixLogger.error('Foreground message handler failed', error);
275
+ }
319
276
 
320
- const data = remoteMessage.data ?? {};
277
+ try {
321
278
  const clixPayload = this.parseClixPayload(data);
322
- if (clixPayload) {
323
- ClixLogger.debug('Parsed Clix payload:', clixPayload);
324
- this.processedMessageIds.add(messageId);
325
- if (Platform.OS === 'android') {
326
- // NOTE(nyanxyz): on iOS, Received event is tracked in NSE
327
- await this.handlePushReceived(data);
328
- }
279
+ if (!clixPayload) {
280
+ ClixLogger.warn('No Clix payload found in background message');
281
+ return;
282
+ }
329
283
 
330
- if (await this.shouldDisplayForegroundNotification(data)) {
331
- await this.displayNotification(remoteMessage, clixPayload);
332
- } else {
333
- ClixLogger.debug(
334
- 'Foreground message suppressed by user handler:',
335
- messageId
336
- );
337
- }
338
- } else {
339
- ClixLogger.warn('No Clix payload found in foreground message');
284
+ this.processedMessageIds.add(messageId);
285
+
286
+ if (Platform.OS === 'android') {
287
+ // NOTE(nyanxyz): on iOS, Received event is tracked in Notification Service Extension
288
+ await this.trackPushReceivedEvent(clixPayload);
340
289
  }
290
+
291
+ await this.displayNotification(remoteMessage, clixPayload);
341
292
  } catch (error) {
342
293
  ClixLogger.error('Failed to handle foreground message', error);
343
294
  }
344
295
  }
345
296
 
346
- async requestPermission(): Promise<NotificationSettings> {
347
- const settings = await notifee.requestPermission({
348
- alert: true,
349
- badge: true,
350
- sound: true,
351
- provisional: false,
352
- announcement: false,
353
- carPlay: false,
354
- criticalAlert: false,
297
+ /**
298
+ * iOS: background notification tap handler
299
+ */
300
+ private async handleNotificationOpenedApp(
301
+ remoteMessage: FirebaseMessagingTypes.RemoteMessage
302
+ ): Promise<void> {
303
+ ClixLogger.debug('Handling notification opened from background:', {
304
+ messageId: remoteMessage.messageId,
305
+ data: remoteMessage.data,
355
306
  });
356
- ClixLogger.debug('Push notification permission status:', settings);
357
-
358
- const isGranted =
359
- settings.authorizationStatus === AuthorizationStatus.AUTHORIZED ||
360
- settings.authorizationStatus === AuthorizationStatus.PROVISIONAL;
361
- await this.setPermissionGranted(isGranted);
362
-
363
- return settings;
364
- }
365
-
366
- async setPermissionGranted(isGranted: boolean): Promise<void> {
367
- if (!this.deviceService) {
368
- ClixLogger.debug(
369
- 'Device service is not initialized, skipping push permission upsert'
370
- );
371
- return;
372
- }
373
307
 
308
+ const data = remoteMessage.data ?? {};
374
309
  try {
375
- await this.deviceService.upsertIsPushPermissionGranted(isGranted);
376
- ClixLogger.debug(
377
- `Push permission status reported to server: ${
378
- isGranted ? 'granted' : 'denied'
379
- }`
380
- );
310
+ await this.notificationOpenedAppHandler?.(data);
381
311
  } catch (error) {
382
- ClixLogger.warn('Failed to upsert push permission status', error);
312
+ ClixLogger.error('Notification opened app handler failed', error);
383
313
  }
384
- }
385
314
 
386
- private setupTokenRefreshListener(): void {
387
- this.unsubscribeTokenRefresh = this.messagingService.onTokenRefresh(
388
- async (token: string) => {
389
- try {
390
- ClixLogger.debug('Push token refreshed');
391
- this.currentPushToken = token;
392
- await this.saveAndRegisterToken(token);
393
- } catch (error) {
394
- ClixLogger.error('Failed to handle token refresh', error);
395
- await this.handleFcmTokenError(error);
396
- }
315
+ try {
316
+ const clixPayload = this.parseClixPayload(data);
317
+ if (clixPayload) {
318
+ await this.trackPushTappedEvent(clixPayload);
397
319
  }
398
- );
320
+ if (this.autoHandleLandingUrl) {
321
+ await this.handleUrlNavigation(data);
322
+ }
323
+ } catch (error) {
324
+ ClixLogger.error(
325
+ 'Failed to handle notification opened from background',
326
+ error
327
+ );
328
+ }
399
329
  }
400
330
 
401
- private async checkInitialNotification(): Promise<void> {
331
+ /**
332
+ * iOS: app launched from a quit state via a notification
333
+ */
334
+ private async handleInitialNotification(): Promise<void> {
402
335
  try {
403
- const initialNotification =
404
- await this.messagingService.getInitialNotification();
336
+ const initialNotification = await messaging().getInitialNotification();
405
337
  if (initialNotification) {
406
338
  ClixLogger.debug(
407
339
  'App launched from notification:',
408
340
  initialNotification.messageId
409
341
  );
410
- await this.handleNotificationTap(initialNotification.data ?? {});
342
+
343
+ const data = initialNotification.data ?? {};
344
+ const clixPayload = this.parseClixPayload(data);
345
+ if (clixPayload) {
346
+ await this.trackPushTappedEvent(clixPayload);
347
+ }
348
+ if (this.autoHandleLandingUrl) {
349
+ await this.handleUrlNavigation(data);
350
+ }
411
351
  }
412
352
  } catch (error) {
413
353
  ClixLogger.error('Failed to handle initial notification', error);
414
354
  }
415
355
  }
416
356
 
417
- private async getAndUpdateToken(): Promise<void> {
418
- try {
419
- const token = await this.getCurrentToken();
420
- if (token) {
421
- await this.registerTokenWithServer(token);
357
+ /**
358
+ * Android: background notification tap handler
359
+ */
360
+ private async handleNotificationEvent(event: Event): Promise<void> {
361
+ const { type, detail } = event;
362
+
363
+ switch (type) {
364
+ case EventType.PRESS:
365
+ case EventType.ACTION_PRESS: {
366
+ const data = detail.notification?.data || {};
367
+ const clixPayload = this.parseClixPayload(data);
368
+ if (clixPayload) {
369
+ await this.trackPushTappedEvent(clixPayload);
370
+ }
371
+ if (this.autoHandleLandingUrl) {
372
+ await this.handleUrlNavigation(data);
373
+ }
374
+ break;
422
375
  }
423
- } catch (error) {
424
- ClixLogger.error('Failed to update push token', error);
376
+ case EventType.DISMISSED:
377
+ ClixLogger.debug('Notification dismissed');
378
+ break;
379
+ default:
380
+ ClixLogger.debug('Unhandled notification event type:', type);
425
381
  }
426
382
  }
427
383
 
428
- private async getOrFetchToken(): Promise<string | null> {
429
- if (this.tokenService) {
430
- const savedToken = this.tokenService.getCurrentToken();
431
- if (savedToken) return savedToken;
384
+ /**
385
+ * iOS & Android: foreground notification tap handler
386
+ */
387
+ private async handleForegroundNotificationEvent(event: Event): Promise<void> {
388
+ try {
389
+ await this.foregroundEventHandler?.(event);
390
+ } catch (error) {
391
+ ClixLogger.error('Foreground notification event handler failed', error);
432
392
  }
433
393
 
434
- const token = await this.messagingService.getToken();
435
-
436
- if (token) {
437
- ClixLogger.debug('Got push token:', token.substring(0, 20) + '...');
438
- this.tokenService?.saveToken(token);
439
- }
440
- return token;
394
+ await this.handleNotificationEvent(event);
441
395
  }
442
396
 
443
- private async saveAndRegisterToken(token: string): Promise<void> {
444
- if (this.tokenService) {
445
- this.tokenService.saveToken(token);
446
- ClixLogger.debug('New push token saved via TokenService');
397
+ private async trackPushReceivedEvent(
398
+ payload: ClixPushNotificationPayload
399
+ ): Promise<void> {
400
+ try {
401
+ await this.eventService.trackEvent(
402
+ 'PUSH_NOTIFICATION_RECEIVED',
403
+ {},
404
+ payload.messageId,
405
+ payload.userJourneyId,
406
+ payload.userJourneyNodeId
407
+ );
408
+ ClixLogger.debug(
409
+ 'PUSH_NOTIFICATION_RECEIVED event tracked:',
410
+ payload.messageId
411
+ );
412
+ } catch (error) {
413
+ ClixLogger.error(
414
+ 'Failed to track PUSH_NOTIFICATION_RECEIVED event',
415
+ error
416
+ );
447
417
  }
448
- await this.registerTokenWithServer(token);
449
418
  }
450
419
 
451
- private async registerTokenWithServer(token: string): Promise<void> {
452
- if (this.deviceService) {
453
- await this.deviceService.upsertToken(token);
454
- ClixLogger.debug('Push token registered with server');
420
+ private async trackPushTappedEvent(
421
+ payload: ClixPushNotificationPayload
422
+ ): Promise<void> {
423
+ try {
424
+ await this.eventService.trackEvent(
425
+ 'PUSH_NOTIFICATION_TAPPED',
426
+ {},
427
+ payload.messageId,
428
+ payload.userJourneyId,
429
+ payload.userJourneyNodeId
430
+ );
431
+ ClixLogger.debug(
432
+ 'PUSH_NOTIFICATION_TAPPED event tracked:',
433
+ payload.messageId
434
+ );
435
+ } catch (error) {
436
+ ClixLogger.error('Failed to track PUSH_NOTIFICATION_TAPPED event', error);
455
437
  }
456
438
  }
457
439
 
@@ -460,29 +442,23 @@ export class NotificationService {
460
442
  clixPayload: ClixPushNotificationPayload
461
443
  ): Promise<void> {
462
444
  try {
463
- const notificationContent = this.extractNotificationContent(
464
- remoteMessage.notification,
465
- clixPayload
466
- );
467
-
468
445
  ClixLogger.debug('Creating notification config with content:', {
469
- title: notificationContent.title,
470
- body: notificationContent.body,
471
- hasImage: !!notificationContent.imageUrl,
472
- imageUrl: notificationContent.imageUrl,
446
+ title: clixPayload.title,
447
+ body: clixPayload.body,
448
+ hasImage: !!clixPayload.imageUrl,
449
+ imageUrl: clixPayload.imageUrl,
473
450
  });
474
451
 
475
452
  const notificationConfig = await this.createNotificationConfig(
476
453
  remoteMessage,
477
454
  clixPayload,
478
- notificationContent,
479
455
  NotificationService.DEFAULT_CHANNEL.id
480
456
  );
481
457
 
482
458
  await notifee.displayNotification(notificationConfig);
483
459
  ClixLogger.debug(
484
460
  'Notification displayed successfully:',
485
- notificationContent.title
461
+ clixPayload.title
486
462
  );
487
463
  } catch (error) {
488
464
  ClixLogger.error('Failed to display notification', error);
@@ -501,16 +477,15 @@ export class NotificationService {
501
477
  private async createNotificationConfig(
502
478
  remoteMessage: FirebaseMessagingTypes.RemoteMessage,
503
479
  clixPayload: ClixPushNotificationPayload,
504
- notificationContent: NotificationContent,
505
480
  channelId: string
506
481
  ) {
507
- const imageUrl = notificationContent.imageUrl;
482
+ const imageUrl = clixPayload.imageUrl;
508
483
 
509
484
  const config: Notification &
510
485
  Required<Pick<Notification, 'android' | 'ios'>> = {
511
486
  id: remoteMessage.messageId || Date.now().toString(),
512
- title: notificationContent.title,
513
- body: notificationContent.body,
487
+ title: clixPayload.title,
488
+ body: clixPayload.body,
514
489
  data: remoteMessage.data ?? {},
515
490
  android: {
516
491
  channelId,
@@ -521,11 +496,10 @@ export class NotificationService {
521
496
  groupSummary: false,
522
497
  groupAlertBehavior: AndroidGroupAlertBehavior.CHILDREN,
523
498
  sound: 'default',
524
- ticker: notificationContent.body,
525
- actions: this.createNotificationActions(clixPayload),
499
+ ticker: clixPayload.body,
526
500
  style: {
527
501
  type: AndroidStyle.BIGTEXT,
528
- text: notificationContent.body ?? '',
502
+ text: clixPayload.body,
529
503
  },
530
504
  pressAction: {
531
505
  id: 'default',
@@ -549,122 +523,6 @@ export class NotificationService {
549
523
  return config;
550
524
  }
551
525
 
552
- private createNotificationActions(clixPayload: ClixPushNotificationPayload) {
553
- const actions = [];
554
-
555
- actions.push({
556
- title: 'Open',
557
- pressAction: {
558
- id: 'default',
559
- },
560
- });
561
-
562
- if (clixPayload.customProperties?.actions) {
563
- const customActions = clixPayload.customProperties.actions;
564
- if (Array.isArray(customActions)) {
565
- customActions.forEach((action) => {
566
- if (action.title && action.actionId) {
567
- actions.push({
568
- title: action.title,
569
- pressAction: {
570
- id: action.actionId,
571
- },
572
- });
573
- }
574
- });
575
- }
576
- }
577
-
578
- return actions;
579
- }
580
-
581
- private isValidImageUrl(url: string): boolean {
582
- return url.trim() !== '' && url.startsWith('http');
583
- }
584
-
585
- private extractNotificationContent(
586
- fcmNotification: FirebaseMessagingTypes.Notification | null | undefined,
587
- clixPayload: ClixPushNotificationPayload
588
- ): NotificationContent {
589
- ClixLogger.debug('Extracting notification content from payload:', {
590
- fcmTitle: fcmNotification?.title,
591
- fcmBody: fcmNotification?.body,
592
- customProperties: clixPayload.customProperties,
593
- messageId: clixPayload.messageId,
594
- imageUrl: clixPayload.imageUrl,
595
- });
596
-
597
- const title =
598
- fcmNotification?.title ||
599
- clixPayload.customProperties?.title ||
600
- 'New Message';
601
- const body =
602
- fcmNotification?.body || clixPayload.customProperties?.body || '';
603
-
604
- ClixLogger.debug('Extracted notification content:', { title, body });
605
-
606
- // Validate imageUrl before using it
607
- let imageUrl: string | undefined;
608
- if (clixPayload.imageUrl) {
609
- ClixLogger.debug('Processing image URL:', clixPayload.imageUrl);
610
- if (this.isValidImageUrl(clixPayload.imageUrl)) {
611
- imageUrl = clixPayload.imageUrl;
612
- ClixLogger.debug('Image URL validated successfully');
613
- } else {
614
- ClixLogger.warn('Invalid image URL, skipping:', clixPayload.imageUrl);
615
- }
616
- }
617
-
618
- return {
619
- title,
620
- body,
621
- imageUrl,
622
- };
623
- }
624
-
625
- private async handlePushReceived(data: Record<string, any>): Promise<void> {
626
- try {
627
- const clixPayload = this.parseClixPayload(data);
628
- if (clixPayload) {
629
- await this.trackPushEvent('PUSH_NOTIFICATION_RECEIVED', clixPayload);
630
- }
631
- ClixLogger.debug('Push notification received and processed');
632
- } catch (error) {
633
- ClixLogger.error('Failed to handle push received', error);
634
- }
635
- }
636
-
637
- private async handlePushTapped(data: Record<string, any>): Promise<void> {
638
- try {
639
- const clixPayload = this.parseClixPayload(data);
640
- if (clixPayload) {
641
- await this.trackPushEvent('PUSH_NOTIFICATION_TAPPED', clixPayload);
642
- }
643
- if (this.autoHandleLandingUrl) {
644
- await this.handleUrlNavigation(data);
645
- }
646
- ClixLogger.debug('Push notification tapped and processed');
647
- } catch (error) {
648
- ClixLogger.error('Failed to handle push tapped', error);
649
- }
650
- }
651
-
652
- private async handleNotificationTap(
653
- data: Record<string, any>
654
- ): Promise<void> {
655
- try {
656
- await this.openedHandler?.(data);
657
- } catch (error) {
658
- ClixLogger.error('Failed to handle notification tap', error);
659
- }
660
-
661
- try {
662
- await this.handlePushTapped(data);
663
- } catch (error) {
664
- ClixLogger.error('Failed to handle notification tap', error);
665
- }
666
- }
667
-
668
526
  private async handleUrlNavigation(data: Record<string, any>): Promise<void> {
669
527
  try {
670
528
  let url: string | undefined;
@@ -699,128 +557,55 @@ export class NotificationService {
699
557
  userInfo: Record<string, any>
700
558
  ): ClixPushNotificationPayload | null {
701
559
  try {
702
- let payload: any = userInfo;
703
- if (userInfo.clix) {
704
- if (typeof userInfo.clix === 'object') {
705
- payload = userInfo.clix;
706
- } else if (typeof userInfo.clix === 'string') {
707
- payload = JSON.parse(userInfo.clix);
708
- }
560
+ let data = userInfo?.clix;
561
+ if (data == null) {
562
+ ClixLogger.debug("No 'clix' entry found in notification data");
563
+ return null;
709
564
  }
710
- const toCamel = (s: string) =>
711
- s.replace(/_([a-z])/g, (g) => (g[1] ?? '').toUpperCase());
712
- const result: any = {};
713
- if (!payload) return null;
714
- for (const key in payload) {
715
- result[toCamel(key)] = payload[key];
716
- }
717
-
718
- ClixLogger.debug('Parsed Clix payload result:', result);
719
565
 
720
- if (!result.messageId) return null;
566
+ if (typeof data === 'string') {
567
+ try {
568
+ data = JSON.parse(data);
569
+ } catch (parseError) {
570
+ ClixLogger.error(
571
+ 'Failed to parse Clix payload JSON string',
572
+ parseError
573
+ );
574
+ return null;
575
+ }
576
+ }
721
577
 
722
- // Extract title and body into customProperties if they exist
723
- const customProperties: Record<string, any> = {};
724
- if (result.title) customProperties.title = result.title;
725
- if (result.body) customProperties.body = result.body;
578
+ ClixLogger.debug('Parsing Clix payload from notification data:', data);
726
579
 
727
- const finalResult = {
728
- ...result,
729
- customProperties:
730
- Object.keys(customProperties).length > 0
731
- ? customProperties
732
- : undefined,
580
+ const payload: ClixPushNotificationPayload = {
581
+ messageId: data.message_id,
582
+ title: data.title,
583
+ body: data.body,
584
+ imageUrl: data.image_url || undefined,
585
+ landingUrl: data.landing_url || undefined,
586
+ userJourneyId: data.user_journey_id || undefined,
587
+ userJourneyNodeId: data.user_journey_node_id || undefined,
733
588
  };
734
589
 
735
- ClixLogger.debug('Final Clix payload result:', finalResult);
590
+ ClixLogger.debug('Constructed Clix payload:', payload);
736
591
 
737
- return new ClixPushNotificationPayload(finalResult);
738
- } catch (error) {
739
- ClixLogger.error('Failed to parse Clix payload', error);
740
- return null;
741
- }
742
- }
743
-
744
- private async trackPushEvent(
745
- eventType: string,
746
- clixPayload: ClixPushNotificationPayload
747
- ): Promise<void> {
748
- const properties = this.extractTrackingProperties(clixPayload);
749
- const messageId = clixPayload.messageId;
750
- await this.eventService.trackEvent(eventType, properties, messageId);
751
- ClixLogger.debug(`${eventType} tracked:`, messageId);
752
- }
753
-
754
- private extractTrackingProperties(
755
- clixPayload: ClixPushNotificationPayload
756
- ): Record<string, any> {
757
- const properties: Record<string, any> = {};
758
- if (clixPayload.messageId) properties.messageId = clixPayload.messageId;
759
- if (clixPayload.campaignId) properties.campaignId = clixPayload.campaignId;
760
- if (clixPayload.trackingId) properties.trackingId = clixPayload.trackingId;
761
- return properties;
762
- }
763
-
764
- private async trackEventInBackground(
765
- clixPayload: ClixPushNotificationPayload
766
- ): Promise<void> {
767
- const messageId = clixPayload.messageId;
768
- if (!messageId) {
769
- ClixLogger.warn('No messageId found in payload, skipping event tracking');
770
- return;
771
- }
772
- try {
773
- const configData =
774
- this.storageService.get<Record<string, any>>('clix_config');
775
- if (!configData) {
776
- ClixLogger.error('No Clix config found in storage');
777
- return;
592
+ if (!payload.messageId) {
593
+ ClixLogger.error('No messageId found in Clix payload');
594
+ return null;
778
595
  }
779
- let deviceId = this.storageService.get<string>('clix_device_id');
780
- if (!deviceId) {
781
- ClixLogger.warn(
782
- 'No device ID found in storage, generating new device ID'
783
- );
784
- deviceId = UUID.generate();
785
- this.storageService.set('clix_device_id', deviceId);
596
+ if (!payload.title) {
597
+ ClixLogger.error('No title found in Clix payload');
598
+ return null;
599
+ }
600
+ if (!payload.body) {
601
+ ClixLogger.error('No body found in Clix payload');
602
+ return null;
786
603
  }
787
604
 
788
- const properties = this.extractTrackingProperties(clixPayload);
789
- await this.eventService.trackEvent(
790
- 'PUSH_NOTIFICATION_RECEIVED',
791
- properties,
792
- messageId
793
- );
794
- ClixLogger.debug(
795
- 'PUSH_NOTIFICATION_RECEIVED event tracked in background'
796
- );
797
- } catch (error) {
798
- ClixLogger.error('Error tracking event in background', error);
799
- }
800
- }
801
-
802
- private async shouldDisplayForegroundNotification(
803
- data: Record<string, any>
804
- ): Promise<boolean> {
805
- try {
806
- const result = await this.messageHandler?.(data);
807
- return result !== false;
605
+ return payload;
808
606
  } catch (error) {
809
- ClixLogger.warn(
810
- 'Foreground message handler failed, displaying notification by default',
811
- error
812
- );
813
- return true;
814
- }
815
- }
816
-
817
- private async handleFcmTokenError(error: any): Promise<void> {
818
- try {
819
- const errorInstance =
820
- error instanceof Error ? error : new Error(String(error));
821
- await this.fcmTokenErrorHandler?.(errorInstance);
822
- } catch (handlerError) {
823
- ClixLogger.warn('FCM token error handler failed', handlerError);
607
+ ClixLogger.error('Failed to parse Clix payload', error);
608
+ return null;
824
609
  }
825
610
  }
826
611
  }