@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/module/index.js
CHANGED
|
@@ -116,7 +116,7 @@ function parseKeyValuePairs(data) {
|
|
|
116
116
|
* Notifee handlers fire for the same notification.
|
|
117
117
|
*/
|
|
118
118
|
const processedClickIds = new Set();
|
|
119
|
-
const CLICK_ID_TTL_MS =
|
|
119
|
+
const CLICK_ID_TTL_MS = 30_000; // Clear after 30 seconds
|
|
120
120
|
|
|
121
121
|
function markClickProcessed(id) {
|
|
122
122
|
if (processedClickIds.has(id)) {
|
|
@@ -127,9 +127,17 @@ function markClickProcessed(id) {
|
|
|
127
127
|
return true; // First time processing
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Track messageIds that already received a direct click ACK from the background
|
|
132
|
+
* handler when the SDK was not ready. Prevents setupNotifeeEventListeners from
|
|
133
|
+
* sending a duplicate click ACK for the same notification.
|
|
134
|
+
*/
|
|
135
|
+
const directAckSentIds = new Set();
|
|
136
|
+
|
|
130
137
|
/** @internal Test-only: reset click deduplication state */
|
|
131
138
|
export function _resetClickDedup() {
|
|
132
139
|
processedClickIds.clear();
|
|
140
|
+
directAckSentIds.clear();
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
/**
|
|
@@ -615,22 +623,32 @@ export default class SwanSDK {
|
|
|
615
623
|
}
|
|
616
624
|
this.listeners[event].push(callback);
|
|
617
625
|
|
|
618
|
-
// On
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
|
|
626
|
+
// On every NOTIFICATION_OPENED listener registration, check for buffered notifications.
|
|
627
|
+
// Runs on cold start (process dead → getInitialNotification buffered), warm restart
|
|
628
|
+
// (process alive but Activity destroyed by swipe-from-recents on Android), and
|
|
629
|
+
// Metro hot-reload (component unmount/remount cycles).
|
|
630
|
+
if (event === SwanSDK.EVENTS.NOTIFICATION_OPENED) {
|
|
622
631
|
// Defer to next microtask so the listener is fully registered before events emit
|
|
623
632
|
Promise.resolve().then(() => {
|
|
624
|
-
// Deliver buffered standard/image push notification
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
633
|
+
// Deliver buffered standard/image push notification.
|
|
634
|
+
// Skip carousel — checkPendingCarouselClick() handles those with per-item routes.
|
|
635
|
+
if (this.pendingNotificationPayload) {
|
|
636
|
+
const payload = this.pendingNotificationPayload;
|
|
637
|
+
this.pendingNotificationPayload = null;
|
|
638
|
+
if (payload.notificationType !== 'carousel') {
|
|
639
|
+
this.emit(SwanSDK.EVENTS.NOTIFICATION_OPENED, payload);
|
|
640
|
+
this.emitDeepLinkOpened({
|
|
641
|
+
...payload,
|
|
642
|
+
source: 'push'
|
|
643
|
+
});
|
|
644
|
+
}
|
|
631
645
|
}
|
|
632
|
-
// Check for pending carousel click (native side holds data until consumed)
|
|
646
|
+
// Check for pending carousel click (native side holds data until consumed).
|
|
647
|
+
// Unconditional: handles both cold start and warm restart carousel taps.
|
|
633
648
|
this.checkPendingCarouselClick();
|
|
649
|
+
if (!this.coldStartCheckDone) {
|
|
650
|
+
this.coldStartCheckDone = true;
|
|
651
|
+
}
|
|
634
652
|
});
|
|
635
653
|
}
|
|
636
654
|
|
|
@@ -660,9 +678,11 @@ export default class SwanSDK {
|
|
|
660
678
|
*/
|
|
661
679
|
emitNotificationOpened(payload) {
|
|
662
680
|
const listeners = this.listeners[SwanSDK.EVENTS.NOTIFICATION_OPENED];
|
|
663
|
-
if (
|
|
664
|
-
//
|
|
665
|
-
//
|
|
681
|
+
if (!listeners || listeners.length === 0) {
|
|
682
|
+
// No listeners yet — buffer for delivery when first listener registers.
|
|
683
|
+
// Handles: true cold start (process dead), Activity recreation (process alive
|
|
684
|
+
// but Activity destroyed by swipe-from-recents on Android), and Metro
|
|
685
|
+
// hot-reload (component unmount/remount cycles).
|
|
666
686
|
Logger.log('[SwanSDK] Buffering notification payload for deferred delivery');
|
|
667
687
|
this.pendingNotificationPayload = payload;
|
|
668
688
|
return;
|
|
@@ -2882,31 +2902,48 @@ export default class SwanSDK {
|
|
|
2882
2902
|
// similar to how Firebase requires onMessage() at module level.
|
|
2883
2903
|
|
|
2884
2904
|
// Check if app was opened by tapping a Notifee-displayed notification (data-only architecture)
|
|
2885
|
-
// Notifee's getInitialNotification() returns { notification, pressAction }.
|
|
2886
|
-
// The FCM data we stored via displayNotification({ data: {...} }) lives at
|
|
2887
|
-
// notification.data — NOT at the top-level .data (which doesn't exist).
|
|
2888
2905
|
const initialNotification = await notifee.getInitialNotification();
|
|
2906
|
+
Logger.log('[SwanSDK] getInitialNotification result:', JSON.stringify(initialNotification, null, 2));
|
|
2889
2907
|
if (initialNotification && !this.initialNotificationHandled) {
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2908
|
+
// Notifee returned an initial notification — mark as handled
|
|
2909
|
+
// unconditionally to prevent Firebase fallback from double-emitting.
|
|
2910
|
+
this.initialNotificationHandled = true;
|
|
2911
|
+
if (!this.isPushEnabled()) {
|
|
2912
|
+
Logger.log('[SwanSDK] Push notifications disabled, ignoring initial notification');
|
|
2913
|
+
} else {
|
|
2914
|
+
const notificationData = initialNotification.notification?.data || {};
|
|
2915
|
+
const messageId = notificationData.messageId;
|
|
2916
|
+
|
|
2917
|
+
// Carousel notifications: skip standard path entirely.
|
|
2918
|
+
// checkPendingCarouselClick() (triggered by addListener) handles
|
|
2919
|
+
// per-item routes from native storage / App Group.
|
|
2920
|
+
if (notificationData.notificationType === 'carousel') {
|
|
2921
|
+
Logger.log('[SwanSDK] Carousel initial notification, deferring to checkPendingCarouselClick');
|
|
2922
|
+
} else if (messageId && !markClickProcessed(messageId)) {
|
|
2923
|
+
Logger.log('[SwanSDK] Click already processed for messageId:', messageId);
|
|
2924
|
+
} else {
|
|
2925
|
+
Logger.log('[SwanSDK] App opened from Notifee notification tap:', messageId);
|
|
2926
|
+
|
|
2927
|
+
// Extract deep link information
|
|
2928
|
+
const resolvedRoute = notificationData.route || notificationData.defaultRoute;
|
|
2929
|
+
const deepLinkPayload = {
|
|
2930
|
+
...notificationData,
|
|
2931
|
+
route: resolvedRoute,
|
|
2932
|
+
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
2933
|
+
title: initialNotification.notification?.title || notificationData.title,
|
|
2934
|
+
body: initialNotification.notification?.body || notificationData.body
|
|
2935
|
+
};
|
|
2936
|
+
|
|
2937
|
+
// Emit deep link FIRST to ensure delivery even if ACK fails
|
|
2938
|
+
Logger.log('[SwanSDK] Cold-start deepLinkPayload:', JSON.stringify(deepLinkPayload, null, 2));
|
|
2939
|
+
this.emitNotificationOpened(deepLinkPayload);
|
|
2940
|
+
|
|
2941
|
+
// Send click ACK after emit (skip if background handler already sent one)
|
|
2942
|
+
if (messageId && !directAckSentIds.has(messageId)) {
|
|
2943
|
+
await this.sendNotificationAck(messageId, 'clicked');
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2896
2946
|
}
|
|
2897
|
-
// Extract deep link information
|
|
2898
|
-
// For carousel notifications, the default route is in 'defaultRoute' field
|
|
2899
|
-
const resolvedRoute = notificationData.route || notificationData.defaultRoute;
|
|
2900
|
-
const deepLinkPayload = {
|
|
2901
|
-
...notificationData,
|
|
2902
|
-
route: resolvedRoute,
|
|
2903
|
-
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
2904
|
-
title: initialNotification.notification?.title || notificationData.title,
|
|
2905
|
-
body: initialNotification.notification?.body || notificationData.body
|
|
2906
|
-
};
|
|
2907
|
-
|
|
2908
|
-
// Emit notificationOpened event for host app to handle deep linking
|
|
2909
|
-
this.emitNotificationOpened(deepLinkPayload);
|
|
2910
2947
|
}
|
|
2911
2948
|
|
|
2912
2949
|
// Also check Firebase's getInitialNotification for iOS auto-displayed notifications
|
|
@@ -2930,7 +2967,21 @@ export default class SwanSDK {
|
|
|
2930
2967
|
const messaging = require('@react-native-firebase/messaging').default;
|
|
2931
2968
|
const initialNotification = await messaging().getInitialNotification();
|
|
2932
2969
|
if (initialNotification && !this.initialNotificationHandled) {
|
|
2933
|
-
|
|
2970
|
+
this.initialNotificationHandled = true;
|
|
2971
|
+
if (!this.isPushEnabled()) {
|
|
2972
|
+
Logger.log('[SwanSDK] Push notifications disabled, ignoring Firebase initial notification');
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
const notificationData = initialNotification.data || {};
|
|
2976
|
+
// Use custom messageId from data (consistent with Notifee handlers),
|
|
2977
|
+
// fall back to Firebase's system messageId
|
|
2978
|
+
const messageId = notificationData.messageId || initialNotification.messageId;
|
|
2979
|
+
|
|
2980
|
+
// Carousel notifications: defer to checkPendingCarouselClick for per-item routes
|
|
2981
|
+
if (notificationData.notificationType === 'carousel') {
|
|
2982
|
+
Logger.log('[SwanSDK] Carousel Firebase initial notification, deferring to checkPendingCarouselClick');
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2934
2985
|
|
|
2935
2986
|
// Deduplication: prevent double click handling
|
|
2936
2987
|
if (messageId && !markClickProcessed(messageId)) {
|
|
@@ -2939,12 +2990,7 @@ export default class SwanSDK {
|
|
|
2939
2990
|
}
|
|
2940
2991
|
Logger.log('[SwanSDK] App opened from Firebase notification (killed state):', messageId);
|
|
2941
2992
|
|
|
2942
|
-
// Mark as handled to prevent duplicate ACKs
|
|
2943
|
-
this.initialNotificationHandled = true;
|
|
2944
|
-
const notificationData = initialNotification.data || {};
|
|
2945
|
-
|
|
2946
2993
|
// Extract deep link information
|
|
2947
|
-
// For carousel notifications, the default route is in 'defaultRoute' field
|
|
2948
2994
|
const resolvedRoute = notificationData.route || notificationData.defaultRoute;
|
|
2949
2995
|
const deepLinkPayload = {
|
|
2950
2996
|
...notificationData,
|
|
@@ -2954,13 +3000,11 @@ export default class SwanSDK {
|
|
|
2954
3000
|
body: initialNotification.notification?.body || notificationData.body
|
|
2955
3001
|
};
|
|
2956
3002
|
|
|
2957
|
-
// Emit
|
|
3003
|
+
// Emit deep link FIRST, then ACK
|
|
2958
3004
|
this.emitNotificationOpened(deepLinkPayload);
|
|
2959
|
-
|
|
2960
|
-
// Send click ACK
|
|
2961
|
-
if (messageId) {
|
|
3005
|
+
if (messageId && !directAckSentIds.has(messageId)) {
|
|
2962
3006
|
await this.sendNotificationAck(messageId, 'clicked');
|
|
2963
|
-
Logger.log('[SwanSDK]
|
|
3007
|
+
Logger.log('[SwanSDK] Firebase initial notification click ACK sent');
|
|
2964
3008
|
}
|
|
2965
3009
|
}
|
|
2966
3010
|
} catch (error) {
|
|
@@ -3811,7 +3855,7 @@ export function createNotificationOpenedHandler() {
|
|
|
3811
3855
|
Logger.warn('[SwanSDK] No messageId in notification data, skipping click ACK');
|
|
3812
3856
|
}
|
|
3813
3857
|
} catch (error) {
|
|
3814
|
-
Logger.error('[SwanSDK] Error handling
|
|
3858
|
+
Logger.error('[SwanSDK] Error handling notification opened event:', error);
|
|
3815
3859
|
}
|
|
3816
3860
|
};
|
|
3817
3861
|
}
|
|
@@ -3949,41 +3993,8 @@ export function createNotifeeBackgroundHandler() {
|
|
|
3949
3993
|
// Get messageId and notification data
|
|
3950
3994
|
const messageId = detail?.notification?.data?.messageId;
|
|
3951
3995
|
const notificationData = detail?.notification?.data || {};
|
|
3952
|
-
|
|
3953
|
-
// Deduplication: prevent double click handling
|
|
3954
|
-
if (messageId && !markClickProcessed(messageId)) {
|
|
3955
|
-
Logger.log('[SwanSDK] Click already processed for messageId:', messageId);
|
|
3956
|
-
return;
|
|
3957
|
-
}
|
|
3958
|
-
|
|
3959
|
-
// Extract deep link information
|
|
3960
|
-
// Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
|
|
3961
|
-
// For carousel notifications, the default route is in 'defaultRoute' field
|
|
3962
|
-
let route = notificationData.route || notificationData.defaultRoute;
|
|
3963
|
-
|
|
3964
|
-
// For carousel notifications on iOS, the Content Extension saves per-item
|
|
3965
|
-
// click data (including item-specific route) to the App Group.
|
|
3966
|
-
if (Platform.OS === 'ios' && notificationData.notificationType === 'carousel') {
|
|
3967
|
-
try {
|
|
3968
|
-
const clickData = await SharedCredentialsManager.readTemplateClickData();
|
|
3969
|
-
if (clickData?.route) {
|
|
3970
|
-
Logger.log('[SwanSDK] Background: carousel item route from Content Extension:', clickData.route);
|
|
3971
|
-
route = clickData.route;
|
|
3972
|
-
}
|
|
3973
|
-
} catch (err) {
|
|
3974
|
-
Logger.warn('[SwanSDK] Failed to read Content Extension click data:', err);
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
const deepLinkPayload = {
|
|
3978
|
-
...notificationData,
|
|
3979
|
-
route,
|
|
3980
|
-
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
3981
|
-
title: detail?.notification?.title,
|
|
3982
|
-
body: detail?.notification?.body
|
|
3983
|
-
};
|
|
3984
3996
|
Logger.log('[SwanSDK] Background notification clicked:', {
|
|
3985
3997
|
messageId,
|
|
3986
|
-
route: deepLinkPayload.route,
|
|
3987
3998
|
type: notificationData.notificationType
|
|
3988
3999
|
});
|
|
3989
4000
|
|
|
@@ -3991,10 +4002,39 @@ export function createNotifeeBackgroundHandler() {
|
|
|
3991
4002
|
const sdkInstance = SwanSDK.getCurrentInstance();
|
|
3992
4003
|
const isSDKReady = sdkInstance && sdkInstance.isReady();
|
|
3993
4004
|
if (isSDKReady) {
|
|
4005
|
+
// Deduplication: only when SDK can deliver the deep link
|
|
4006
|
+
if (messageId && !markClickProcessed(messageId)) {
|
|
4007
|
+
Logger.log('[SwanSDK] Click already processed for messageId:', messageId);
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
3994
4010
|
if (!sdkInstance.isPushEnabled()) {
|
|
3995
4011
|
return;
|
|
3996
4012
|
}
|
|
3997
4013
|
|
|
4014
|
+
// Extract deep link information
|
|
4015
|
+
let route = notificationData.route || notificationData.defaultRoute;
|
|
4016
|
+
|
|
4017
|
+
// For carousel notifications on iOS, the Content Extension saves per-item
|
|
4018
|
+
// click data (including item-specific route) to the App Group.
|
|
4019
|
+
if (Platform.OS === 'ios' && notificationData.notificationType === 'carousel') {
|
|
4020
|
+
try {
|
|
4021
|
+
const clickData = await SharedCredentialsManager.readTemplateClickData();
|
|
4022
|
+
if (clickData?.route) {
|
|
4023
|
+
Logger.log('[SwanSDK] Background: carousel item route from Content Extension:', clickData.route);
|
|
4024
|
+
route = clickData.route;
|
|
4025
|
+
}
|
|
4026
|
+
} catch (err) {
|
|
4027
|
+
Logger.warn('[SwanSDK] Failed to read Content Extension click data:', err);
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
const deepLinkPayload = {
|
|
4031
|
+
...notificationData,
|
|
4032
|
+
route,
|
|
4033
|
+
keyValuePairs: parseKeyValuePairs(notificationData),
|
|
4034
|
+
title: detail?.notification?.title,
|
|
4035
|
+
body: detail?.notification?.body
|
|
4036
|
+
};
|
|
4037
|
+
|
|
3998
4038
|
// Emit notificationOpened event for host app to handle
|
|
3999
4039
|
sdkInstance.emitNotificationOpened(deepLinkPayload);
|
|
4000
4040
|
Logger.log('[SwanSDK] Background: emitted notificationOpened with route:', deepLinkPayload.route);
|
|
@@ -4007,9 +4047,12 @@ export function createNotifeeBackgroundHandler() {
|
|
|
4007
4047
|
return;
|
|
4008
4048
|
}
|
|
4009
4049
|
|
|
4010
|
-
// SDK not ready (app was killed)
|
|
4050
|
+
// SDK not ready (app was killed) — send ACK directly via fetch.
|
|
4051
|
+
// Do NOT markClickProcessed here: setupNotifeeEventListeners will
|
|
4052
|
+
// handle dedup and deep link routing when the SDK initializes.
|
|
4011
4053
|
Logger.log('[SwanSDK] SDK not ready, sending click ACK via direct fetch');
|
|
4012
4054
|
if (messageId) {
|
|
4055
|
+
directAckSentIds.add(messageId);
|
|
4013
4056
|
await sendDirectNotificationAck(messageId, 'clicked');
|
|
4014
4057
|
}
|
|
4015
4058
|
} catch (error) {
|