@loyalytics/swan-react-native-sdk 2.5.1-beta.5 → 2.5.1-beta.7

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.
@@ -131,7 +131,7 @@ function parseKeyValuePairs(data) {
131
131
  * Notifee handlers fire for the same notification.
132
132
  */
133
133
  const processedClickIds = new Set();
134
- const CLICK_ID_TTL_MS = 10_000; // Clear after 10 seconds
134
+ const CLICK_ID_TTL_MS = 30_000; // Clear after 30 seconds
135
135
 
136
136
  function markClickProcessed(id) {
137
137
  if (processedClickIds.has(id)) {
@@ -142,9 +142,17 @@ function markClickProcessed(id) {
142
142
  return true; // First time processing
143
143
  }
144
144
 
145
+ /**
146
+ * Track messageIds that already received a direct click ACK from the background
147
+ * handler when the SDK was not ready. Prevents setupNotifeeEventListeners from
148
+ * sending a duplicate click ACK for the same notification.
149
+ */
150
+ const directAckSentIds = new Set();
151
+
145
152
  /** @internal Test-only: reset click deduplication state */
146
153
  function _resetClickDedup() {
147
154
  processedClickIds.clear();
155
+ directAckSentIds.clear();
148
156
  }
149
157
 
150
158
  /**
@@ -2897,31 +2905,48 @@ class SwanSDK {
2897
2905
  // similar to how Firebase requires onMessage() at module level.
2898
2906
 
2899
2907
  // Check if app was opened by tapping a Notifee-displayed notification (data-only architecture)
2900
- // Notifee's getInitialNotification() returns { notification, pressAction }.
2901
- // The FCM data we stored via displayNotification({ data: {...} }) lives at
2902
- // notification.data — NOT at the top-level .data (which doesn't exist).
2903
2908
  const initialNotification = await notifee.getInitialNotification();
2909
+ _Logger.default.log('[SwanSDK] getInitialNotification result:', JSON.stringify(initialNotification, null, 2));
2904
2910
  if (initialNotification && !this.initialNotificationHandled) {
2905
- const messageId = initialNotification?.notification?.id;
2906
- const notificationData = initialNotification.notification?.data || {};
2907
- if (messageId && markClickProcessed(messageId)) {
2908
- _Logger.default.log('[SwanSDK] App opened from Notifee notification tap:', messageId);
2909
- this.initialNotificationHandled = true;
2910
- await this.sendNotificationAck(messageId, 'clicked');
2911
+ // Notifee returned an initial notification — mark as handled
2912
+ // unconditionally to prevent Firebase fallback from double-emitting.
2913
+ this.initialNotificationHandled = true;
2914
+ if (!this.isPushEnabled()) {
2915
+ _Logger.default.log('[SwanSDK] Push notifications disabled, ignoring initial notification');
2916
+ } else {
2917
+ const notificationData = initialNotification.notification?.data || {};
2918
+ const messageId = notificationData.messageId;
2919
+
2920
+ // Carousel notifications: skip standard path entirely.
2921
+ // checkPendingCarouselClick() (triggered by addListener) handles
2922
+ // per-item routes from native storage / App Group.
2923
+ if (notificationData.notificationType === 'carousel') {
2924
+ _Logger.default.log('[SwanSDK] Carousel initial notification, deferring to checkPendingCarouselClick');
2925
+ } else if (messageId && !markClickProcessed(messageId)) {
2926
+ _Logger.default.log('[SwanSDK] Click already processed for messageId:', messageId);
2927
+ } else {
2928
+ _Logger.default.log('[SwanSDK] App opened from Notifee notification tap:', messageId);
2929
+
2930
+ // Extract deep link information
2931
+ const resolvedRoute = notificationData.route || notificationData.defaultRoute;
2932
+ const deepLinkPayload = {
2933
+ ...notificationData,
2934
+ route: resolvedRoute,
2935
+ keyValuePairs: parseKeyValuePairs(notificationData),
2936
+ title: initialNotification.notification?.title || notificationData.title,
2937
+ body: initialNotification.notification?.body || notificationData.body
2938
+ };
2939
+
2940
+ // Emit deep link FIRST to ensure delivery even if ACK fails
2941
+ _Logger.default.log('[SwanSDK] Cold-start deepLinkPayload:', JSON.stringify(deepLinkPayload, null, 2));
2942
+ this.emitNotificationOpened(deepLinkPayload);
2943
+
2944
+ // Send click ACK after emit (skip if background handler already sent one)
2945
+ if (messageId && !directAckSentIds.has(messageId)) {
2946
+ await this.sendNotificationAck(messageId, 'clicked');
2947
+ }
2948
+ }
2911
2949
  }
2912
- // Extract deep link information
2913
- // For carousel notifications, the default route is in 'defaultRoute' field
2914
- const resolvedRoute = notificationData.route || notificationData.defaultRoute;
2915
- const deepLinkPayload = {
2916
- ...notificationData,
2917
- route: resolvedRoute,
2918
- keyValuePairs: parseKeyValuePairs(notificationData),
2919
- title: initialNotification.notification?.title || notificationData.title,
2920
- body: initialNotification.notification?.body || notificationData.body
2921
- };
2922
-
2923
- // Emit notificationOpened event for host app to handle deep linking
2924
- this.emitNotificationOpened(deepLinkPayload);
2925
2950
  }
2926
2951
 
2927
2952
  // Also check Firebase's getInitialNotification for iOS auto-displayed notifications
@@ -2945,7 +2970,21 @@ class SwanSDK {
2945
2970
  const messaging = require('@react-native-firebase/messaging').default;
2946
2971
  const initialNotification = await messaging().getInitialNotification();
2947
2972
  if (initialNotification && !this.initialNotificationHandled) {
2948
- const messageId = initialNotification.messageId;
2973
+ this.initialNotificationHandled = true;
2974
+ if (!this.isPushEnabled()) {
2975
+ _Logger.default.log('[SwanSDK] Push notifications disabled, ignoring Firebase initial notification');
2976
+ return;
2977
+ }
2978
+ const notificationData = initialNotification.data || {};
2979
+ // Use custom messageId from data (consistent with Notifee handlers),
2980
+ // fall back to Firebase's system messageId
2981
+ const messageId = notificationData.messageId || initialNotification.messageId;
2982
+
2983
+ // Carousel notifications: defer to checkPendingCarouselClick for per-item routes
2984
+ if (notificationData.notificationType === 'carousel') {
2985
+ _Logger.default.log('[SwanSDK] Carousel Firebase initial notification, deferring to checkPendingCarouselClick');
2986
+ return;
2987
+ }
2949
2988
 
2950
2989
  // Deduplication: prevent double click handling
2951
2990
  if (messageId && !markClickProcessed(messageId)) {
@@ -2954,12 +2993,7 @@ class SwanSDK {
2954
2993
  }
2955
2994
  _Logger.default.log('[SwanSDK] App opened from Firebase notification (killed state):', messageId);
2956
2995
 
2957
- // Mark as handled to prevent duplicate ACKs
2958
- this.initialNotificationHandled = true;
2959
- const notificationData = initialNotification.data || {};
2960
-
2961
2996
  // Extract deep link information
2962
- // For carousel notifications, the default route is in 'defaultRoute' field
2963
2997
  const resolvedRoute = notificationData.route || notificationData.defaultRoute;
2964
2998
  const deepLinkPayload = {
2965
2999
  ...notificationData,
@@ -2969,13 +3003,11 @@ class SwanSDK {
2969
3003
  body: initialNotification.notification?.body || notificationData.body
2970
3004
  };
2971
3005
 
2972
- // Emit notificationOpened event for host app to handle deep linking
3006
+ // Emit deep link FIRST, then ACK
2973
3007
  this.emitNotificationOpened(deepLinkPayload);
2974
-
2975
- // Send click ACK
2976
- if (messageId) {
3008
+ if (messageId && !directAckSentIds.has(messageId)) {
2977
3009
  await this.sendNotificationAck(messageId, 'clicked');
2978
- _Logger.default.log('[SwanSDK] Firebase initial notification click ACK sent');
3010
+ _Logger.default.log('[SwanSDK] Firebase initial notification click ACK sent');
2979
3011
  }
2980
3012
  }
2981
3013
  } catch (error) {
@@ -3827,7 +3859,7 @@ function createNotificationOpenedHandler() {
3827
3859
  _Logger.default.warn('[SwanSDK] No messageId in notification data, skipping click ACK');
3828
3860
  }
3829
3861
  } catch (error) {
3830
- _Logger.default.error('[SwanSDK] Error handling Notifee foreground event:', error);
3862
+ _Logger.default.error('[SwanSDK] Error handling notification opened event:', error);
3831
3863
  }
3832
3864
  };
3833
3865
  }
@@ -3965,41 +3997,8 @@ function createNotifeeBackgroundHandler() {
3965
3997
  // Get messageId and notification data
3966
3998
  const messageId = detail?.notification?.data?.messageId;
3967
3999
  const notificationData = detail?.notification?.data || {};
3968
-
3969
- // Deduplication: prevent double click handling
3970
- if (messageId && !markClickProcessed(messageId)) {
3971
- _Logger.default.log('[SwanSDK] Click already processed for messageId:', messageId);
3972
- return;
3973
- }
3974
-
3975
- // Extract deep link information
3976
- // Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
3977
- // For carousel notifications, the default route is in 'defaultRoute' field
3978
- let route = notificationData.route || notificationData.defaultRoute;
3979
-
3980
- // For carousel notifications on iOS, the Content Extension saves per-item
3981
- // click data (including item-specific route) to the App Group.
3982
- if (_reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel') {
3983
- try {
3984
- const clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
3985
- if (clickData?.route) {
3986
- _Logger.default.log('[SwanSDK] Background: carousel item route from Content Extension:', clickData.route);
3987
- route = clickData.route;
3988
- }
3989
- } catch (err) {
3990
- _Logger.default.warn('[SwanSDK] Failed to read Content Extension click data:', err);
3991
- }
3992
- }
3993
- const deepLinkPayload = {
3994
- ...notificationData,
3995
- route,
3996
- keyValuePairs: parseKeyValuePairs(notificationData),
3997
- title: detail?.notification?.title,
3998
- body: detail?.notification?.body
3999
- };
4000
4000
  _Logger.default.log('[SwanSDK] Background notification clicked:', {
4001
4001
  messageId,
4002
- route: deepLinkPayload.route,
4003
4002
  type: notificationData.notificationType
4004
4003
  });
4005
4004
 
@@ -4007,10 +4006,39 @@ function createNotifeeBackgroundHandler() {
4007
4006
  const sdkInstance = SwanSDK.getCurrentInstance();
4008
4007
  const isSDKReady = sdkInstance && sdkInstance.isReady();
4009
4008
  if (isSDKReady) {
4009
+ // Deduplication: only when SDK can deliver the deep link
4010
+ if (messageId && !markClickProcessed(messageId)) {
4011
+ _Logger.default.log('[SwanSDK] Click already processed for messageId:', messageId);
4012
+ return;
4013
+ }
4010
4014
  if (!sdkInstance.isPushEnabled()) {
4011
4015
  return;
4012
4016
  }
4013
4017
 
4018
+ // Extract deep link information
4019
+ let route = notificationData.route || notificationData.defaultRoute;
4020
+
4021
+ // For carousel notifications on iOS, the Content Extension saves per-item
4022
+ // click data (including item-specific route) to the App Group.
4023
+ if (_reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel') {
4024
+ try {
4025
+ const clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
4026
+ if (clickData?.route) {
4027
+ _Logger.default.log('[SwanSDK] Background: carousel item route from Content Extension:', clickData.route);
4028
+ route = clickData.route;
4029
+ }
4030
+ } catch (err) {
4031
+ _Logger.default.warn('[SwanSDK] Failed to read Content Extension click data:', err);
4032
+ }
4033
+ }
4034
+ const deepLinkPayload = {
4035
+ ...notificationData,
4036
+ route,
4037
+ keyValuePairs: parseKeyValuePairs(notificationData),
4038
+ title: detail?.notification?.title,
4039
+ body: detail?.notification?.body
4040
+ };
4041
+
4014
4042
  // Emit notificationOpened event for host app to handle
4015
4043
  sdkInstance.emitNotificationOpened(deepLinkPayload);
4016
4044
  _Logger.default.log('[SwanSDK] Background: emitted notificationOpened with route:', deepLinkPayload.route);
@@ -4023,9 +4051,12 @@ function createNotifeeBackgroundHandler() {
4023
4051
  return;
4024
4052
  }
4025
4053
 
4026
- // SDK not ready (app was killed) - send ACK directly via fetch
4054
+ // SDK not ready (app was killed) send ACK directly via fetch.
4055
+ // Do NOT markClickProcessed here: setupNotifeeEventListeners will
4056
+ // handle dedup and deep link routing when the SDK initializes.
4027
4057
  _Logger.default.log('[SwanSDK] SDK not ready, sending click ACK via direct fetch');
4028
4058
  if (messageId) {
4059
+ directAckSentIds.add(messageId);
4029
4060
  await sendDirectNotificationAck(messageId, 'clicked');
4030
4061
  }
4031
4062
  } catch (error) {