@loyalytics/swan-react-native-sdk 2.7.1-beta.2 → 2.7.1-beta.3
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.
- package/ios/SwanNotificationContentExtension/NotificationViewController.swift +50 -84
- package/ios/SwanNotificationContentExtension/templates/CarouselView.swift +1 -0
- package/lib/commonjs/index.js +6 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/version.d.ts +1 -1
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -12,8 +12,8 @@ import UserNotificationsUI
|
|
|
12
12
|
*
|
|
13
13
|
* User interaction:
|
|
14
14
|
* - Swipe left/right on the carousel to navigate between items
|
|
15
|
-
* - Tap on
|
|
16
|
-
* -
|
|
15
|
+
* - Tap on a carousel image to save click data to App Group and open the app
|
|
16
|
+
* - SDK's checkPendingCarouselClick() reads the click data and handles navigation
|
|
17
17
|
*
|
|
18
18
|
* Action button identifiers (reserved for future use if categories are registered):
|
|
19
19
|
* - swan_prev: Navigate to previous carousel item
|
|
@@ -23,7 +23,6 @@ import UserNotificationsUI
|
|
|
23
23
|
class NotificationViewController: UIViewController, UNNotificationContentExtension {
|
|
24
24
|
|
|
25
25
|
private var carouselView: CarouselView?
|
|
26
|
-
private var currentUserInfo: [AnyHashable: Any] = [:]
|
|
27
26
|
|
|
28
27
|
/// Action button identifiers (reserved for future use if notification categories are registered)
|
|
29
28
|
private enum ActionID {
|
|
@@ -32,6 +31,10 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
|
|
|
32
31
|
static let open = "swan_open"
|
|
33
32
|
}
|
|
34
33
|
|
|
34
|
+
/// Set to true after onItemTapped saves click data, so didReceive doesn't overwrite
|
|
35
|
+
/// with a potentially stale currentIndex (e.g., if auto-scroll advanced between tap and callback).
|
|
36
|
+
private var tapHandled = false
|
|
37
|
+
|
|
35
38
|
required init?(coder: NSCoder) {
|
|
36
39
|
self.carouselView = nil
|
|
37
40
|
super.init(coder: coder)
|
|
@@ -84,21 +87,16 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
|
|
|
84
87
|
completion(.doNotDismiss)
|
|
85
88
|
|
|
86
89
|
case ActionID.open, UNNotificationDefaultActionIdentifier:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
) { opened in
|
|
96
|
-
if opened {
|
|
97
|
-
completion(.dismiss)
|
|
98
|
-
} else {
|
|
99
|
-
completion(.dismissAndForwardAction)
|
|
100
|
-
}
|
|
90
|
+
// If onItemTapped already saved click data, skip to avoid overwriting
|
|
91
|
+
// with a potentially stale currentIndex (auto-scroll race).
|
|
92
|
+
if !tapHandled, let carousel = carouselView {
|
|
93
|
+
handleCarouselTap(
|
|
94
|
+
itemIndex: carousel.currentIndex,
|
|
95
|
+
itemRoute: carousel.currentItem?.route,
|
|
96
|
+
userInfo: userInfo
|
|
97
|
+
)
|
|
101
98
|
}
|
|
99
|
+
completion(.dismissAndForwardAction)
|
|
102
100
|
|
|
103
101
|
default:
|
|
104
102
|
completion(.dismissAndForwardAction)
|
|
@@ -129,22 +127,18 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
|
|
|
129
127
|
let mode: CarouselView.CarouselMode = modeString == "auto" ? .auto : .manual
|
|
130
128
|
carousel.configure(with: items, mode: mode, autoScrollInterval: interval / 1000.0)
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Tap on carousel image opens the app via deep link URL (CleverTap pattern)
|
|
135
|
-
// Falls back to App Group + performNotificationDefaultAction if openURL fails
|
|
130
|
+
// Tap on carousel image: save click data to App Group, then open the app.
|
|
131
|
+
// SDK's checkPendingCarouselClick() reads the data and handles navigation.
|
|
136
132
|
carousel.onItemTapped = { [weak self] tappedIndex in
|
|
137
|
-
guard let self = self
|
|
133
|
+
guard let self = self else { return }
|
|
138
134
|
let tappedItem = tappedIndex < items.count ? items[tappedIndex] : nil
|
|
139
|
-
self.
|
|
135
|
+
self.handleCarouselTap(
|
|
140
136
|
itemIndex: tappedIndex,
|
|
141
137
|
itemRoute: tappedItem?.route,
|
|
142
138
|
userInfo: userInfo
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
}
|
|
139
|
+
)
|
|
140
|
+
self.tapHandled = true
|
|
141
|
+
self.extensionContext?.performNotificationDefaultAction()
|
|
148
142
|
}
|
|
149
143
|
|
|
150
144
|
self.carouselView = carousel
|
|
@@ -155,74 +149,44 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
|
|
|
155
149
|
preferredContentSize = CGSize(width: width, height: width * imageAspectRatio + 80)
|
|
156
150
|
}
|
|
157
151
|
|
|
158
|
-
// MARK: -
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
///
|
|
162
|
-
///
|
|
163
|
-
///
|
|
164
|
-
///
|
|
165
|
-
///
|
|
166
|
-
|
|
167
|
-
/// - userInfo: Push notification payload
|
|
168
|
-
/// - completion: Called with `true` if openURL succeeded, `false` if fell back
|
|
169
|
-
private func openDeepLink(
|
|
152
|
+
// MARK: - Click Data Persistence
|
|
153
|
+
// Tested in SwanSDKTestable/Tests/SwanParsersTests.swift (ClickDataSerializationTests, RouteResolutionTests)
|
|
154
|
+
|
|
155
|
+
/// Save carousel click data to App Group for the SDK to read.
|
|
156
|
+
/// The SDK's checkPendingCarouselClick() reads this data on app open and
|
|
157
|
+
/// handles navigation + click ACK. This avoids extensionContext.open(url)
|
|
158
|
+
/// race conditions on cold start where the app hasn't finished initializing
|
|
159
|
+
/// React Navigation before the URL arrives.
|
|
160
|
+
private func handleCarouselTap(
|
|
170
161
|
itemIndex: Int,
|
|
171
162
|
itemRoute: String?,
|
|
172
|
-
userInfo: [AnyHashable: Any]
|
|
173
|
-
completion: @escaping (Bool) -> Void
|
|
163
|
+
userInfo: [AnyHashable: Any]
|
|
174
164
|
) {
|
|
175
|
-
|
|
176
|
-
let
|
|
177
|
-
|
|
165
|
+
// messageId: try data field first, then FCM's auto-generated key
|
|
166
|
+
let messageId = (userInfo["messageId"] as? String)
|
|
167
|
+
?? (userInfo["gcm.message_id"] as? String)
|
|
168
|
+
?? ""
|
|
169
|
+
// Use per-item route if non-nil and non-empty, otherwise fall back to defaultRoute/route.
|
|
178
170
|
// Logic mirrors SwanParsers.resolveRoute() — keep in sync.
|
|
179
171
|
let route: String
|
|
180
172
|
if let itemRoute = itemRoute, !itemRoute.isEmpty {
|
|
181
173
|
route = itemRoute
|
|
182
174
|
} else {
|
|
183
|
-
route = defaultRoute
|
|
175
|
+
route = (userInfo["defaultRoute"] as? String)
|
|
176
|
+
?? (userInfo["route"] as? String)
|
|
177
|
+
?? ""
|
|
184
178
|
}
|
|
185
179
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
// If changing this logic, update SwanParsers.swift too (and vice versa)
|
|
194
|
-
if !route.isEmpty, route.contains("://") {
|
|
195
|
-
guard var components = URLComponents(string: route) else {
|
|
196
|
-
completion(false)
|
|
197
|
-
return
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Merge swan_ tracking params with any existing query params from the route
|
|
201
|
-
var queryItems = components.queryItems ?? []
|
|
202
|
-
queryItems.append(contentsOf: [
|
|
203
|
-
URLQueryItem(name: "swan_carousel", value: "1"),
|
|
204
|
-
URLQueryItem(name: "swan_comm_id", value: messageId),
|
|
205
|
-
URLQueryItem(name: "swan_item_index", value: "\(itemIndex)"),
|
|
206
|
-
])
|
|
207
|
-
components.queryItems = queryItems
|
|
208
|
-
|
|
209
|
-
if let url = components.url {
|
|
210
|
-
self.extensionContext?.open(url) { success in
|
|
211
|
-
DispatchQueue.main.async {
|
|
212
|
-
completion(success)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
completion(false)
|
|
180
|
+
saveClickData(
|
|
181
|
+
messageId: messageId,
|
|
182
|
+
itemIndex: itemIndex,
|
|
183
|
+
route: route,
|
|
184
|
+
title: userInfo["title"] as? String ?? "",
|
|
185
|
+
body: userInfo["body"] as? String ?? ""
|
|
186
|
+
)
|
|
220
187
|
}
|
|
221
188
|
|
|
222
|
-
|
|
223
|
-
// Tested in SwanSDKTestable/Tests/SwanParsersTests.swift (ClickDataSerializationTests)
|
|
224
|
-
|
|
225
|
-
private func saveClickData(messageId: String, itemIndex: Int, route: String) {
|
|
189
|
+
private func saveClickData(messageId: String, itemIndex: Int, route: String, title: String, body: String) {
|
|
226
190
|
let appGroupId = "group.swan.sdk.notifications"
|
|
227
191
|
guard let defaults = UserDefaults(suiteName: appGroupId) else { return }
|
|
228
192
|
|
|
@@ -230,6 +194,8 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
|
|
|
230
194
|
"messageId": messageId,
|
|
231
195
|
"itemIndex": itemIndex,
|
|
232
196
|
"route": route,
|
|
197
|
+
"title": title,
|
|
198
|
+
"body": body,
|
|
233
199
|
"timestamp": Date().timeIntervalSince1970,
|
|
234
200
|
]
|
|
235
201
|
|
|
@@ -279,6 +279,7 @@ extension CarouselView: UICollectionViewDataSource, UICollectionViewDelegate, UI
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
282
|
+
guard scrollView.bounds.width > 0 else { return }
|
|
282
283
|
let page = Int(scrollView.contentOffset.x / scrollView.bounds.width)
|
|
283
284
|
if page != currentIndex {
|
|
284
285
|
currentIndex = page
|
package/lib/commonjs/index.js
CHANGED
|
@@ -1891,6 +1891,12 @@ class SwanSDK {
|
|
|
1891
1891
|
}
|
|
1892
1892
|
}
|
|
1893
1893
|
if (!clickData) return;
|
|
1894
|
+
|
|
1895
|
+
// Discard stale click data (>60s old) from a previous session
|
|
1896
|
+
if (clickData.timestamp && Date.now() / 1000 - clickData.timestamp > 60) {
|
|
1897
|
+
_Logger.default.log('[SwanSDK] Stale carousel click data (>60s old), discarding');
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1894
1900
|
_Logger.default.log('[SwanSDK] Pending carousel click found:', JSON.stringify(clickData));
|
|
1895
1901
|
const {
|
|
1896
1902
|
messageId,
|