@clix-so/react-native-sdk 1.0.0 → 1.1.1-beta.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 (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 +253 -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 +109 -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 +321 -534
  83. package/src/services/TokenService.ts +4 -71
  84. package/src/utils/http/HTTPClient.ts +154 -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,214 +61,161 @@ 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> {
265
215
  ClixLogger.debug('Handling background message:', remoteMessage.messageId);
266
216
 
217
+ setTimeout(() => ClixLogger.debug('still alive after 3s'), 3000);
218
+
267
219
  const data = remoteMessage.data ?? {};
268
220
  try {
269
221
  await this.backgroundMessageHandler?.(data);
@@ -278,180 +230,212 @@ export class NotificationService {
278
230
  return;
279
231
  }
280
232
 
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);
291
-
292
233
  if (!remoteMessage.notification) {
293
234
  await this.displayNotification(remoteMessage, clixPayload);
294
235
  }
236
+
237
+ await this.trackPushReceivedEvent(clixPayload);
295
238
  } catch (error) {
296
239
  ClixLogger.error('Background message handler error:', error);
297
240
  }
298
241
  }
299
242
 
243
+ /**
244
+ * iOS & Android: foreground message handler
245
+ */
300
246
  private async handleForegroundMessage(
301
247
  remoteMessage: FirebaseMessagingTypes.RemoteMessage
302
248
  ): Promise<void> {
303
249
  ClixLogger.debug('Handling foreground message:', remoteMessage.messageId);
304
250
 
305
- try {
306
- const messageId = remoteMessage.messageId;
307
- if (!messageId) {
308
- ClixLogger.warn('No messageId found in foreground message');
309
- return;
310
- }
251
+ const messageId = remoteMessage.messageId;
252
+ if (!messageId) {
253
+ ClixLogger.warn('No messageId found in foreground message');
254
+ return;
255
+ }
256
+
257
+ if (this.processedMessageIds.has(messageId)) {
258
+ ClixLogger.debug(
259
+ 'Message already processed, skipping duplicate:',
260
+ messageId
261
+ );
262
+ return;
263
+ }
311
264
 
312
- if (this.processedMessageIds.has(messageId)) {
265
+ const data = remoteMessage.data ?? {};
266
+ try {
267
+ const result = await this.messageHandler?.(data);
268
+ if (result === false) {
313
269
  ClixLogger.debug(
314
- 'Message already processed, skipping duplicate:',
315
- messageId
270
+ 'Foreground message suppressed by user handler:',
271
+ remoteMessage.messageId
316
272
  );
317
273
  return;
318
274
  }
275
+ } catch (error) {
276
+ ClixLogger.error('Foreground message handler failed', error);
277
+ }
319
278
 
320
- const data = remoteMessage.data ?? {};
279
+ try {
321
280
  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
- }
281
+ if (!clixPayload) {
282
+ ClixLogger.warn('No Clix payload found in background message');
283
+ return;
284
+ }
329
285
 
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');
286
+ this.processedMessageIds.add(messageId);
287
+
288
+ await this.displayNotification(remoteMessage, clixPayload);
289
+
290
+ if (Platform.OS === 'android') {
291
+ // NOTE(nyanxyz): on iOS, Received event is tracked in Notification Service Extension
292
+ await this.trackPushReceivedEvent(clixPayload);
340
293
  }
341
294
  } catch (error) {
342
295
  ClixLogger.error('Failed to handle foreground message', error);
343
296
  }
344
297
  }
345
298
 
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,
299
+ /**
300
+ * iOS: background notification tap handler
301
+ */
302
+ private async handleNotificationOpenedApp(
303
+ remoteMessage: FirebaseMessagingTypes.RemoteMessage
304
+ ): Promise<void> {
305
+ ClixLogger.debug('Handling notification opened from background:', {
306
+ messageId: remoteMessage.messageId,
307
+ data: remoteMessage.data,
355
308
  });
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
309
 
310
+ const data = remoteMessage.data ?? {};
374
311
  try {
375
- await this.deviceService.upsertIsPushPermissionGranted(isGranted);
376
- ClixLogger.debug(
377
- `Push permission status reported to server: ${
378
- isGranted ? 'granted' : 'denied'
379
- }`
380
- );
312
+ await this.notificationOpenedAppHandler?.(data);
381
313
  } catch (error) {
382
- ClixLogger.warn('Failed to upsert push permission status', error);
314
+ ClixLogger.error('Notification opened app handler failed', error);
383
315
  }
384
- }
385
316
 
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
- }
317
+ try {
318
+ const clixPayload = this.parseClixPayload(data);
319
+ if (clixPayload) {
320
+ await this.trackPushTappedEvent(clixPayload);
397
321
  }
398
- );
322
+ if (this.autoHandleLandingUrl) {
323
+ await this.handleUrlNavigation(data);
324
+ }
325
+ } catch (error) {
326
+ ClixLogger.error(
327
+ 'Failed to handle notification opened from background',
328
+ error
329
+ );
330
+ }
399
331
  }
400
332
 
401
- private async checkInitialNotification(): Promise<void> {
333
+ /**
334
+ * iOS: app launched from a quit state via a notification
335
+ */
336
+ private async handleInitialNotification(): Promise<void> {
402
337
  try {
403
- const initialNotification =
404
- await this.messagingService.getInitialNotification();
338
+ const initialNotification = await messaging().getInitialNotification();
405
339
  if (initialNotification) {
406
340
  ClixLogger.debug(
407
341
  'App launched from notification:',
408
342
  initialNotification.messageId
409
343
  );
410
- await this.handleNotificationTap(initialNotification.data ?? {});
344
+
345
+ const data = initialNotification.data ?? {};
346
+ const clixPayload = this.parseClixPayload(data);
347
+ if (clixPayload) {
348
+ await this.trackPushTappedEvent(clixPayload);
349
+ }
350
+ if (this.autoHandleLandingUrl) {
351
+ await this.handleUrlNavigation(data);
352
+ }
411
353
  }
412
354
  } catch (error) {
413
355
  ClixLogger.error('Failed to handle initial notification', error);
414
356
  }
415
357
  }
416
358
 
417
- private async getAndUpdateToken(): Promise<void> {
418
- try {
419
- const token = await this.getCurrentToken();
420
- if (token) {
421
- await this.registerTokenWithServer(token);
359
+ /**
360
+ * Android: background notification tap handler
361
+ */
362
+ private async handleNotificationEvent(event: Event): Promise<void> {
363
+ const { type, detail } = event;
364
+
365
+ switch (type) {
366
+ case EventType.PRESS:
367
+ case EventType.ACTION_PRESS: {
368
+ const data = detail.notification?.data || {};
369
+ const clixPayload = this.parseClixPayload(data);
370
+ if (clixPayload) {
371
+ await this.trackPushTappedEvent(clixPayload);
372
+ }
373
+ if (this.autoHandleLandingUrl) {
374
+ await this.handleUrlNavigation(data);
375
+ }
376
+ break;
422
377
  }
423
- } catch (error) {
424
- ClixLogger.error('Failed to update push token', error);
378
+ case EventType.DISMISSED:
379
+ ClixLogger.debug('Notification dismissed');
380
+ break;
381
+ default:
382
+ ClixLogger.debug('Unhandled notification event type:', type);
425
383
  }
426
384
  }
427
385
 
428
- private async getOrFetchToken(): Promise<string | null> {
429
- if (this.tokenService) {
430
- const savedToken = this.tokenService.getCurrentToken();
431
- if (savedToken) return savedToken;
386
+ /**
387
+ * iOS & Android: foreground notification tap handler
388
+ */
389
+ private async handleForegroundNotificationEvent(event: Event): Promise<void> {
390
+ try {
391
+ await this.foregroundEventHandler?.(event);
392
+ } catch (error) {
393
+ ClixLogger.error('Foreground notification event handler failed', error);
432
394
  }
433
395
 
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;
396
+ await this.handleNotificationEvent(event);
441
397
  }
442
398
 
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');
399
+ private async trackPushReceivedEvent(
400
+ payload: ClixPushNotificationPayload
401
+ ): Promise<void> {
402
+ try {
403
+ await this.eventService.trackEvent(
404
+ 'PUSH_NOTIFICATION_RECEIVED',
405
+ {},
406
+ payload.messageId,
407
+ payload.userJourneyId,
408
+ payload.userJourneyNodeId
409
+ );
410
+ ClixLogger.debug(
411
+ 'PUSH_NOTIFICATION_RECEIVED event tracked:',
412
+ payload.messageId
413
+ );
414
+ } catch (error) {
415
+ ClixLogger.error(
416
+ 'Failed to track PUSH_NOTIFICATION_RECEIVED event',
417
+ error
418
+ );
447
419
  }
448
- await this.registerTokenWithServer(token);
449
420
  }
450
421
 
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');
422
+ private async trackPushTappedEvent(
423
+ payload: ClixPushNotificationPayload
424
+ ): Promise<void> {
425
+ try {
426
+ await this.eventService.trackEvent(
427
+ 'PUSH_NOTIFICATION_TAPPED',
428
+ {},
429
+ payload.messageId,
430
+ payload.userJourneyId,
431
+ payload.userJourneyNodeId
432
+ );
433
+ ClixLogger.debug(
434
+ 'PUSH_NOTIFICATION_TAPPED event tracked:',
435
+ payload.messageId
436
+ );
437
+ } catch (error) {
438
+ ClixLogger.error('Failed to track PUSH_NOTIFICATION_TAPPED event', error);
455
439
  }
456
440
  }
457
441
 
@@ -460,29 +444,23 @@ export class NotificationService {
460
444
  clixPayload: ClixPushNotificationPayload
461
445
  ): Promise<void> {
462
446
  try {
463
- const notificationContent = this.extractNotificationContent(
464
- remoteMessage.notification,
465
- clixPayload
466
- );
467
-
468
447
  ClixLogger.debug('Creating notification config with content:', {
469
- title: notificationContent.title,
470
- body: notificationContent.body,
471
- hasImage: !!notificationContent.imageUrl,
472
- imageUrl: notificationContent.imageUrl,
448
+ title: clixPayload.title,
449
+ body: clixPayload.body,
450
+ hasImage: !!clixPayload.imageUrl,
451
+ imageUrl: clixPayload.imageUrl,
473
452
  });
474
453
 
475
454
  const notificationConfig = await this.createNotificationConfig(
476
455
  remoteMessage,
477
456
  clixPayload,
478
- notificationContent,
479
457
  NotificationService.DEFAULT_CHANNEL.id
480
458
  );
481
459
 
482
460
  await notifee.displayNotification(notificationConfig);
483
461
  ClixLogger.debug(
484
462
  'Notification displayed successfully:',
485
- notificationContent.title
463
+ clixPayload.title
486
464
  );
487
465
  } catch (error) {
488
466
  ClixLogger.error('Failed to display notification', error);
@@ -501,16 +479,15 @@ export class NotificationService {
501
479
  private async createNotificationConfig(
502
480
  remoteMessage: FirebaseMessagingTypes.RemoteMessage,
503
481
  clixPayload: ClixPushNotificationPayload,
504
- notificationContent: NotificationContent,
505
482
  channelId: string
506
483
  ) {
507
- const imageUrl = notificationContent.imageUrl;
484
+ const imageUrl = clixPayload.imageUrl;
508
485
 
509
486
  const config: Notification &
510
487
  Required<Pick<Notification, 'android' | 'ios'>> = {
511
488
  id: remoteMessage.messageId || Date.now().toString(),
512
- title: notificationContent.title,
513
- body: notificationContent.body,
489
+ title: clixPayload.title,
490
+ body: clixPayload.body,
514
491
  data: remoteMessage.data ?? {},
515
492
  android: {
516
493
  channelId,
@@ -521,11 +498,10 @@ export class NotificationService {
521
498
  groupSummary: false,
522
499
  groupAlertBehavior: AndroidGroupAlertBehavior.CHILDREN,
523
500
  sound: 'default',
524
- ticker: notificationContent.body,
525
- actions: this.createNotificationActions(clixPayload),
501
+ ticker: clixPayload.body,
526
502
  style: {
527
503
  type: AndroidStyle.BIGTEXT,
528
- text: notificationContent.body ?? '',
504
+ text: clixPayload.body,
529
505
  },
530
506
  pressAction: {
531
507
  id: 'default',
@@ -549,122 +525,6 @@ export class NotificationService {
549
525
  return config;
550
526
  }
551
527
 
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
528
  private async handleUrlNavigation(data: Record<string, any>): Promise<void> {
669
529
  try {
670
530
  let url: string | undefined;
@@ -699,128 +559,55 @@ export class NotificationService {
699
559
  userInfo: Record<string, any>
700
560
  ): ClixPushNotificationPayload | null {
701
561
  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
- }
709
- }
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];
562
+ let data = userInfo?.clix;
563
+ if (data == null) {
564
+ ClixLogger.debug("No 'clix' entry found in notification data");
565
+ return null;
716
566
  }
717
567
 
718
- ClixLogger.debug('Parsed Clix payload result:', result);
719
-
720
- if (!result.messageId) return null;
568
+ if (typeof data === 'string') {
569
+ try {
570
+ data = JSON.parse(data);
571
+ } catch (parseError) {
572
+ ClixLogger.error(
573
+ 'Failed to parse Clix payload JSON string',
574
+ parseError
575
+ );
576
+ return null;
577
+ }
578
+ }
721
579
 
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;
580
+ ClixLogger.debug('Parsing Clix payload from notification data:', data);
726
581
 
727
- const finalResult = {
728
- ...result,
729
- customProperties:
730
- Object.keys(customProperties).length > 0
731
- ? customProperties
732
- : undefined,
582
+ const payload: ClixPushNotificationPayload = {
583
+ messageId: data.message_id,
584
+ title: data.title,
585
+ body: data.body,
586
+ imageUrl: data.image_url || undefined,
587
+ landingUrl: data.landing_url || undefined,
588
+ userJourneyId: data.user_journey_id || undefined,
589
+ userJourneyNodeId: data.user_journey_node_id || undefined,
733
590
  };
734
591
 
735
- ClixLogger.debug('Final Clix payload result:', finalResult);
592
+ ClixLogger.debug('Constructed Clix payload:', payload);
736
593
 
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;
594
+ if (!payload.messageId) {
595
+ ClixLogger.error('No messageId found in Clix payload');
596
+ return null;
778
597
  }
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);
598
+ if (!payload.title) {
599
+ ClixLogger.error('No title found in Clix payload');
600
+ return null;
601
+ }
602
+ if (!payload.body) {
603
+ ClixLogger.error('No body found in Clix payload');
604
+ return null;
786
605
  }
787
606
 
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
- );
607
+ return payload;
797
608
  } 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;
808
- } 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);
609
+ ClixLogger.error('Failed to parse Clix payload', error);
610
+ return null;
824
611
  }
825
612
  }
826
613
  }