@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.
- package/android/src/main/kotlin/com/loyalytics/swan/SwanNotificationModule.kt +145 -1
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselAutoRemoteViews.kt +3 -22
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselFilmstripRemoteViews.kt +7 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselRemoteViews.kt +7 -0
- package/android/src/main/kotlin/com/loyalytics/swan/templates/carousel/CarouselTemplate.kt +141 -9
- package/android/src/main/res/layout/swan_carousel_auto_expanded.xml +1 -1
- package/android/src/main/res/layout/swan_carousel_expanded.xml +1 -1
- package/android/src/main/res/layout/swan_carousel_filmstrip_expanded.xml +1 -1
- package/ios/SwanAppGroup.m +55 -0
- package/ios/SwanNotificationContentExtension/NotificationViewController.swift +90 -28
- package/ios/SwanNotificationContentExtension/templates/CarouselView.swift +5 -0
- package/ios/SwanNotificationServiceExtension/NotificationService.swift +1 -0
- package/lib/commonjs/components/HeaderView.js +0 -1
- package/lib/commonjs/components/HeaderView.js.map +1 -1
- package/lib/commonjs/index.js +226 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/SharedCredentialsManager.js +46 -8
- package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/components/HeaderView.js +1 -1
- package/lib/module/components/HeaderView.js.map +1 -1
- package/lib/module/index.js +224 -17
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/SharedCredentialsManager.js +48 -9
- package/lib/module/utils/SharedCredentialsManager.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/src/components/HeaderView.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +19 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +6 -0
- package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/version.d.ts +1 -1
- package/lib/typescript/module/src/components/HeaderView.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +19 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +6 -0
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/package.json +23 -10
- package/scripts/setup-ios-extension.js +61 -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
|
-
*
|
|
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 (
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
self.saveClickData(
|
|
123
|
-
messageId: messageId,
|
|
138
|
+
self.openDeepLink(
|
|
124
139
|
itemIndex: cv.currentIndex,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
saveClickData(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 {
|
|
@@ -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":";;;;;;AAAA,IAAAA,YAAA,GAAAC,OAAA;
|
|
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":[]}
|
package/lib/commonjs/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
3553
|
-
//
|
|
3554
|
-
//
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
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
|