@loyalytics/swan-react-native-sdk 2.3.1-beta.0 → 2.3.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationModule.kt +145 -1
  2. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselAutoRemoteViews.kt +3 -22
  3. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselFilmstripRemoteViews.kt +7 -0
  4. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselRemoteViews.kt +7 -0
  5. package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplate.kt +141 -9
  6. package/android/src/main/res/layout/swan_carousel_auto_expanded.xml +1 -1
  7. package/android/src/main/res/layout/swan_carousel_expanded.xml +1 -1
  8. package/android/src/main/res/layout/swan_carousel_filmstrip_expanded.xml +1 -1
  9. package/ios/SwanAppGroup.m +55 -0
  10. package/ios/SwanNotificationContentExtension/NotificationViewController.swift +90 -28
  11. package/ios/SwanNotificationContentExtension/templates/CarouselView.swift +5 -0
  12. package/ios/SwanNotificationServiceExtension/NotificationService.swift +1 -0
  13. package/lib/commonjs/components/HeaderView.js +0 -1
  14. package/lib/commonjs/components/HeaderView.js.map +1 -1
  15. package/lib/commonjs/index.js +226 -18
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/utils/SharedCredentialsManager.js +46 -8
  18. package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -1
  19. package/lib/commonjs/version.js +1 -1
  20. package/lib/module/components/HeaderView.js +1 -1
  21. package/lib/module/components/HeaderView.js.map +1 -1
  22. package/lib/module/index.js +224 -17
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/utils/SharedCredentialsManager.js +48 -9
  25. package/lib/module/utils/SharedCredentialsManager.js.map +1 -1
  26. package/lib/module/version.js +1 -1
  27. package/lib/typescript/commonjs/src/components/HeaderView.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/src/index.d.ts +19 -0
  29. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +6 -0
  31. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/src/version.d.ts +1 -1
  33. package/lib/typescript/module/src/components/HeaderView.d.ts.map +1 -1
  34. package/lib/typescript/module/src/index.d.ts +19 -0
  35. package/lib/typescript/module/src/index.d.ts.map +1 -1
  36. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +6 -0
  37. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -1
  38. package/lib/typescript/module/src/version.d.ts +1 -1
  39. package/package.json +23 -10
  40. package/scripts/setup-ios-extension.js +61 -41
  41. package/swan-react-native-sdk.podspec +1 -0
@@ -10,7 +10,12 @@ import UserNotificationsUI
10
10
  * - swan_carousel: Swipeable product carousel
11
11
  * (Future: swan_timer, swan_cta, swan_rating)
12
12
  *
13
- * Action buttons (registered by SDK via Notifee setNotificationCategories):
13
+ * User interaction:
14
+ * - Swipe left/right on the carousel to navigate between items
15
+ * - Tap on the carousel image to open the deep link for the current item
16
+ * - Deep link uses extensionContext.open(URL) with the deepLinkScheme from the push payload
17
+ *
18
+ * Action button identifiers (reserved for future use if categories are registered):
14
19
  * - swan_prev: Navigate to previous carousel item
15
20
  * - swan_next: Navigate to next carousel item
16
21
  * - swan_open: Open deep link for current item
@@ -18,8 +23,9 @@ import UserNotificationsUI
18
23
  class NotificationViewController: UIViewController, UNNotificationContentExtension {
19
24
 
20
25
  private var carouselView: CarouselView?
26
+ private var currentUserInfo: [AnyHashable: Any] = [:]
21
27
 
22
- /// Action button identifiers (must match what SDK registers via setNotificationCategories)
28
+ /// Action button identifiers (reserved for future use if notification categories are registered)
23
29
  private enum ActionID {
24
30
  static let prev = "swan_prev"
25
31
  static let next = "swan_next"
@@ -77,14 +83,22 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
77
83
  carouselView?.scrollToNext()
78
84
  completion(.doNotDismiss)
79
85
 
80
- case ActionID.open:
81
- handleCarouselAction(response: response, userInfo: userInfo)
82
- completion(.dismissAndForwardAction)
83
-
84
- case UNNotificationDefaultActionIdentifier:
85
- // User tapped the notification body itself
86
- handleCarouselAction(response: response, userInfo: userInfo)
87
- completion(.dismissAndForwardAction)
86
+ case ActionID.open, UNNotificationDefaultActionIdentifier:
87
+ guard let carousel = carouselView else {
88
+ completion(.dismissAndForwardAction)
89
+ return
90
+ }
91
+ openDeepLink(
92
+ itemIndex: carousel.currentIndex,
93
+ itemRoute: carousel.currentItem?.route,
94
+ userInfo: userInfo
95
+ ) { opened in
96
+ if opened {
97
+ completion(.dismiss)
98
+ } else {
99
+ completion(.dismissAndForwardAction)
100
+ }
101
+ }
88
102
 
89
103
  default:
90
104
  completion(.dismissAndForwardAction)
@@ -115,16 +129,21 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
115
129
  let mode: CarouselView.CarouselMode = modeString == "auto" ? .auto : .manual
116
130
  carousel.configure(with: items, mode: mode, autoScrollInterval: interval / 1000.0)
117
131
 
118
- // Tap on carousel image/labels saves click data and opens the app
132
+ self.currentUserInfo = userInfo
133
+
134
+ // Tap on carousel image/labels opens the app via deep link URL (CleverTap pattern)
135
+ // Falls back to App Group + performNotificationDefaultAction if openURL fails
119
136
  carousel.onItemTapped = { [weak self] in
120
137
  guard let self = self, let cv = self.carouselView else { return }
121
- let messageId = userInfo["messageId"] as? String ?? ""
122
- self.saveClickData(
123
- messageId: messageId,
138
+ self.openDeepLink(
124
139
  itemIndex: cv.currentIndex,
125
- route: cv.currentItem?.route ?? ""
126
- )
127
- self.extensionContext?.performNotificationDefaultAction()
140
+ itemRoute: cv.currentItem?.route,
141
+ userInfo: userInfo
142
+ ) { opened in
143
+ if !opened {
144
+ self.extensionContext?.performNotificationDefaultAction()
145
+ }
146
+ }
128
147
  }
129
148
 
130
149
  self.carouselView = carousel
@@ -135,21 +154,64 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
135
154
  preferredContentSize = CGSize(width: width, height: width * imageAspectRatio + 80)
136
155
  }
137
156
 
138
- private func handleCarouselAction(response: UNNotificationResponse, userInfo: [AnyHashable: Any]) {
139
- guard let carousel = carouselView else { return }
140
-
157
+ // MARK: - Deep Link via URL (CleverTap pattern)
158
+
159
+ /// Try to open a deep link URL directly from the Content Extension.
160
+ /// If the push payload includes `deepLinkScheme`, constructs a URL like
161
+ /// `swanexample://product/123?swan_carousel=1&swan_comm_id=abc` and opens it.
162
+ /// On failure, falls back to saving click data to App Group.
163
+ ///
164
+ /// - Parameters:
165
+ /// - itemIndex: Current carousel item index
166
+ /// - itemRoute: Route from the tapped carousel item (e.g., "/product/123")
167
+ /// - userInfo: Push notification payload
168
+ /// - completion: Called with `true` if openURL succeeded, `false` if fell back
169
+ private func openDeepLink(
170
+ itemIndex: Int,
171
+ itemRoute: String?,
172
+ userInfo: [AnyHashable: Any],
173
+ completion: @escaping (Bool) -> Void
174
+ ) {
141
175
  let messageId = userInfo["messageId"] as? String ?? ""
142
- let currentItem = carousel.currentItem
143
-
144
- // Save click data to App Group for main app to read
145
- saveClickData(
146
- messageId: messageId,
147
- itemIndex: carousel.currentIndex,
148
- route: currentItem?.route ?? ""
149
- )
176
+ let route = itemRoute ?? userInfo["defaultRoute"] as? String ?? ""
177
+
178
+ // Always save click data first the async extensionContext.open() completion
179
+ // uses [weak self], so saveClickData() could be skipped if self is deallocated.
180
+ // Writing before the async call guarantees the data is persisted.
181
+ saveClickData(messageId: messageId, itemIndex: itemIndex, route: route)
182
+
183
+ // Try openURL if deepLinkScheme is provided
184
+ if let scheme = userInfo["deepLinkScheme"] as? String, !scheme.isEmpty, !route.isEmpty {
185
+ // Strip leading "/" from route
186
+ let path = route.hasPrefix("/") ? String(route.dropFirst()) : route
187
+
188
+ // Parse route to preserve any existing query params (e.g., /product/123?ref=push)
189
+ // Use a dummy scheme to make URLComponents parse it correctly
190
+ var components = URLComponents(string: "dummy://\(path)") ?? URLComponents()
191
+ components.scheme = scheme
192
+
193
+ // Merge swan_ tracking params with any existing query params from the route
194
+ var queryItems = components.queryItems ?? []
195
+ queryItems.append(contentsOf: [
196
+ URLQueryItem(name: "swan_carousel", value: "1"),
197
+ URLQueryItem(name: "swan_comm_id", value: messageId),
198
+ URLQueryItem(name: "swan_item_index", value: "\(itemIndex)"),
199
+ ])
200
+ components.queryItems = queryItems
201
+
202
+ if let url = components.url {
203
+ self.extensionContext?.open(url) { success in
204
+ completion(success)
205
+ }
206
+ return
207
+ }
208
+ }
209
+
210
+ completion(false)
150
211
  }
151
212
 
152
213
  // MARK: - Click Data Persistence
214
+ // Tested in SwanSDKTestable/Tests/SwanParsersTests.swift (ClickDataSerializationTests)
153
215
 
154
216
  private func saveClickData(messageId: String, itemIndex: Int, route: String) {
155
217
  let appGroupId = "group.swan.sdk.notifications"
@@ -218,6 +218,7 @@ class CarouselView: UIView {
218
218
  }
219
219
 
220
220
  // MARK: - Parsing
221
+ // Tested in SwanSDKTestable/Tests/SwanParsersTests.swift (CarouselParsingTests)
221
222
 
222
223
  static func parseItems(from jsonString: String) -> [CarouselItem] {
223
224
  guard let data = jsonString.data(using: .utf8),
@@ -278,6 +279,10 @@ extension CarouselView: UICollectionViewDataSource, UICollectionViewDelegate, UI
278
279
  return collectionView.bounds.size
279
280
  }
280
281
 
282
+ func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
283
+ onItemTapped?()
284
+ }
285
+
281
286
  func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
282
287
  let page = Int(scrollView.contentOffset.x / scrollView.bounds.width)
283
288
  if page != currentIndex {
@@ -164,6 +164,7 @@ class NotificationService: UNNotificationServiceExtension {
164
164
  }
165
165
 
166
166
  // MARK: - Credential Storage
167
+ // Tested in SwanSDKTestable/Tests/SwanParsersTests.swift (CredentialParsingTests)
167
168
 
168
169
  private struct SwanCredentials {
169
170
  let appId: String
@@ -8,7 +8,6 @@ var _reactNative = require("react-native");
8
8
  var _MaterialIcons = _interopRequireDefault(require("react-native-vector-icons/MaterialIcons"));
9
9
  var _jsxRuntime = require("react/jsx-runtime");
10
10
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
- // Install with `npm install react-native-vector-icons`
12
11
  const {
13
12
  width
14
13
  } = _reactNative.Dimensions.get('window'); // Screen dimensions for responsive layout
@@ -1 +1 @@
1
- {"version":3,"names":["_reactNative","require","_MaterialIcons","_interopRequireDefault","_jsxRuntime","e","__esModule","default","width","Dimensions","get","HeaderView","designConfig","onClose","onButtonClick","commId","themeBackgroundColor","crossButtonColor","backgroundImageURL","iconURL","title","titleColor","description","descriptionColor","primaryButtonSwitch","primaryButtonColor","primaryButtonFontColor","primaryButtonText","primaryButtonAction","primaryButtonActionType","secondaryButtonSwitch","secondaryButtonColor","secondaryButtonFontColor","secondaryButtonText","secondaryButtonAction","secondaryButtonActionType","renderButton","type","text","bgColor","fontColor","action","actionType","extraStyle","jsx","TouchableOpacity","style","styles","button","backgroundColor","onPress","event","children","Text","buttonText","color","numberOfLines","jsxs","View","notification","Image","source","uri","background","resizeMode","foreground","content","close","name","size","iconContainer","icon","textContainer","buttonContainer","marginRight","StyleSheet","create","overflow","position","flexDirection","top","left","elevation","shadowColor","shadowOffset","height","shadowOpacity","shadowRadius","zIndex","alignItems","padding","right","borderRadius","marginLeft","flex","fontWeight","fontSize","lineHeight","marginTop","marginHorizontal","marginBottom","justifyContent","_default","exports"],"sourceRoot":"../../../src","sources":["components/HeaderView.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["_reactNative","require","_MaterialIcons","_interopRequireDefault","_jsxRuntime","e","__esModule","default","width","Dimensions","get","HeaderView","designConfig","onClose","onButtonClick","commId","themeBackgroundColor","crossButtonColor","backgroundImageURL","iconURL","title","titleColor","description","descriptionColor","primaryButtonSwitch","primaryButtonColor","primaryButtonFontColor","primaryButtonText","primaryButtonAction","primaryButtonActionType","secondaryButtonSwitch","secondaryButtonColor","secondaryButtonFontColor","secondaryButtonText","secondaryButtonAction","secondaryButtonActionType","renderButton","type","text","bgColor","fontColor","action","actionType","extraStyle","jsx","TouchableOpacity","style","styles","button","backgroundColor","onPress","event","children","Text","buttonText","color","numberOfLines","jsxs","View","notification","Image","source","uri","background","resizeMode","foreground","content","close","name","size","iconContainer","icon","textContainer","buttonContainer","marginRight","StyleSheet","create","overflow","position","flexDirection","top","left","elevation","shadowColor","shadowOffset","height","shadowOpacity","shadowRadius","zIndex","alignItems","padding","right","borderRadius","marginLeft","flex","fontWeight","fontSize","lineHeight","marginTop","marginHorizontal","marginBottom","justifyContent","_default","exports"],"sourceRoot":"../../../src","sources":["components/HeaderView.tsx"],"mappings":"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","ignoreList":[]}
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "SharedCredentialsManager", {
11
11
  }
12
12
  });
13
13
  exports.SwanEcomSDK = void 0;
14
+ exports._resetClickDedup = _resetClickDedup;
14
15
  exports.createBackgroundMessageHandler = createBackgroundMessageHandler;
15
16
  exports.createForegroundMessageHandler = createForegroundMessageHandler;
16
17
  exports.createNotifeeBackgroundHandler = createNotifeeBackgroundHandler;
@@ -29,7 +30,6 @@ var _FullScreenView = _interopRequireDefault(require("./components/FullScreenVie
29
30
  var _react = require("react");
30
31
  var _reactNativeSqlite = _interopRequireDefault(require("react-native-sqlite-2"));
31
32
  var _reactNativeDeviceInfo = _interopRequireDefault(require("react-native-device-info"));
32
- var _geolocation = _interopRequireDefault(require("@react-native-community/geolocation"));
33
33
  var _Logger = _interopRequireDefault(require("./utils/Logger.js"));
34
34
  var _EventQueueManager = require("./core/EventQueueManager.js");
35
35
  var _FlushManager = require("./core/FlushManager.js");
@@ -125,6 +125,28 @@ function parseKeyValuePairs(data) {
125
125
  }
126
126
  }
127
127
 
128
+ /**
129
+ * Click deduplication guard.
130
+ * Prevents double click handling if both Firebase (onNotificationOpenedApp) and
131
+ * Notifee handlers fire for the same notification.
132
+ */
133
+ const processedClickIds = new Set();
134
+ const CLICK_ID_TTL_MS = 10_000; // Clear after 10 seconds
135
+
136
+ function markClickProcessed(id) {
137
+ if (processedClickIds.has(id)) {
138
+ return false; // Already processed
139
+ }
140
+ processedClickIds.add(id);
141
+ setTimeout(() => processedClickIds.delete(id), CLICK_ID_TTL_MS);
142
+ return true; // First time processing
143
+ }
144
+
145
+ /** @internal Test-only: reset click deduplication state */
146
+ function _resetClickDedup() {
147
+ processedClickIds.clear();
148
+ }
149
+
128
150
  /**
129
151
  * Notification deep link payload
130
152
  * Emitted when user clicks on a push notification
@@ -166,6 +188,7 @@ class SwanSDK {
166
188
  pendingPushEventListeners = [];
167
189
  appStateSubscription = null;
168
190
  linkingSubscription = null;
191
+ carouselClickSubscription = null;
169
192
 
170
193
  // Track if initial notification was already handled to prevent duplicate ACKs
171
194
  initialNotificationHandled = false;
@@ -743,6 +766,29 @@ class SwanSDK {
743
766
  return;
744
767
  }
745
768
 
769
+ // Detect iOS carousel click via URL (Content Extension openURL pattern)
770
+ // URL format: scheme://route?existing=params&swan_carousel=1&swan_comm_id=messageId&swan_item_index=0
771
+ if (parsed.params.swan_carousel === '1') {
772
+ const messageId = parsed.params.swan_comm_id;
773
+ _Logger.default.log('[SwanSDK] Carousel click via URL, route:', parsed.path, 'messageId:', messageId);
774
+ if (messageId) {
775
+ this.sendNotificationAck(messageId, 'clicked');
776
+ // Mark as processed so checkPendingCarouselClick() (App Group path)
777
+ // doesn't emit a duplicate NOTIFICATION_OPENED event
778
+ markClickProcessed(messageId);
779
+ }
780
+
781
+ // Reconstruct route with original query params (exclude swan_ tracking params)
782
+ const originalParams = Object.entries(parsed.params).filter(([key]) => !key.startsWith('swan_')).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&');
783
+ const route = originalParams ? `${parsed.path}?${originalParams}` : parsed.path;
784
+ const payload = {
785
+ route: route || undefined,
786
+ keyValuePairs: {}
787
+ };
788
+ this.emitNotificationOpened(payload);
789
+ return; // Handled — don't process as regular deep link
790
+ }
791
+
746
792
  // Extract swan_ prefixed parameters
747
793
  const swanParams = {};
748
794
  for (const [key, value] of Object.entries(parsed.params)) {
@@ -912,9 +958,16 @@ class SwanSDK {
912
958
  _Logger.default.log('[SwanSDK] Tracking initial app launch event...');
913
959
  this.appLaunched();
914
960
 
961
+ // Check for pending carousel click (covers killed/quit → fresh launch)
962
+ this.checkPendingCarouselClick();
963
+
915
964
  // AppState listener setup (always runs)
916
965
  _Logger.default.log('[SwanSDK] Setting up AppState listener...');
917
966
  this.setupAppStateListener();
967
+
968
+ // Listen for native carousel click events (covers foreground click case
969
+ // where AppState stays 'active' and polling never triggers)
970
+ this.setupCarouselClickListener();
918
971
  _Logger.default.log('Event queue system initialized successfully');
919
972
  } catch (error) {
920
973
  _Logger.default.error('Failed to initialize event queue:', error);
@@ -936,10 +989,47 @@ class SwanSDK {
936
989
  if (nextAppState === 'active') {
937
990
  _Logger.default.log('[SwanSDK] App came to foreground, tracking appLaunched event');
938
991
  this.appLaunched();
992
+ this.checkPendingCarouselClick();
939
993
  }
940
994
  });
941
995
  _Logger.default.log('[SwanSDK] AppState listener set up successfully');
942
996
  }
997
+
998
+ /**
999
+ * Listen for native carousel click events emitted via DeviceEventEmitter.
1000
+ * Covers the foreground-click case where AppState stays 'active'.
1001
+ */
1002
+ setupCarouselClickListener() {
1003
+ if (_reactNative.Platform.OS !== 'android') return;
1004
+ if (this.carouselClickSubscription) {
1005
+ this.carouselClickSubscription.remove();
1006
+ }
1007
+ this.carouselClickSubscription = _reactNative.DeviceEventEmitter.addListener('swanCarouselClick', clickData => {
1008
+ _Logger.default.log('[SwanSDK] Carousel click event from native:', JSON.stringify(clickData));
1009
+
1010
+ // Consume the pending click so polling doesn't fire again
1011
+ const {
1012
+ SwanNotificationModule
1013
+ } = _reactNative.NativeModules;
1014
+ SwanNotificationModule?.getPendingCarouselClick?.();
1015
+ const {
1016
+ messageId,
1017
+ route,
1018
+ title,
1019
+ body
1020
+ } = clickData;
1021
+ if (messageId) {
1022
+ this.sendNotificationAck(messageId, 'clicked');
1023
+ }
1024
+ const deepLinkPayload = {
1025
+ route: route || undefined,
1026
+ title: title || undefined,
1027
+ body: body || undefined,
1028
+ keyValuePairs: {}
1029
+ };
1030
+ this.emitNotificationOpened(deepLinkPayload);
1031
+ });
1032
+ }
943
1033
  createTable(tableName) {
944
1034
  return new Promise((resolve, reject) => {
945
1035
  if (!this.db) {
@@ -1423,7 +1513,8 @@ class SwanSDK {
1423
1513
  // This will trigger the Info.plist strings we discussed
1424
1514
  // It handles the 'Not Determined' -> 'Requested' flow automatically
1425
1515
  try {
1426
- _geolocation.default.requestAuthorization();
1516
+ const Geolocation = require('@react-native-community/geolocation').default;
1517
+ Geolocation.requestAuthorization();
1427
1518
  return true; // iOS handles the dialog; if denied, getCurrentPosition will trigger the error callback
1428
1519
  } catch (e) {
1429
1520
  return false;
@@ -1440,13 +1531,20 @@ class SwanSDK {
1440
1531
  * @param checkOnly - If true, only checks for existing permission without requesting (non-blocking)
1441
1532
  */
1442
1533
  async getDeviceLocation(checkOnly = false) {
1534
+ let Geolocation;
1535
+ try {
1536
+ Geolocation = require('@react-native-community/geolocation').default;
1537
+ } catch (e) {
1538
+ _Logger.default.warn('[SwanSDK] @react-native-community/geolocation is not installed, skipping location');
1539
+ return null;
1540
+ }
1443
1541
  const hasPermission = await this.hasLocationPermission(checkOnly);
1444
1542
  if (!hasPermission) {
1445
1543
  _Logger.default.log('[SwanSDK] Location permission not granted (checkOnly mode), skipping location');
1446
1544
  return null;
1447
1545
  }
1448
1546
  return new Promise(resolve => {
1449
- _geolocation.default.getCurrentPosition(position => {
1547
+ Geolocation.getCurrentPosition(position => {
1450
1548
  _Logger.default.log(position);
1451
1549
  const {
1452
1550
  latitude,
@@ -1705,6 +1803,72 @@ class SwanSDK {
1705
1803
  this.trackEvent(ECOM_EVENTS.APP_LAUNCHED, data || {});
1706
1804
  }
1707
1805
 
1806
+ /**
1807
+ * Check for a pending carousel click and emit NOTIFICATION_OPENED.
1808
+ * Native carousel clicks bypass Notifee, so the JS event system never fires.
1809
+ *
1810
+ * Android: reads from SwanNotificationModule.getPendingCarouselClick()
1811
+ * iOS: reads from App Group via SharedCredentialsManager.readTemplateClickData()
1812
+ * (Content Extension writes click data to App Group UserDefaults)
1813
+ *
1814
+ * @internal
1815
+ */
1816
+ async checkPendingCarouselClick() {
1817
+ try {
1818
+ let clickData = null;
1819
+ if (_reactNative.Platform.OS === 'android') {
1820
+ const {
1821
+ SwanNotificationModule
1822
+ } = _reactNative.NativeModules;
1823
+ if (!SwanNotificationModule?.getPendingCarouselClick) return;
1824
+ clickData = await SwanNotificationModule.getPendingCarouselClick();
1825
+ } else if (_reactNative.Platform.OS === 'ios') {
1826
+ // Read click data from App Group (written by Content Extension)
1827
+ clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
1828
+
1829
+ // If no data yet, retry after a short delay.
1830
+ // The Content Extension may still be writing when the app foregrounds.
1831
+ if (!clickData) {
1832
+ await new Promise(r => setTimeout(r, 500));
1833
+ clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
1834
+ }
1835
+ }
1836
+ if (!clickData) return;
1837
+ _Logger.default.log('[SwanSDK] Pending carousel click found:', JSON.stringify(clickData));
1838
+ const {
1839
+ messageId,
1840
+ route,
1841
+ title,
1842
+ body
1843
+ } = clickData;
1844
+
1845
+ // Deduplication: prevent double click handling.
1846
+ // On iOS, multiple handlers can fire for the same carousel tap
1847
+ // (Notifee foreground, Firebase onNotificationOpenedApp, AppState change).
1848
+ // Use the same markClickProcessed() as other handlers to prevent duplicates.
1849
+ if (messageId && !markClickProcessed(messageId)) {
1850
+ _Logger.default.log('[SwanSDK] Carousel click already processed for messageId:', messageId);
1851
+ return;
1852
+ }
1853
+
1854
+ // Send click ACK
1855
+ if (messageId) {
1856
+ this.sendNotificationAck(messageId, 'clicked');
1857
+ }
1858
+
1859
+ // Emit NOTIFICATION_OPENED event (same as Notifee click handler)
1860
+ const deepLinkPayload = {
1861
+ route: route || undefined,
1862
+ title: title || undefined,
1863
+ body: body || undefined,
1864
+ keyValuePairs: {}
1865
+ };
1866
+ this.emitNotificationOpened(deepLinkPayload);
1867
+ } catch (error) {
1868
+ _Logger.default.error('[SwanSDK] Error checking pending carousel click:', error);
1869
+ }
1870
+ }
1871
+
1708
1872
  /**
1709
1873
  * @param { { success: boolean } } data
1710
1874
  */
@@ -2709,7 +2873,7 @@ class SwanSDK {
2709
2873
  if (initialNotification && !this.initialNotificationHandled) {
2710
2874
  const messageId = initialNotification?.notification?.id;
2711
2875
  const notificationData = initialNotification.data || {};
2712
- if (messageId) {
2876
+ if (messageId && markClickProcessed(messageId)) {
2713
2877
  _Logger.default.log('[SwanSDK] App opened from Notifee notification tap:', messageId);
2714
2878
  this.initialNotificationHandled = true;
2715
2879
  await this.sendNotificationAck(messageId, 'clicked');
@@ -2750,13 +2914,17 @@ class SwanSDK {
2750
2914
  const messaging = require('@react-native-firebase/messaging').default;
2751
2915
  const initialNotification = await messaging().getInitialNotification();
2752
2916
  if (initialNotification && !this.initialNotificationHandled) {
2753
- _Logger.default.log('[SwanSDK] App opened from Firebase notification (killed state):', initialNotification.messageId);
2917
+ const messageId = initialNotification.messageId;
2918
+
2919
+ // Deduplication: prevent double click handling
2920
+ if (messageId && !markClickProcessed(messageId)) {
2921
+ _Logger.default.log('[SwanSDK] Firebase initial notification click already processed:', messageId);
2922
+ return;
2923
+ }
2924
+ _Logger.default.log('[SwanSDK] App opened from Firebase notification (killed state):', messageId);
2754
2925
 
2755
2926
  // Mark as handled to prevent duplicate ACKs
2756
2927
  this.initialNotificationHandled = true;
2757
-
2758
- // Get messageId and notification data
2759
- const messageId = initialNotification.messageId;
2760
2928
  const notificationData = initialNotification.data || {};
2761
2929
 
2762
2930
  // Extract deep link information
@@ -3398,6 +3566,21 @@ function createBackgroundMessageHandler() {
3398
3566
  return;
3399
3567
  }
3400
3568
 
3569
+ // If iOS displayed the notification natively (aps.alert present),
3570
+ // skip Notifee display to avoid duplicates. Just send delivery ACK.
3571
+ if (_reactNative.Platform.OS === 'ios' && remoteMessage?.notification) {
3572
+ _Logger.default.log('[SwanSDK] iOS native notification (aps.alert), skipping Notifee display');
3573
+ const sdkReady = sdkInstance && sdkInstance.isReady();
3574
+ if (sdkReady) {
3575
+ await sdkInstance.sendNotificationAck(messageId, 'delivered');
3576
+ } else if (messageId) {
3577
+ sendDirectNotificationAck(messageId, 'delivered').catch(err => {
3578
+ _Logger.default.log('[SwanSDK] Direct delivery ACK failed:', err);
3579
+ });
3580
+ }
3581
+ return;
3582
+ }
3583
+
3401
3584
  // Handle silent push (no UI display)
3402
3585
  if (isSilent) {
3403
3586
  console.log('[SwanSDK] Silent push received in background, skipping notification display');
@@ -3549,18 +3732,26 @@ function createNotificationOpenedHandler() {
3549
3732
  const messageId = event?.messageId;
3550
3733
  const notificationData = event?.data || {};
3551
3734
 
3552
- // For iOS carousel notifications, iOS displays the notification (not Notifee),
3553
- // so this handler MUST process the click regardless of NES status.
3554
- // For non-carousel, defer to Notifee click handlers unless NES is active.
3555
- const isIOSCarousel = _reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel';
3556
- if (!isIOSCarousel) {
3557
- const isNESActive = await _SharedCredentialsManager.SharedCredentialsManager.isNotificationServiceExtensionActive();
3558
- if (!isNESActive && _reactNative.Platform.OS === 'ios') {
3559
- _Logger.default.log('[SwanSDK] Non-carousel iOS without NES, deferring to Notifee click handler');
3560
- return;
3561
- }
3735
+ // Use custom messageId (from push payload data) for deduplication.
3736
+ // On iOS, both Firebase onNotificationOpenedApp AND Notifee event handlers
3737
+ // fire for the same notification tap (Content Extension performNotificationDefaultAction).
3738
+ // Firebase uses its own messageId, Notifee uses data.messageId they differ.
3739
+ // Using the custom messageId ensures both handlers use the same dedup key.
3740
+ const customMessageId = notificationData.messageId;
3741
+ const dedupId = customMessageId || messageId;
3742
+
3743
+ // Deduplication: prevent double click handling
3744
+ if (dedupId && !markClickProcessed(dedupId)) {
3745
+ _Logger.default.log('[SwanSDK] Click already processed for messageId:', dedupId);
3746
+ return;
3562
3747
  }
3563
3748
 
3749
+ // If onNotificationOpenedApp fires, it means iOS displayed the notification
3750
+ // natively (aps.alert present). Notifee handlers won't fire for iOS-native
3751
+ // notifications, so this handler MUST process the click.
3752
+ // For data-only messages (no aps.alert), Firebase never calls this handler.
3753
+ const isIOSCarousel = _reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel';
3754
+
3564
3755
  // Extract deep link information
3565
3756
  // For carousel notifications, the default route is in 'defaultRoute' field
3566
3757
  let route = notificationData.route || notificationData.defaultRoute;
@@ -3593,6 +3784,11 @@ function createNotificationOpenedHandler() {
3593
3784
  // Emit notificationOpened event for host app to handle
3594
3785
  sdkInstance.emitNotificationOpened(deepLinkPayload);
3595
3786
 
3787
+ // Send delivery ACK (background handler may not have fired for aps.alert notifications)
3788
+ if (messageId && _reactNative.Platform.OS === 'ios') {
3789
+ await sdkInstance.sendNotificationAck(messageId, 'delivered');
3790
+ }
3791
+
3596
3792
  // Send click ACK
3597
3793
  if (messageId) {
3598
3794
  await sdkInstance.sendNotificationAck(messageId, 'clicked');
@@ -3649,6 +3845,12 @@ function createNotifeeForegroundHandler() {
3649
3845
  const messageId = event?.detail?.notification?.data?.messageId;
3650
3846
  const notificationData = event?.detail?.notification?.data || {};
3651
3847
 
3848
+ // Deduplication: prevent double click handling
3849
+ if (messageId && !markClickProcessed(messageId)) {
3850
+ _Logger.default.log('[SwanSDK] Click already processed for messageId:', messageId);
3851
+ return;
3852
+ }
3853
+
3652
3854
  // Extract deep link information
3653
3855
  // Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
3654
3856
  // For carousel notifications, the default route is in 'defaultRoute' field
@@ -3733,6 +3935,12 @@ function createNotifeeBackgroundHandler() {
3733
3935
  const messageId = detail?.notification?.data?.messageId;
3734
3936
  const notificationData = detail?.notification?.data || {};
3735
3937
 
3938
+ // Deduplication: prevent double click handling
3939
+ if (messageId && !markClickProcessed(messageId)) {
3940
+ _Logger.default.log('[SwanSDK] Click already processed for messageId:', messageId);
3941
+ return;
3942
+ }
3943
+
3736
3944
  // Extract deep link information
3737
3945
  // Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
3738
3946
  // For carousel notifications, the default route is in 'defaultRoute' field