@loyalytics/swan-react-native-sdk 2.7.0 → 2.7.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 (27) hide show
  1. package/ios/SwanNotificationContentExtension/Info.plist +2 -0
  2. package/ios/SwanNotificationContentExtension/MainInterface.storyboard +1 -2
  3. package/ios/SwanNotificationContentExtension/NotificationViewController.swift +14 -5
  4. package/ios/SwanNotificationContentExtension/templates/CarouselView.swift +6 -11
  5. package/lib/commonjs/index.js +57 -5
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/utils/SharedCredentialsManager.js +26 -0
  8. package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -1
  9. package/lib/commonjs/version.js +1 -1
  10. package/lib/commonjs/version.js.map +1 -1
  11. package/lib/module/index.js +57 -5
  12. package/lib/module/index.js.map +1 -1
  13. package/lib/module/utils/SharedCredentialsManager.js +26 -0
  14. package/lib/module/utils/SharedCredentialsManager.js.map +1 -1
  15. package/lib/module/version.js +1 -1
  16. package/lib/module/version.js.map +1 -1
  17. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  18. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +13 -0
  19. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/src/version.d.ts +1 -1
  21. package/lib/typescript/commonjs/src/version.d.ts.map +1 -1
  22. package/lib/typescript/module/src/index.d.ts.map +1 -1
  23. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +13 -0
  24. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -1
  25. package/lib/typescript/module/src/version.d.ts +1 -1
  26. package/lib/typescript/module/src/version.d.ts.map +1 -1
  27. package/package.json +1 -1
@@ -30,6 +30,8 @@
30
30
  <real>0.7</real>
31
31
  <key>UNNotificationExtensionDefaultContentHidden</key>
32
32
  <false/>
33
+ <key>UNNotificationExtensionUserInteractionEnabled</key>
34
+ <true/>
33
35
  </dict>
34
36
  <key>NSExtensionPointIdentifier</key>
35
37
  <string>com.apple.usernotifications.content-extension</string>
@@ -1,6 +1,5 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="AppleSDK" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="jFH-bh-RYg">
3
- <device id="retina6_12" orientation="portrait" appearance="light"/>
2
+ <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17139" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="jFH-bh-RYg">
4
3
  <scenes>
5
4
  <scene sceneID="8ME-jv-LvP">
6
5
  <objects>
@@ -131,13 +131,14 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
131
131
 
132
132
  self.currentUserInfo = userInfo
133
133
 
134
- // Tap on carousel image/labels opens the app via deep link URL (CleverTap pattern)
134
+ // Tap on carousel image opens the app via deep link URL (CleverTap pattern)
135
135
  // Falls back to App Group + performNotificationDefaultAction if openURL fails
136
- carousel.onItemTapped = { [weak self] in
136
+ carousel.onItemTapped = { [weak self] tappedIndex in
137
137
  guard let self = self, let cv = self.carouselView else { return }
138
+ let tappedItem = tappedIndex < items.count ? items[tappedIndex] : nil
138
139
  self.openDeepLink(
139
- itemIndex: cv.currentIndex,
140
- itemRoute: cv.currentItem?.route,
140
+ itemIndex: tappedIndex,
141
+ itemRoute: tappedItem?.route,
141
142
  userInfo: userInfo
142
143
  ) { opened in
143
144
  if !opened {
@@ -172,7 +173,15 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
172
173
  completion: @escaping (Bool) -> Void
173
174
  ) {
174
175
  let messageId = userInfo["messageId"] as? String ?? ""
175
- let route = itemRoute ?? userInfo["defaultRoute"] as? String ?? ""
176
+ let defaultRoute = userInfo["defaultRoute"] as? String
177
+ // Use per-item route if non-nil and non-empty, otherwise fall back to defaultRoute.
178
+ // Logic mirrors SwanParsers.resolveRoute() — keep in sync.
179
+ let route: String
180
+ if let itemRoute = itemRoute, !itemRoute.isEmpty {
181
+ route = itemRoute
182
+ } else {
183
+ route = defaultRoute ?? ""
184
+ }
176
185
 
177
186
  // Always save click data first — the async extensionContext.open() completion
178
187
  // uses [weak self], so saveClickData() could be skipped if self is deallocated.
@@ -79,8 +79,8 @@ class CarouselView: UIView {
79
79
  return label
80
80
  }()
81
81
 
82
- /// Called when user taps a carousel item (image or label area)
83
- var onItemTapped: (() -> Void)?
82
+ /// Called when user taps a carousel image, passes the tapped item index
83
+ var onItemTapped: ((Int) -> Void)?
84
84
 
85
85
  // Image cache
86
86
  private var imageCache: [Int: UIImage] = [:]
@@ -109,10 +109,6 @@ class CarouselView: UIView {
109
109
  addSubview($0)
110
110
  }
111
111
 
112
- // Tap anywhere on carousel opens the app
113
- let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
114
- self.addGestureRecognizer(tap)
115
-
116
112
  NSLayoutConstraint.activate([
117
113
  collectionView.topAnchor.constraint(equalTo: topAnchor),
118
114
  collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
@@ -203,10 +199,6 @@ class CarouselView: UIView {
203
199
  autoScrollTimer = nil
204
200
  }
205
201
 
206
- @objc private func handleTap() {
207
- onItemTapped?()
208
- }
209
-
210
202
  // MARK: - Helpers
211
203
 
212
204
  private func updateLabels() {
@@ -280,7 +272,10 @@ extension CarouselView: UICollectionViewDataSource, UICollectionViewDelegate, UI
280
272
  }
281
273
 
282
274
  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
283
- onItemTapped?()
275
+ currentIndex = indexPath.item
276
+ pageControl.currentPage = indexPath.item
277
+ updateLabels()
278
+ onItemTapped?(indexPath.item)
284
279
  }
285
280
 
286
281
  func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
@@ -153,8 +153,17 @@ const directAckSentIds = new Set();
153
153
  function _resetClickDedup() {
154
154
  processedClickIds.clear();
155
155
  directAckSentIds.clear();
156
+ _lastCarouselClickHandledAt = 0;
156
157
  }
157
158
 
159
+ /**
160
+ * Timestamp of the last iOS carousel click handled by checkPendingCarouselClick().
161
+ * Used by createNotificationOpenedHandler to detect when checkPendingCarouselClick
162
+ * already consumed the one-time App Group click data, avoiding a duplicate
163
+ * NOTIFICATION_OPENED emission with the wrong (default) route.
164
+ */
165
+ let _lastCarouselClickHandledAt = 0;
166
+
158
167
  /**
159
168
  * Notification deep link payload
160
169
  * Emitted when user clicks on a push notification
@@ -1899,6 +1908,12 @@ class SwanSDK {
1899
1908
  return;
1900
1909
  }
1901
1910
 
1911
+ // Signal that we handled this iOS carousel click, so
1912
+ // createNotificationOpenedHandler can skip its duplicate emit.
1913
+ if (_reactNative.Platform.OS === 'ios') {
1914
+ _lastCarouselClickHandledAt = Date.now();
1915
+ }
1916
+
1902
1917
  // Send click ACK
1903
1918
  if (messageId) {
1904
1919
  this.sendNotificationAck(messageId, 'clicked');
@@ -3831,16 +3846,53 @@ function createNotificationOpenedHandler() {
3831
3846
  // For carousel notifications, the default route is in 'defaultRoute' field
3832
3847
  let route = notificationData.route || notificationData.defaultRoute;
3833
3848
 
3834
- // For carousel on iOS, check for per-item route from Content Extension
3849
+ // For carousel on iOS, Content Extension writes per-item click data to
3850
+ // App Group. Two handlers race to read it:
3851
+ // 1. checkPendingCarouselClick() — fires on AppState 'active'
3852
+ // 2. This handler — fires on Firebase onNotificationOpenedApp
3853
+ //
3854
+ // readTemplateClickData() is destructive (clears after read), so the
3855
+ // second reader gets null and falls back to defaultRoute — wrong.
3856
+ //
3857
+ // Fix: peek (non-destructive) to check if data exists. If it does,
3858
+ // defer to checkPendingCarouselClick which has exponential backoff
3859
+ // and is the canonical iOS carousel click handler.
3860
+ // If no data: either already consumed or simple tap (no Content Extension).
3835
3861
  if (isIOSCarousel) {
3836
3862
  try {
3837
- const clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
3863
+ const clickData = await _SharedCredentialsManager.SharedCredentialsManager.peekTemplateClickData();
3838
3864
  if (clickData?.route) {
3839
- _Logger.default.log('[SwanSDK] Carousel item route from Content Extension:', clickData.route);
3840
- route = clickData.route;
3865
+ // Content Extension click data exists checkPendingCarouselClick
3866
+ // will read it (with backoff) and emit NOTIFICATION_OPENED.
3867
+ // Send ACKs here too as safety net: if app is already foregrounded,
3868
+ // AppState won't transition to 'active' so checkPendingCarouselClick
3869
+ // may never fire.
3870
+ _Logger.default.log('[SwanSDK] iOS carousel click data found, deferring to checkPendingCarouselClick');
3871
+ if (messageId) {
3872
+ await sdkInstance.sendNotificationAck(messageId, 'delivered');
3873
+ }
3874
+ if (messageId) {
3875
+ await sdkInstance.sendNotificationAck(messageId, 'clicked');
3876
+ }
3877
+ return;
3878
+ }
3879
+
3880
+ // No click data from peek. Either:
3881
+ // (a) checkPendingCarouselClick already consumed it
3882
+ // (b) Simple tap — no Content Extension interaction
3883
+ // Brief wait then check if (a).
3884
+ await new Promise(r => setTimeout(r, 300));
3885
+ if (Date.now() - _lastCarouselClickHandledAt < 5000) {
3886
+ _Logger.default.log('[SwanSDK] iOS carousel click already handled by checkPendingCarouselClick');
3887
+ if (messageId) {
3888
+ await sdkInstance.sendNotificationAck(messageId, 'clicked');
3889
+ }
3890
+ return;
3841
3891
  }
3892
+ // Case (b): simple tap, proceed with defaultRoute below
3893
+ _Logger.default.log('[SwanSDK] iOS carousel simple tap, using defaultRoute:', route);
3842
3894
  } catch (err) {
3843
- _Logger.default.warn('[SwanSDK] Failed to read Content Extension click data:', err);
3895
+ _Logger.default.warn('[SwanSDK] Failed to peek Content Extension click data:', err);
3844
3896
  }
3845
3897
  }
3846
3898
  const deepLinkPayload = {