@loyalytics/swan-react-native-sdk 2.5.1-beta.4 → 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.
- package/lib/commonjs/index.js +100 -66
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/index.js +100 -66
- 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
package/lib/commonjs/index.js
CHANGED
|
@@ -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 =
|
|
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
|
/**
|
|
@@ -2898,27 +2906,47 @@ class SwanSDK {
|
|
|
2898
2906
|
|
|
2899
2907
|
// Check if app was opened by tapping a Notifee-displayed notification (data-only architecture)
|
|
2900
2908
|
const initialNotification = await notifee.getInitialNotification();
|
|
2909
|
+
_Logger.default.log('[SwanSDK] getInitialNotification result:', JSON.stringify(initialNotification, null, 2));
|
|
2901
2910
|
if (initialNotification && !this.initialNotificationHandled) {
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
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
|
+
}
|
|
2908
2949
|
}
|
|
2909
|
-
// Extract deep link information
|
|
2910
|
-
// For carousel notifications, the default route is in 'defaultRoute' field
|
|
2911
|
-
const resolvedRoute = notificationData.route || notificationData.defaultRoute;
|
|
2912
|
-
const deepLinkPayload = {
|
|
2913
|
-
...notificationData,
|
|
2914
|
-
route: resolvedRoute,
|
|
2915
|
-
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
2916
|
-
title: initialNotification.notification?.title || notificationData.title,
|
|
2917
|
-
body: initialNotification.notification?.body || notificationData.body
|
|
2918
|
-
};
|
|
2919
|
-
|
|
2920
|
-
// Emit notificationOpened event for host app to handle deep linking
|
|
2921
|
-
this.emitNotificationOpened(deepLinkPayload);
|
|
2922
2950
|
}
|
|
2923
2951
|
|
|
2924
2952
|
// Also check Firebase's getInitialNotification for iOS auto-displayed notifications
|
|
@@ -2942,7 +2970,21 @@ class SwanSDK {
|
|
|
2942
2970
|
const messaging = require('@react-native-firebase/messaging').default;
|
|
2943
2971
|
const initialNotification = await messaging().getInitialNotification();
|
|
2944
2972
|
if (initialNotification && !this.initialNotificationHandled) {
|
|
2945
|
-
|
|
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
|
+
}
|
|
2946
2988
|
|
|
2947
2989
|
// Deduplication: prevent double click handling
|
|
2948
2990
|
if (messageId && !markClickProcessed(messageId)) {
|
|
@@ -2951,12 +2993,7 @@ class SwanSDK {
|
|
|
2951
2993
|
}
|
|
2952
2994
|
_Logger.default.log('[SwanSDK] App opened from Firebase notification (killed state):', messageId);
|
|
2953
2995
|
|
|
2954
|
-
// Mark as handled to prevent duplicate ACKs
|
|
2955
|
-
this.initialNotificationHandled = true;
|
|
2956
|
-
const notificationData = initialNotification.data || {};
|
|
2957
|
-
|
|
2958
2996
|
// Extract deep link information
|
|
2959
|
-
// For carousel notifications, the default route is in 'defaultRoute' field
|
|
2960
2997
|
const resolvedRoute = notificationData.route || notificationData.defaultRoute;
|
|
2961
2998
|
const deepLinkPayload = {
|
|
2962
2999
|
...notificationData,
|
|
@@ -2966,13 +3003,11 @@ class SwanSDK {
|
|
|
2966
3003
|
body: initialNotification.notification?.body || notificationData.body
|
|
2967
3004
|
};
|
|
2968
3005
|
|
|
2969
|
-
// Emit
|
|
3006
|
+
// Emit deep link FIRST, then ACK
|
|
2970
3007
|
this.emitNotificationOpened(deepLinkPayload);
|
|
2971
|
-
|
|
2972
|
-
// Send click ACK
|
|
2973
|
-
if (messageId) {
|
|
3008
|
+
if (messageId && !directAckSentIds.has(messageId)) {
|
|
2974
3009
|
await this.sendNotificationAck(messageId, 'clicked');
|
|
2975
|
-
_Logger.default.log('[SwanSDK]
|
|
3010
|
+
_Logger.default.log('[SwanSDK] Firebase initial notification click ACK sent');
|
|
2976
3011
|
}
|
|
2977
3012
|
}
|
|
2978
3013
|
} catch (error) {
|
|
@@ -3824,7 +3859,7 @@ function createNotificationOpenedHandler() {
|
|
|
3824
3859
|
_Logger.default.warn('[SwanSDK] No messageId in notification data, skipping click ACK');
|
|
3825
3860
|
}
|
|
3826
3861
|
} catch (error) {
|
|
3827
|
-
_Logger.default.error('[SwanSDK] Error handling
|
|
3862
|
+
_Logger.default.error('[SwanSDK] Error handling notification opened event:', error);
|
|
3828
3863
|
}
|
|
3829
3864
|
};
|
|
3830
3865
|
}
|
|
@@ -3962,41 +3997,8 @@ function createNotifeeBackgroundHandler() {
|
|
|
3962
3997
|
// Get messageId and notification data
|
|
3963
3998
|
const messageId = detail?.notification?.data?.messageId;
|
|
3964
3999
|
const notificationData = detail?.notification?.data || {};
|
|
3965
|
-
|
|
3966
|
-
// Deduplication: prevent double click handling
|
|
3967
|
-
if (messageId && !markClickProcessed(messageId)) {
|
|
3968
|
-
_Logger.default.log('[SwanSDK] Click already processed for messageId:', messageId);
|
|
3969
|
-
return;
|
|
3970
|
-
}
|
|
3971
|
-
|
|
3972
|
-
// Extract deep link information
|
|
3973
|
-
// Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
|
|
3974
|
-
// For carousel notifications, the default route is in 'defaultRoute' field
|
|
3975
|
-
let route = notificationData.route || notificationData.defaultRoute;
|
|
3976
|
-
|
|
3977
|
-
// For carousel notifications on iOS, the Content Extension saves per-item
|
|
3978
|
-
// click data (including item-specific route) to the App Group.
|
|
3979
|
-
if (_reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel') {
|
|
3980
|
-
try {
|
|
3981
|
-
const clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
|
|
3982
|
-
if (clickData?.route) {
|
|
3983
|
-
_Logger.default.log('[SwanSDK] Background: carousel item route from Content Extension:', clickData.route);
|
|
3984
|
-
route = clickData.route;
|
|
3985
|
-
}
|
|
3986
|
-
} catch (err) {
|
|
3987
|
-
_Logger.default.warn('[SwanSDK] Failed to read Content Extension click data:', err);
|
|
3988
|
-
}
|
|
3989
|
-
}
|
|
3990
|
-
const deepLinkPayload = {
|
|
3991
|
-
...notificationData,
|
|
3992
|
-
route,
|
|
3993
|
-
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
3994
|
-
title: detail?.notification?.title,
|
|
3995
|
-
body: detail?.notification?.body
|
|
3996
|
-
};
|
|
3997
4000
|
_Logger.default.log('[SwanSDK] Background notification clicked:', {
|
|
3998
4001
|
messageId,
|
|
3999
|
-
route: deepLinkPayload.route,
|
|
4000
4002
|
type: notificationData.notificationType
|
|
4001
4003
|
});
|
|
4002
4004
|
|
|
@@ -4004,10 +4006,39 @@ function createNotifeeBackgroundHandler() {
|
|
|
4004
4006
|
const sdkInstance = SwanSDK.getCurrentInstance();
|
|
4005
4007
|
const isSDKReady = sdkInstance && sdkInstance.isReady();
|
|
4006
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
|
+
}
|
|
4007
4014
|
if (!sdkInstance.isPushEnabled()) {
|
|
4008
4015
|
return;
|
|
4009
4016
|
}
|
|
4010
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
|
+
|
|
4011
4042
|
// Emit notificationOpened event for host app to handle
|
|
4012
4043
|
sdkInstance.emitNotificationOpened(deepLinkPayload);
|
|
4013
4044
|
_Logger.default.log('[SwanSDK] Background: emitted notificationOpened with route:', deepLinkPayload.route);
|
|
@@ -4020,9 +4051,12 @@ function createNotifeeBackgroundHandler() {
|
|
|
4020
4051
|
return;
|
|
4021
4052
|
}
|
|
4022
4053
|
|
|
4023
|
-
// SDK not ready (app was killed)
|
|
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.
|
|
4024
4057
|
_Logger.default.log('[SwanSDK] SDK not ready, sending click ACK via direct fetch');
|
|
4025
4058
|
if (messageId) {
|
|
4059
|
+
directAckSentIds.add(messageId);
|
|
4026
4060
|
await sendDirectNotificationAck(messageId, 'clicked');
|
|
4027
4061
|
}
|
|
4028
4062
|
} catch (error) {
|