@loyalytics/swan-react-native-sdk 2.5.1-beta.5 → 2.5.1-beta.8
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 +127 -84
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/index.js +127 -84
- 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
|
/**
|
|
@@ -630,22 +638,32 @@ class SwanSDK {
|
|
|
630
638
|
}
|
|
631
639
|
this.listeners[event].push(callback);
|
|
632
640
|
|
|
633
|
-
// On
|
|
634
|
-
//
|
|
635
|
-
|
|
636
|
-
|
|
641
|
+
// On every NOTIFICATION_OPENED listener registration, check for buffered notifications.
|
|
642
|
+
// Runs on cold start (process dead → getInitialNotification buffered), warm restart
|
|
643
|
+
// (process alive but Activity destroyed by swipe-from-recents on Android), and
|
|
644
|
+
// Metro hot-reload (component unmount/remount cycles).
|
|
645
|
+
if (event === SwanSDK.EVENTS.NOTIFICATION_OPENED) {
|
|
637
646
|
// Defer to next microtask so the listener is fully registered before events emit
|
|
638
647
|
Promise.resolve().then(() => {
|
|
639
|
-
// Deliver buffered standard/image push notification
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
648
|
+
// Deliver buffered standard/image push notification.
|
|
649
|
+
// Skip carousel — checkPendingCarouselClick() handles those with per-item routes.
|
|
650
|
+
if (this.pendingNotificationPayload) {
|
|
651
|
+
const payload = this.pendingNotificationPayload;
|
|
652
|
+
this.pendingNotificationPayload = null;
|
|
653
|
+
if (payload.notificationType !== 'carousel') {
|
|
654
|
+
this.emit(SwanSDK.EVENTS.NOTIFICATION_OPENED, payload);
|
|
655
|
+
this.emitDeepLinkOpened({
|
|
656
|
+
...payload,
|
|
657
|
+
source: 'push'
|
|
658
|
+
});
|
|
659
|
+
}
|
|
646
660
|
}
|
|
647
|
-
// Check for pending carousel click (native side holds data until consumed)
|
|
661
|
+
// Check for pending carousel click (native side holds data until consumed).
|
|
662
|
+
// Unconditional: handles both cold start and warm restart carousel taps.
|
|
648
663
|
this.checkPendingCarouselClick();
|
|
664
|
+
if (!this.coldStartCheckDone) {
|
|
665
|
+
this.coldStartCheckDone = true;
|
|
666
|
+
}
|
|
649
667
|
});
|
|
650
668
|
}
|
|
651
669
|
|
|
@@ -675,9 +693,11 @@ class SwanSDK {
|
|
|
675
693
|
*/
|
|
676
694
|
emitNotificationOpened(payload) {
|
|
677
695
|
const listeners = this.listeners[SwanSDK.EVENTS.NOTIFICATION_OPENED];
|
|
678
|
-
if (
|
|
679
|
-
//
|
|
680
|
-
//
|
|
696
|
+
if (!listeners || listeners.length === 0) {
|
|
697
|
+
// No listeners yet — buffer for delivery when first listener registers.
|
|
698
|
+
// Handles: true cold start (process dead), Activity recreation (process alive
|
|
699
|
+
// but Activity destroyed by swipe-from-recents on Android), and Metro
|
|
700
|
+
// hot-reload (component unmount/remount cycles).
|
|
681
701
|
_Logger.default.log('[SwanSDK] Buffering notification payload for deferred delivery');
|
|
682
702
|
this.pendingNotificationPayload = payload;
|
|
683
703
|
return;
|
|
@@ -2897,31 +2917,48 @@ class SwanSDK {
|
|
|
2897
2917
|
// similar to how Firebase requires onMessage() at module level.
|
|
2898
2918
|
|
|
2899
2919
|
// Check if app was opened by tapping a Notifee-displayed notification (data-only architecture)
|
|
2900
|
-
// Notifee's getInitialNotification() returns { notification, pressAction }.
|
|
2901
|
-
// The FCM data we stored via displayNotification({ data: {...} }) lives at
|
|
2902
|
-
// notification.data — NOT at the top-level .data (which doesn't exist).
|
|
2903
2920
|
const initialNotification = await notifee.getInitialNotification();
|
|
2921
|
+
_Logger.default.log('[SwanSDK] getInitialNotification result:', JSON.stringify(initialNotification, null, 2));
|
|
2904
2922
|
if (initialNotification && !this.initialNotificationHandled) {
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2923
|
+
// Notifee returned an initial notification — mark as handled
|
|
2924
|
+
// unconditionally to prevent Firebase fallback from double-emitting.
|
|
2925
|
+
this.initialNotificationHandled = true;
|
|
2926
|
+
if (!this.isPushEnabled()) {
|
|
2927
|
+
_Logger.default.log('[SwanSDK] Push notifications disabled, ignoring initial notification');
|
|
2928
|
+
} else {
|
|
2929
|
+
const notificationData = initialNotification.notification?.data || {};
|
|
2930
|
+
const messageId = notificationData.messageId;
|
|
2931
|
+
|
|
2932
|
+
// Carousel notifications: skip standard path entirely.
|
|
2933
|
+
// checkPendingCarouselClick() (triggered by addListener) handles
|
|
2934
|
+
// per-item routes from native storage / App Group.
|
|
2935
|
+
if (notificationData.notificationType === 'carousel') {
|
|
2936
|
+
_Logger.default.log('[SwanSDK] Carousel initial notification, deferring to checkPendingCarouselClick');
|
|
2937
|
+
} else if (messageId && !markClickProcessed(messageId)) {
|
|
2938
|
+
_Logger.default.log('[SwanSDK] Click already processed for messageId:', messageId);
|
|
2939
|
+
} else {
|
|
2940
|
+
_Logger.default.log('[SwanSDK] App opened from Notifee notification tap:', messageId);
|
|
2941
|
+
|
|
2942
|
+
// Extract deep link information
|
|
2943
|
+
const resolvedRoute = notificationData.route || notificationData.defaultRoute;
|
|
2944
|
+
const deepLinkPayload = {
|
|
2945
|
+
...notificationData,
|
|
2946
|
+
route: resolvedRoute,
|
|
2947
|
+
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
2948
|
+
title: initialNotification.notification?.title || notificationData.title,
|
|
2949
|
+
body: initialNotification.notification?.body || notificationData.body
|
|
2950
|
+
};
|
|
2951
|
+
|
|
2952
|
+
// Emit deep link FIRST to ensure delivery even if ACK fails
|
|
2953
|
+
_Logger.default.log('[SwanSDK] Cold-start deepLinkPayload:', JSON.stringify(deepLinkPayload, null, 2));
|
|
2954
|
+
this.emitNotificationOpened(deepLinkPayload);
|
|
2955
|
+
|
|
2956
|
+
// Send click ACK after emit (skip if background handler already sent one)
|
|
2957
|
+
if (messageId && !directAckSentIds.has(messageId)) {
|
|
2958
|
+
await this.sendNotificationAck(messageId, 'clicked');
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2911
2961
|
}
|
|
2912
|
-
// Extract deep link information
|
|
2913
|
-
// For carousel notifications, the default route is in 'defaultRoute' field
|
|
2914
|
-
const resolvedRoute = notificationData.route || notificationData.defaultRoute;
|
|
2915
|
-
const deepLinkPayload = {
|
|
2916
|
-
...notificationData,
|
|
2917
|
-
route: resolvedRoute,
|
|
2918
|
-
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
2919
|
-
title: initialNotification.notification?.title || notificationData.title,
|
|
2920
|
-
body: initialNotification.notification?.body || notificationData.body
|
|
2921
|
-
};
|
|
2922
|
-
|
|
2923
|
-
// Emit notificationOpened event for host app to handle deep linking
|
|
2924
|
-
this.emitNotificationOpened(deepLinkPayload);
|
|
2925
2962
|
}
|
|
2926
2963
|
|
|
2927
2964
|
// Also check Firebase's getInitialNotification for iOS auto-displayed notifications
|
|
@@ -2945,7 +2982,21 @@ class SwanSDK {
|
|
|
2945
2982
|
const messaging = require('@react-native-firebase/messaging').default;
|
|
2946
2983
|
const initialNotification = await messaging().getInitialNotification();
|
|
2947
2984
|
if (initialNotification && !this.initialNotificationHandled) {
|
|
2948
|
-
|
|
2985
|
+
this.initialNotificationHandled = true;
|
|
2986
|
+
if (!this.isPushEnabled()) {
|
|
2987
|
+
_Logger.default.log('[SwanSDK] Push notifications disabled, ignoring Firebase initial notification');
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
const notificationData = initialNotification.data || {};
|
|
2991
|
+
// Use custom messageId from data (consistent with Notifee handlers),
|
|
2992
|
+
// fall back to Firebase's system messageId
|
|
2993
|
+
const messageId = notificationData.messageId || initialNotification.messageId;
|
|
2994
|
+
|
|
2995
|
+
// Carousel notifications: defer to checkPendingCarouselClick for per-item routes
|
|
2996
|
+
if (notificationData.notificationType === 'carousel') {
|
|
2997
|
+
_Logger.default.log('[SwanSDK] Carousel Firebase initial notification, deferring to checkPendingCarouselClick');
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
2949
3000
|
|
|
2950
3001
|
// Deduplication: prevent double click handling
|
|
2951
3002
|
if (messageId && !markClickProcessed(messageId)) {
|
|
@@ -2954,12 +3005,7 @@ class SwanSDK {
|
|
|
2954
3005
|
}
|
|
2955
3006
|
_Logger.default.log('[SwanSDK] App opened from Firebase notification (killed state):', messageId);
|
|
2956
3007
|
|
|
2957
|
-
// Mark as handled to prevent duplicate ACKs
|
|
2958
|
-
this.initialNotificationHandled = true;
|
|
2959
|
-
const notificationData = initialNotification.data || {};
|
|
2960
|
-
|
|
2961
3008
|
// Extract deep link information
|
|
2962
|
-
// For carousel notifications, the default route is in 'defaultRoute' field
|
|
2963
3009
|
const resolvedRoute = notificationData.route || notificationData.defaultRoute;
|
|
2964
3010
|
const deepLinkPayload = {
|
|
2965
3011
|
...notificationData,
|
|
@@ -2969,13 +3015,11 @@ class SwanSDK {
|
|
|
2969
3015
|
body: initialNotification.notification?.body || notificationData.body
|
|
2970
3016
|
};
|
|
2971
3017
|
|
|
2972
|
-
// Emit
|
|
3018
|
+
// Emit deep link FIRST, then ACK
|
|
2973
3019
|
this.emitNotificationOpened(deepLinkPayload);
|
|
2974
|
-
|
|
2975
|
-
// Send click ACK
|
|
2976
|
-
if (messageId) {
|
|
3020
|
+
if (messageId && !directAckSentIds.has(messageId)) {
|
|
2977
3021
|
await this.sendNotificationAck(messageId, 'clicked');
|
|
2978
|
-
_Logger.default.log('[SwanSDK]
|
|
3022
|
+
_Logger.default.log('[SwanSDK] Firebase initial notification click ACK sent');
|
|
2979
3023
|
}
|
|
2980
3024
|
}
|
|
2981
3025
|
} catch (error) {
|
|
@@ -3827,7 +3871,7 @@ function createNotificationOpenedHandler() {
|
|
|
3827
3871
|
_Logger.default.warn('[SwanSDK] No messageId in notification data, skipping click ACK');
|
|
3828
3872
|
}
|
|
3829
3873
|
} catch (error) {
|
|
3830
|
-
_Logger.default.error('[SwanSDK] Error handling
|
|
3874
|
+
_Logger.default.error('[SwanSDK] Error handling notification opened event:', error);
|
|
3831
3875
|
}
|
|
3832
3876
|
};
|
|
3833
3877
|
}
|
|
@@ -3965,41 +4009,8 @@ function createNotifeeBackgroundHandler() {
|
|
|
3965
4009
|
// Get messageId and notification data
|
|
3966
4010
|
const messageId = detail?.notification?.data?.messageId;
|
|
3967
4011
|
const notificationData = detail?.notification?.data || {};
|
|
3968
|
-
|
|
3969
|
-
// Deduplication: prevent double click handling
|
|
3970
|
-
if (messageId && !markClickProcessed(messageId)) {
|
|
3971
|
-
_Logger.default.log('[SwanSDK] Click already processed for messageId:', messageId);
|
|
3972
|
-
return;
|
|
3973
|
-
}
|
|
3974
|
-
|
|
3975
|
-
// Extract deep link information
|
|
3976
|
-
// Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
|
|
3977
|
-
// For carousel notifications, the default route is in 'defaultRoute' field
|
|
3978
|
-
let route = notificationData.route || notificationData.defaultRoute;
|
|
3979
|
-
|
|
3980
|
-
// For carousel notifications on iOS, the Content Extension saves per-item
|
|
3981
|
-
// click data (including item-specific route) to the App Group.
|
|
3982
|
-
if (_reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel') {
|
|
3983
|
-
try {
|
|
3984
|
-
const clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
|
|
3985
|
-
if (clickData?.route) {
|
|
3986
|
-
_Logger.default.log('[SwanSDK] Background: carousel item route from Content Extension:', clickData.route);
|
|
3987
|
-
route = clickData.route;
|
|
3988
|
-
}
|
|
3989
|
-
} catch (err) {
|
|
3990
|
-
_Logger.default.warn('[SwanSDK] Failed to read Content Extension click data:', err);
|
|
3991
|
-
}
|
|
3992
|
-
}
|
|
3993
|
-
const deepLinkPayload = {
|
|
3994
|
-
...notificationData,
|
|
3995
|
-
route,
|
|
3996
|
-
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
3997
|
-
title: detail?.notification?.title,
|
|
3998
|
-
body: detail?.notification?.body
|
|
3999
|
-
};
|
|
4000
4012
|
_Logger.default.log('[SwanSDK] Background notification clicked:', {
|
|
4001
4013
|
messageId,
|
|
4002
|
-
route: deepLinkPayload.route,
|
|
4003
4014
|
type: notificationData.notificationType
|
|
4004
4015
|
});
|
|
4005
4016
|
|
|
@@ -4007,10 +4018,39 @@ function createNotifeeBackgroundHandler() {
|
|
|
4007
4018
|
const sdkInstance = SwanSDK.getCurrentInstance();
|
|
4008
4019
|
const isSDKReady = sdkInstance && sdkInstance.isReady();
|
|
4009
4020
|
if (isSDKReady) {
|
|
4021
|
+
// Deduplication: only when SDK can deliver the deep link
|
|
4022
|
+
if (messageId && !markClickProcessed(messageId)) {
|
|
4023
|
+
_Logger.default.log('[SwanSDK] Click already processed for messageId:', messageId);
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4010
4026
|
if (!sdkInstance.isPushEnabled()) {
|
|
4011
4027
|
return;
|
|
4012
4028
|
}
|
|
4013
4029
|
|
|
4030
|
+
// Extract deep link information
|
|
4031
|
+
let route = notificationData.route || notificationData.defaultRoute;
|
|
4032
|
+
|
|
4033
|
+
// For carousel notifications on iOS, the Content Extension saves per-item
|
|
4034
|
+
// click data (including item-specific route) to the App Group.
|
|
4035
|
+
if (_reactNative.Platform.OS === 'ios' && notificationData.notificationType === 'carousel') {
|
|
4036
|
+
try {
|
|
4037
|
+
const clickData = await _SharedCredentialsManager.SharedCredentialsManager.readTemplateClickData();
|
|
4038
|
+
if (clickData?.route) {
|
|
4039
|
+
_Logger.default.log('[SwanSDK] Background: carousel item route from Content Extension:', clickData.route);
|
|
4040
|
+
route = clickData.route;
|
|
4041
|
+
}
|
|
4042
|
+
} catch (err) {
|
|
4043
|
+
_Logger.default.warn('[SwanSDK] Failed to read Content Extension click data:', err);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
const deepLinkPayload = {
|
|
4047
|
+
...notificationData,
|
|
4048
|
+
route,
|
|
4049
|
+
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
4050
|
+
title: detail?.notification?.title,
|
|
4051
|
+
body: detail?.notification?.body
|
|
4052
|
+
};
|
|
4053
|
+
|
|
4014
4054
|
// Emit notificationOpened event for host app to handle
|
|
4015
4055
|
sdkInstance.emitNotificationOpened(deepLinkPayload);
|
|
4016
4056
|
_Logger.default.log('[SwanSDK] Background: emitted notificationOpened with route:', deepLinkPayload.route);
|
|
@@ -4023,9 +4063,12 @@ function createNotifeeBackgroundHandler() {
|
|
|
4023
4063
|
return;
|
|
4024
4064
|
}
|
|
4025
4065
|
|
|
4026
|
-
// SDK not ready (app was killed)
|
|
4066
|
+
// SDK not ready (app was killed) — send ACK directly via fetch.
|
|
4067
|
+
// Do NOT markClickProcessed here: setupNotifeeEventListeners will
|
|
4068
|
+
// handle dedup and deep link routing when the SDK initializes.
|
|
4027
4069
|
_Logger.default.log('[SwanSDK] SDK not ready, sending click ACK via direct fetch');
|
|
4028
4070
|
if (messageId) {
|
|
4071
|
+
directAckSentIds.add(messageId);
|
|
4029
4072
|
await sendDirectNotificationAck(messageId, 'clicked');
|
|
4030
4073
|
}
|
|
4031
4074
|
} catch (error) {
|