@loyalytics/swan-react-native-sdk 2.0.1

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.
Files changed (204) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +67 -0
  3. package/docs/IOS_NOTIFICATION_EXTENSION_SETUP.md +335 -0
  4. package/ios/README.md +64 -0
  5. package/ios/SwanNotificationServiceExtension/Info.plist +31 -0
  6. package/ios/SwanNotificationServiceExtension/NotificationService.swift +337 -0
  7. package/ios/SwanNotificationServiceExtension/SwanNotificationServiceExtension.entitlements +10 -0
  8. package/lib/commonjs/components/FooterView.js +125 -0
  9. package/lib/commonjs/components/FooterView.js.map +1 -0
  10. package/lib/commonjs/components/FullScreenView.js +172 -0
  11. package/lib/commonjs/components/FullScreenView.js.map +1 -0
  12. package/lib/commonjs/components/HeaderView.js +205 -0
  13. package/lib/commonjs/components/HeaderView.js.map +1 -0
  14. package/lib/commonjs/components/PopUpView.js +186 -0
  15. package/lib/commonjs/components/PopUpView.js.map +1 -0
  16. package/lib/commonjs/config/BatchConfig.js +53 -0
  17. package/lib/commonjs/config/BatchConfig.js.map +1 -0
  18. package/lib/commonjs/constants/ApiUrls.js +56 -0
  19. package/lib/commonjs/constants/ApiUrls.js.map +1 -0
  20. package/lib/commonjs/core/EventQueueManager.js +345 -0
  21. package/lib/commonjs/core/EventQueueManager.js.map +1 -0
  22. package/lib/commonjs/core/FlushManager.js +245 -0
  23. package/lib/commonjs/core/FlushManager.js.map +1 -0
  24. package/lib/commonjs/core/NetworkMonitor.js +97 -0
  25. package/lib/commonjs/core/NetworkMonitor.js.map +1 -0
  26. package/lib/commonjs/index.js +3506 -0
  27. package/lib/commonjs/index.js.map +1 -0
  28. package/lib/commonjs/providers/FirebasePushProvider.js +130 -0
  29. package/lib/commonjs/providers/FirebasePushProvider.js.map +1 -0
  30. package/lib/commonjs/providers/NullPushProvider.js +59 -0
  31. package/lib/commonjs/providers/NullPushProvider.js.map +1 -0
  32. package/lib/commonjs/providers/PushNotificationProvider.js +30 -0
  33. package/lib/commonjs/providers/PushNotificationProvider.js.map +1 -0
  34. package/lib/commonjs/services/DeviceRegistrationService.js +248 -0
  35. package/lib/commonjs/services/DeviceRegistrationService.js.map +1 -0
  36. package/lib/commonjs/services/PushTokenService.js +284 -0
  37. package/lib/commonjs/services/PushTokenService.js.map +1 -0
  38. package/lib/commonjs/state/AuthStateMachine.js +161 -0
  39. package/lib/commonjs/state/AuthStateMachine.js.map +1 -0
  40. package/lib/commonjs/state/DeviceStateMachine.js +104 -0
  41. package/lib/commonjs/state/DeviceStateMachine.js.map +1 -0
  42. package/lib/commonjs/state/PushStateMachine.js +129 -0
  43. package/lib/commonjs/state/PushStateMachine.js.map +1 -0
  44. package/lib/commonjs/types/EventQueue.js +50 -0
  45. package/lib/commonjs/types/EventQueue.js.map +1 -0
  46. package/lib/commonjs/types/SDK.js +2 -0
  47. package/lib/commonjs/types/SDK.js.map +1 -0
  48. package/lib/commonjs/utils/FirebaseNotificationManager.js +492 -0
  49. package/lib/commonjs/utils/FirebaseNotificationManager.js.map +1 -0
  50. package/lib/commonjs/utils/Logger.js +56 -0
  51. package/lib/commonjs/utils/Logger.js.map +1 -0
  52. package/lib/commonjs/utils/SharedCredentialsManager.js +146 -0
  53. package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -0
  54. package/lib/commonjs/version.js +12 -0
  55. package/lib/commonjs/version.js.map +1 -0
  56. package/lib/module/components/FooterView.js +121 -0
  57. package/lib/module/components/FooterView.js.map +1 -0
  58. package/lib/module/components/FullScreenView.js +167 -0
  59. package/lib/module/components/FullScreenView.js.map +1 -0
  60. package/lib/module/components/HeaderView.js +199 -0
  61. package/lib/module/components/HeaderView.js.map +1 -0
  62. package/lib/module/components/PopUpView.js +181 -0
  63. package/lib/module/components/PopUpView.js.map +1 -0
  64. package/lib/module/config/BatchConfig.js +49 -0
  65. package/lib/module/config/BatchConfig.js.map +1 -0
  66. package/lib/module/constants/ApiUrls.js +52 -0
  67. package/lib/module/constants/ApiUrls.js.map +1 -0
  68. package/lib/module/core/EventQueueManager.js +340 -0
  69. package/lib/module/core/EventQueueManager.js.map +1 -0
  70. package/lib/module/core/FlushManager.js +240 -0
  71. package/lib/module/core/FlushManager.js.map +1 -0
  72. package/lib/module/core/NetworkMonitor.js +92 -0
  73. package/lib/module/core/NetworkMonitor.js.map +1 -0
  74. package/lib/module/index.js +3494 -0
  75. package/lib/module/index.js.map +1 -0
  76. package/lib/module/providers/FirebasePushProvider.js +124 -0
  77. package/lib/module/providers/FirebasePushProvider.js.map +1 -0
  78. package/lib/module/providers/NullPushProvider.js +53 -0
  79. package/lib/module/providers/NullPushProvider.js.map +1 -0
  80. package/lib/module/providers/PushNotificationProvider.js +26 -0
  81. package/lib/module/providers/PushNotificationProvider.js.map +1 -0
  82. package/lib/module/services/DeviceRegistrationService.js +243 -0
  83. package/lib/module/services/DeviceRegistrationService.js.map +1 -0
  84. package/lib/module/services/PushTokenService.js +278 -0
  85. package/lib/module/services/PushTokenService.js.map +1 -0
  86. package/lib/module/state/AuthStateMachine.js +155 -0
  87. package/lib/module/state/AuthStateMachine.js.map +1 -0
  88. package/lib/module/state/DeviceStateMachine.js +98 -0
  89. package/lib/module/state/DeviceStateMachine.js.map +1 -0
  90. package/lib/module/state/PushStateMachine.js +123 -0
  91. package/lib/module/state/PushStateMachine.js.map +1 -0
  92. package/lib/module/types/EventQueue.js +46 -0
  93. package/lib/module/types/EventQueue.js.map +1 -0
  94. package/lib/module/types/SDK.js +2 -0
  95. package/lib/module/types/SDK.js.map +1 -0
  96. package/lib/module/utils/FirebaseNotificationManager.js +486 -0
  97. package/lib/module/utils/FirebaseNotificationManager.js.map +1 -0
  98. package/lib/module/utils/Logger.js +52 -0
  99. package/lib/module/utils/Logger.js.map +1 -0
  100. package/lib/module/utils/SharedCredentialsManager.js +140 -0
  101. package/lib/module/utils/SharedCredentialsManager.js.map +1 -0
  102. package/lib/module/version.js +8 -0
  103. package/lib/module/version.js.map +1 -0
  104. package/lib/typescript/commonjs/package.json +1 -0
  105. package/lib/typescript/commonjs/src/components/FooterView.d.ts +3 -0
  106. package/lib/typescript/commonjs/src/components/FooterView.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/src/components/FullScreenView.d.ts +3 -0
  108. package/lib/typescript/commonjs/src/components/FullScreenView.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/src/components/HeaderView.d.ts +3 -0
  110. package/lib/typescript/commonjs/src/components/HeaderView.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/src/components/PopUpView.d.ts +3 -0
  112. package/lib/typescript/commonjs/src/components/PopUpView.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/src/config/BatchConfig.d.ts +7 -0
  114. package/lib/typescript/commonjs/src/config/BatchConfig.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/src/constants/ApiUrls.d.ts +56 -0
  116. package/lib/typescript/commonjs/src/constants/ApiUrls.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/src/core/EventQueueManager.d.ts +63 -0
  118. package/lib/typescript/commonjs/src/core/EventQueueManager.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/src/core/FlushManager.d.ts +63 -0
  120. package/lib/typescript/commonjs/src/core/FlushManager.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/src/core/NetworkMonitor.d.ts +38 -0
  122. package/lib/typescript/commonjs/src/core/NetworkMonitor.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/src/index.d.ts +663 -0
  124. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/src/providers/FirebasePushProvider.d.ts +28 -0
  126. package/lib/typescript/commonjs/src/providers/FirebasePushProvider.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts +25 -0
  128. package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/src/providers/PushNotificationProvider.d.ts +105 -0
  130. package/lib/typescript/commonjs/src/providers/PushNotificationProvider.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/src/services/DeviceRegistrationService.d.ts +60 -0
  132. package/lib/typescript/commonjs/src/services/DeviceRegistrationService.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/src/services/PushTokenService.d.ts +82 -0
  134. package/lib/typescript/commonjs/src/services/PushTokenService.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/src/state/AuthStateMachine.d.ts +61 -0
  136. package/lib/typescript/commonjs/src/state/AuthStateMachine.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/src/state/DeviceStateMachine.d.ts +51 -0
  138. package/lib/typescript/commonjs/src/state/DeviceStateMachine.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/src/state/PushStateMachine.d.ts +61 -0
  140. package/lib/typescript/commonjs/src/state/PushStateMachine.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/src/types/EventQueue.d.ts +85 -0
  142. package/lib/typescript/commonjs/src/types/EventQueue.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/src/types/SDK.d.ts +54 -0
  144. package/lib/typescript/commonjs/src/types/SDK.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts +169 -0
  146. package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts.map +1 -0
  147. package/lib/typescript/commonjs/src/utils/Logger.d.ts +32 -0
  148. package/lib/typescript/commonjs/src/utils/Logger.d.ts.map +1 -0
  149. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +54 -0
  150. package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts.map +1 -0
  151. package/lib/typescript/commonjs/src/version.d.ts +2 -0
  152. package/lib/typescript/commonjs/src/version.d.ts.map +1 -0
  153. package/lib/typescript/module/package.json +1 -0
  154. package/lib/typescript/module/src/components/FooterView.d.ts +3 -0
  155. package/lib/typescript/module/src/components/FooterView.d.ts.map +1 -0
  156. package/lib/typescript/module/src/components/FullScreenView.d.ts +3 -0
  157. package/lib/typescript/module/src/components/FullScreenView.d.ts.map +1 -0
  158. package/lib/typescript/module/src/components/HeaderView.d.ts +3 -0
  159. package/lib/typescript/module/src/components/HeaderView.d.ts.map +1 -0
  160. package/lib/typescript/module/src/components/PopUpView.d.ts +3 -0
  161. package/lib/typescript/module/src/components/PopUpView.d.ts.map +1 -0
  162. package/lib/typescript/module/src/config/BatchConfig.d.ts +7 -0
  163. package/lib/typescript/module/src/config/BatchConfig.d.ts.map +1 -0
  164. package/lib/typescript/module/src/constants/ApiUrls.d.ts +56 -0
  165. package/lib/typescript/module/src/constants/ApiUrls.d.ts.map +1 -0
  166. package/lib/typescript/module/src/core/EventQueueManager.d.ts +63 -0
  167. package/lib/typescript/module/src/core/EventQueueManager.d.ts.map +1 -0
  168. package/lib/typescript/module/src/core/FlushManager.d.ts +63 -0
  169. package/lib/typescript/module/src/core/FlushManager.d.ts.map +1 -0
  170. package/lib/typescript/module/src/core/NetworkMonitor.d.ts +38 -0
  171. package/lib/typescript/module/src/core/NetworkMonitor.d.ts.map +1 -0
  172. package/lib/typescript/module/src/index.d.ts +663 -0
  173. package/lib/typescript/module/src/index.d.ts.map +1 -0
  174. package/lib/typescript/module/src/providers/FirebasePushProvider.d.ts +28 -0
  175. package/lib/typescript/module/src/providers/FirebasePushProvider.d.ts.map +1 -0
  176. package/lib/typescript/module/src/providers/NullPushProvider.d.ts +25 -0
  177. package/lib/typescript/module/src/providers/NullPushProvider.d.ts.map +1 -0
  178. package/lib/typescript/module/src/providers/PushNotificationProvider.d.ts +105 -0
  179. package/lib/typescript/module/src/providers/PushNotificationProvider.d.ts.map +1 -0
  180. package/lib/typescript/module/src/services/DeviceRegistrationService.d.ts +60 -0
  181. package/lib/typescript/module/src/services/DeviceRegistrationService.d.ts.map +1 -0
  182. package/lib/typescript/module/src/services/PushTokenService.d.ts +82 -0
  183. package/lib/typescript/module/src/services/PushTokenService.d.ts.map +1 -0
  184. package/lib/typescript/module/src/state/AuthStateMachine.d.ts +61 -0
  185. package/lib/typescript/module/src/state/AuthStateMachine.d.ts.map +1 -0
  186. package/lib/typescript/module/src/state/DeviceStateMachine.d.ts +51 -0
  187. package/lib/typescript/module/src/state/DeviceStateMachine.d.ts.map +1 -0
  188. package/lib/typescript/module/src/state/PushStateMachine.d.ts +61 -0
  189. package/lib/typescript/module/src/state/PushStateMachine.d.ts.map +1 -0
  190. package/lib/typescript/module/src/types/EventQueue.d.ts +85 -0
  191. package/lib/typescript/module/src/types/EventQueue.d.ts.map +1 -0
  192. package/lib/typescript/module/src/types/SDK.d.ts +54 -0
  193. package/lib/typescript/module/src/types/SDK.d.ts.map +1 -0
  194. package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts +169 -0
  195. package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts.map +1 -0
  196. package/lib/typescript/module/src/utils/Logger.d.ts +32 -0
  197. package/lib/typescript/module/src/utils/Logger.d.ts.map +1 -0
  198. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +54 -0
  199. package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -0
  200. package/lib/typescript/module/src/version.d.ts +2 -0
  201. package/lib/typescript/module/src/version.d.ts.map +1 -0
  202. package/package.json +230 -0
  203. package/scripts/generate-version.js +25 -0
  204. package/scripts/setup-ios-extension.js +275 -0
@@ -0,0 +1,3506 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.SWAN_NOTIFICATION_CHANNELS = exports.ECOM_EVENTS = void 0;
7
+ Object.defineProperty(exports, "SharedCredentialsManager", {
8
+ enumerable: true,
9
+ get: function () {
10
+ return _SharedCredentialsManager.SharedCredentialsManager;
11
+ }
12
+ });
13
+ exports.SwanEcomSDK = void 0;
14
+ exports.createBackgroundMessageHandler = createBackgroundMessageHandler;
15
+ exports.createForegroundMessageHandler = createForegroundMessageHandler;
16
+ exports.createNotifeeBackgroundHandler = createNotifeeBackgroundHandler;
17
+ exports.createNotifeeForegroundHandler = createNotifeeForegroundHandler;
18
+ exports.createNotificationOpenedHandler = createNotificationOpenedHandler;
19
+ exports.default = void 0;
20
+ var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
21
+ var _reactNativeBase = _interopRequireDefault(require("react-native-base64"));
22
+ var _reactNativeUuid = _interopRequireDefault(require("react-native-uuid"));
23
+ var _reactNative = require("react-native");
24
+ var _version = require("./version.js");
25
+ var _PopUpView = _interopRequireDefault(require("./components/PopUpView.js"));
26
+ var _HeaderView = _interopRequireDefault(require("./components/HeaderView.js"));
27
+ var _FooterView = _interopRequireDefault(require("./components/FooterView.js"));
28
+ var _FullScreenView = _interopRequireDefault(require("./components/FullScreenView.js"));
29
+ var _react = require("react");
30
+ var _reactNativeSqlite = _interopRequireDefault(require("react-native-sqlite-2"));
31
+ var _reactNativeDeviceInfo = _interopRequireDefault(require("react-native-device-info"));
32
+ var _geolocation = _interopRequireDefault(require("@react-native-community/geolocation"));
33
+ var _Logger = _interopRequireDefault(require("./utils/Logger.js"));
34
+ var _EventQueueManager = require("./core/EventQueueManager.js");
35
+ var _FlushManager = require("./core/FlushManager.js");
36
+ var _NetworkMonitor = require("./core/NetworkMonitor.js");
37
+ var _BatchConfig = require("./config/BatchConfig.js");
38
+ var _netinfo = _interopRequireDefault(require("@react-native-community/netinfo"));
39
+ var _DeviceStateMachine = require("./state/DeviceStateMachine.js");
40
+ var _PushStateMachine = require("./state/PushStateMachine.js");
41
+ var _AuthStateMachine = require("./state/AuthStateMachine.js");
42
+ var _DeviceRegistrationService = require("./services/DeviceRegistrationService.js");
43
+ var _PushTokenService = require("./services/PushTokenService.js");
44
+ var _NullPushProvider = require("./providers/NullPushProvider.js");
45
+ var _SharedCredentialsManager = require("./utils/SharedCredentialsManager.js");
46
+ var _ApiUrls = _interopRequireDefault(require("./constants/ApiUrls.js"));
47
+ var _jsxRuntime = require("react/jsx-runtime");
48
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
49
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } /**
50
+ * Swan React Native SDK
51
+ * Copyright (c) 2025 Loyalytics. All Rights Reserved.
52
+ *
53
+ * PROPRIETARY AND CONFIDENTIAL
54
+ * Unauthorized copying, modification, distribution, or use of this software
55
+ * is strictly prohibited. See LICENSE file for full terms.
56
+ *
57
+ * For licensing inquiries: support@loyalytics.ai
58
+ */ // New imports for refactored architecture
59
+ // Predefined Notification Channels
60
+ // These channels are created automatically by FirebaseNotificationManager on SDK initialization
61
+ // Swan Builder should use these channel IDs when creating push campaigns
62
+ const SWAN_NOTIFICATION_CHANNELS = exports.SWAN_NOTIFICATION_CHANNELS = Object.freeze({
63
+ TRANSACTIONAL: 'swan_transactional',
64
+ // High priority - Orders, OTPs, urgent updates
65
+ ALERTS: 'swan_alerts',
66
+ // High priority - Critical alerts, warnings
67
+ PROMOTIONAL: 'swan_promotional',
68
+ // Normal priority - Marketing, offers, deals
69
+ GENERAL: 'swan_general',
70
+ // Normal priority - General updates, news
71
+ DEFAULT: 'swan_notifications' // Default channel (high priority)
72
+ });
73
+ const ECOM_EVENTS = exports.ECOM_EVENTS = Object.freeze({
74
+ APP_LAUNCHED: 'appLaunched',
75
+ USER_LOGOUT: 'userLogout',
76
+ USER_LOGIN: 'userLogin',
77
+ FORGOT_PASSWORD: 'forgotPassword',
78
+ SEARCH: 'search',
79
+ PRODUCT_VIEWED: 'productViewed',
80
+ PRODUCT_CLICKED: 'productClicked',
81
+ PRODUCT_LIST_VIEWED: 'productListViewed',
82
+ PRODUCT_ADDED_TO_ADD_TO_CART: 'productAddedToaddTocart',
83
+ PRODUCT_REMOVED_FROM_ADD_TO_CART: 'productRemovedFromAddToCart',
84
+ CLEAR_CART: 'clearCart',
85
+ SELECT_CATEGORY: 'selectCategory',
86
+ CATEGORY_VIEWED_PAGE: 'categoryViewedPage',
87
+ PRODUCT_ADDED_TO_WISHLIST: 'productAddedToWishlist',
88
+ PRODUCT_REMOVED_FROM_WISHLIST: 'productRemovedFromWishlist',
89
+ PRODUCT_RATED_OR_REVIEWED: 'productRatedOrReviewed',
90
+ CART_VIEWED: 'cartViewed',
91
+ OFFER_AVAILED: 'offerAvailed',
92
+ CHECKOUT_STARTED: 'checkoutStarted',
93
+ CHECKOUT_COMPLETED: 'checkoutCompleted',
94
+ CHECKOUT_CANCELED: 'checkoutCanceled',
95
+ PAYMENT_INFO_ENTERED: 'paymentInfoEntered',
96
+ ORDER_COMPLETED: 'orderCompleted',
97
+ ORDER_REFUNDED: 'orderRefunded',
98
+ ORDER_CANCELLED: 'orderCancelled',
99
+ ORDER_EXPERIANCE_RATING: 'orderExperianceRating',
100
+ PRODUCT_REVIEW: 'productReview',
101
+ PURCHASED: 'purchased',
102
+ APP_UPDATED: 'appUpdated',
103
+ ACCOUNT_DELETION: 'accountDeletion',
104
+ SHARE: 'share',
105
+ SCREEN: 'screen',
106
+ WISHLIST_PRODUCT_ADDED_TO_CART: 'wishlistProductAddedToCart',
107
+ SHIPPED: 'shipped',
108
+ PRODUCT_QUANTITY_SELECTED: 'productQuantitySelected'
109
+ });
110
+
111
+ /**
112
+ * Notification deep link payload
113
+ * Emitted when user clicks on a push notification
114
+ */
115
+
116
+ // Re-export for external use
117
+
118
+ class SwanSDK {
119
+ listeners = {};
120
+ SDK_VERSION = _version.SDK_VERSION;
121
+ isProduction = 'STAGE';
122
+ appId = '';
123
+ deviceId = '';
124
+ static modalInstances = [];
125
+ isDatabaseConfigured = false;
126
+ currentScreenName = '';
127
+ country = '';
128
+ currency = '';
129
+ businessUnit = '';
130
+ deviceModel = '';
131
+ deviceBrand = '';
132
+ firebaseManager = null; // FirebaseNotificationManager loaded dynamically
133
+ eventQueueManager = null;
134
+ flushManager = null;
135
+ networkMonitor = null;
136
+ batchConfig = _BatchConfig.DEFAULT_BATCH_CONFIG;
137
+ // Removed: isLoggingIn, isLoggingOut (now handled by AuthStateMachine)
138
+ // Removed: isSyncingPushPermission (no longer needed with new architecture)
139
+
140
+ resolveInitialization = () => {};
141
+
142
+ // New: Refactored architecture components
143
+
144
+ pushService = null;
145
+ pendingPushEventListeners = [];
146
+ appStateSubscription = null;
147
+
148
+ // Track if initial notification was already handled to prevent duplicate ACKs
149
+ initialNotificationHandled = false;
150
+ static styles = _reactNative.StyleSheet.create({
151
+ container: {
152
+ flex: 1,
153
+ backgroundColor: '#fff',
154
+ alignItems: 'center',
155
+ justifyContent: 'center'
156
+ },
157
+ modalBackground: {
158
+ flex: 1,
159
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
160
+ // Semi-transparent background
161
+ justifyContent: 'center',
162
+ // Vertically center
163
+ alignItems: 'center' // Horizontally center
164
+ },
165
+ popUpModalContent: {
166
+ flex: 1,
167
+ width: '85%',
168
+ marginTop: '15%',
169
+ alignItems: 'center',
170
+ justifyContent: 'center' // Center the popup content within the modal
171
+ },
172
+ headerModalContent: {
173
+ flex: 1,
174
+ width: '100%',
175
+ marginTop: '5%',
176
+ alignItems: 'center',
177
+ justifyContent: 'center'
178
+ },
179
+ footerModalContent: {
180
+ flex: 1,
181
+ width: '100%',
182
+ marginTop: '5%',
183
+ alignItems: 'center',
184
+ justifyContent: 'center'
185
+ },
186
+ fullScreenModalContent: {
187
+ flex: 1,
188
+ width: '100%',
189
+ marginTop: '5%',
190
+ alignItems: 'center',
191
+ justifyContent: 'center'
192
+ }
193
+ });
194
+ constructor(appId, config = {}) {
195
+ if (SwanSDK.instance) {
196
+ return SwanSDK.instance; // Return the existing instance
197
+ }
198
+ this.isProduction = config.isProduction || false ? 'PROD' : 'STAGE';
199
+ this.appId = appId;
200
+ this.deviceId = '';
201
+ this.notificationToShow = null;
202
+ this.currentScreenName = '';
203
+ this.country = '';
204
+ this.currency = '';
205
+ this.businessUnit = '';
206
+ this.deviceBrand = '';
207
+ this.deviceModel = '';
208
+ this.firebaseManager = null;
209
+
210
+ // Store config
211
+ this.config = config;
212
+
213
+ // Initialize state machines
214
+ this.deviceStateMachine = new _DeviceStateMachine.DeviceStateMachine();
215
+ this.pushStateMachine = new _PushStateMachine.PushStateMachine();
216
+ this.authStateMachine = new _AuthStateMachine.AuthStateMachine();
217
+
218
+ // Initialize services
219
+ this.deviceService = new _DeviceRegistrationService.DeviceRegistrationService(this.appId, this.isProduction, this.sendToSwan.bind(this),
220
+ // Use checkOnly mode for location during registration to avoid blocking on permission dialogs
221
+ () => this.getDeviceLocation(true));
222
+
223
+ // Push service will be initialized later if push is enabled
224
+
225
+ // Initialize the promise that gates access to SDK features
226
+ this.initializationPromise = new Promise(resolve => {
227
+ this.resolveInitialization = resolve;
228
+ });
229
+
230
+ // Enable logging if enabled in config
231
+ if (config.logging) {
232
+ this.enableLogs(config.logging);
233
+ }
234
+ SwanSDK.instance = this;
235
+ }
236
+ static getInstance(appId, config) {
237
+ if (!SwanSDK.instance) {
238
+ SwanSDK.instance = new SwanSDK(appId, config);
239
+ // triggers background init (now uses config from constructor)
240
+ SwanSDK.instance.initializeSDK();
241
+ }
242
+ return SwanSDK.instance;
243
+ }
244
+
245
+ /**
246
+ * Get current SDK instance (if initialized)
247
+ * Used internally by module-level handlers
248
+ */
249
+ static getCurrentInstance() {
250
+ return SwanSDK.instance || null;
251
+ }
252
+ async initializeSDK() {
253
+ try {
254
+ _Logger.default.log('[SwanSDK] Starting SDK initialization...');
255
+
256
+ // Phase 1: Core infrastructure (always runs)
257
+ _Logger.default.log('[SwanSDK] Phase 1: Initializing core infrastructure...');
258
+ await this.initializeDatabase();
259
+ await this.initializeEventQueue();
260
+
261
+ // Start network monitor
262
+ if (this.networkMonitor) {
263
+ // Network monitor already exists from old initialization
264
+ } else {
265
+ this.networkMonitor = new _NetworkMonitor.NetworkMonitor();
266
+ }
267
+
268
+ // Phase 2: Device registration (BACKGROUND - Non-blocking)
269
+ _Logger.default.log('[SwanSDK] Phase 2: Starting device registration in background...');
270
+ this.deviceStateMachine.register(() => this.deviceService.registerDevice()).then(async credentials => {
271
+ // Update deviceId from credentials
272
+ if (credentials?.deviceId) {
273
+ this.deviceId = credentials.deviceId;
274
+ }
275
+
276
+ // Ensure ackUrl is up to date in stored credentials (in case environment changed)
277
+ if (credentials && credentials.ackUrl !== _ApiUrls.default.WEBHOOK_MOBILE_PUSH_URL[this.isProduction]) {
278
+ _Logger.default.log('[SwanSDK] Updating stored ackUrl to match current environment');
279
+ await this.saveCredentials({
280
+ ...credentials,
281
+ ackUrl: _ApiUrls.default.WEBHOOK_MOBILE_PUSH_URL[this.isProduction]
282
+ });
283
+ }
284
+
285
+ // Sync AuthStateMachine state with stored credentials
286
+ this.authStateMachine.restoreState(!!credentials?.currentCDID);
287
+ _Logger.default.log('[SwanSDK] Device registered successfully:', this.deviceId);
288
+ this.emit('deviceRegistered', credentials);
289
+
290
+ // Flush any events that were queued before device registration
291
+ if (this.flushManager) {
292
+ _Logger.default.log('[SwanSDK] Device registered, flushing queued events...');
293
+ await this.flushManager.flush().catch(err => {
294
+ _Logger.default.warn('[SwanSDK] Failed to flush events after device registration:', err);
295
+ });
296
+ }
297
+ }).catch(error => {
298
+ _Logger.default.warn('[SwanSDK] Device registration failed, will retry on network restore:', error);
299
+ this.emit('deviceRegistrationFailed', error);
300
+ // Don't throw - SDK continues to work, events will queue locally
301
+ });
302
+
303
+ // SDK is now ready - emit initialized immediately (don't wait for device registration)
304
+ this.emit('initialized', {
305
+ success: true
306
+ });
307
+ _Logger.default.log('[SwanSDK] SDK initialization completed successfully (device registration continues in background)');
308
+
309
+ // Phase 3: Optional push notifications (non-blocking)
310
+ if (this.config.pushNotifications?.enabled) {
311
+ _Logger.default.log('[SwanSDK] Phase 3: Initializing push notifications...');
312
+ this.initializePushNotifications().catch(error => {
313
+ _Logger.default.error('[SwanSDK] Push initialization failed, continuing without push:', error);
314
+ this.emit('pushInitializationFailed', error);
315
+ });
316
+ } else {
317
+ _Logger.default.log('[SwanSDK] Push notifications disabled, skipping...');
318
+ }
319
+
320
+ // Phase 4: Optional location update (non-blocking)
321
+ _Logger.default.log('[SwanSDK] Phase 4: Updating location...');
322
+ this.updateLocation().catch(error => {
323
+ _Logger.default.warn('[SwanSDK] Location update failed:', error);
324
+ });
325
+ } catch (error) {
326
+ _Logger.default.error('[SwanSDK] SDK Initialization failed:', error);
327
+ throw error;
328
+ } finally {
329
+ this.resolveInitialization();
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Initialize push notifications (new method)
335
+ * Called only if push is enabled in config
336
+ */
337
+ async initializePushNotifications() {
338
+ try {
339
+ this.pushStateMachine.transition(_PushStateMachine.PushState.INITIALIZING);
340
+
341
+ // Load push provider (Firebase or custom)
342
+ const provider = await this.loadPushProvider(this.config.pushNotifications?.provider || 'firebase');
343
+
344
+ // Create push token service
345
+ this.pushService = new _PushTokenService.PushTokenService(provider, this.deviceService);
346
+
347
+ // Setup event listeners BEFORE initialize
348
+ this.setupPushEventListeners();
349
+
350
+ // Setup Notifee event listeners for click tracking
351
+ this.setupNotifeeEventListeners();
352
+
353
+ // Initialize the provider
354
+ await this.pushService.initialize();
355
+
356
+ // Attach any pending event listeners that were registered before initialization
357
+ this.attachPendingEventListeners();
358
+ this.pushStateMachine.transition(_PushStateMachine.PushState.READY);
359
+ _Logger.default.log('[SwanSDK] Push notification service ready');
360
+
361
+ // Auto-request permission if configured
362
+ if (this.config.pushNotifications?.autoRequestPermission) {
363
+ _Logger.default.log('[SwanSDK] Auto-requesting push permission...');
364
+ await this.pushService.requestPermission();
365
+ }
366
+ this.emit('pushNotificationsReady', {
367
+ success: true
368
+ });
369
+ } catch (error) {
370
+ this.pushStateMachine.transition(_PushStateMachine.PushState.ERROR);
371
+ _Logger.default.error('[SwanSDK] Push notification initialization error:', error);
372
+ throw error;
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Load push notification provider based on config
378
+ */
379
+ async loadPushProvider(providerType) {
380
+ if (providerType === 'firebase') {
381
+ // Dynamically import Firebase provider
382
+ const {
383
+ FirebasePushProvider
384
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('./providers/FirebasePushProvider')));
385
+ return new FirebasePushProvider();
386
+ } else if (providerType === 'custom' && this.config.pushNotifications?.customProvider) {
387
+ return this.config.pushNotifications.customProvider;
388
+ }
389
+
390
+ // Fallback to null provider
391
+ _Logger.default.warn('[SwanSDK] Unknown provider type, using NullPushProvider');
392
+ return new _NullPushProvider.NullPushProvider();
393
+ }
394
+
395
+ /**
396
+ * Setup event listeners for push token service
397
+ */
398
+ setupPushEventListeners() {
399
+ if (!this.pushService) return;
400
+
401
+ // Listen for token received/refreshed
402
+ this.pushService.on('tokenReceived', async token => {
403
+ _Logger.default.log('[SwanSDK] Push token received:', token);
404
+ try {
405
+ // Sync push subscription to backend
406
+ await this.syncPushSubscription(token);
407
+ this.pushStateMachine.transition(_PushStateMachine.PushState.ACTIVE);
408
+ this.emit('pushTokenUpdated', token);
409
+ } catch (error) {
410
+ _Logger.default.error('[SwanSDK] Failed to sync push token:', error);
411
+ }
412
+ });
413
+
414
+ // Listen for permission granted
415
+ this.pushService.on('permissionGranted', () => {
416
+ _Logger.default.log('[SwanSDK] Push permission granted');
417
+ this.pushStateMachine.transition(_PushStateMachine.PushState.TOKEN_PENDING);
418
+ });
419
+
420
+ // Listen for permission denied
421
+ this.pushService.on('permissionDenied', () => {
422
+ _Logger.default.warn('[SwanSDK] Push permission denied');
423
+ this.pushStateMachine.transition(_PushStateMachine.PushState.PERMISSION_DENIED);
424
+ });
425
+
426
+ // Listen for notifications (for ACK and auto-display)
427
+ this.pushService.on('notificationReceived', async notification => {
428
+ _Logger.default.log('[SwanSDK] ✅ Notification received in SDK:', notification);
429
+
430
+ // Send ACK
431
+ await this.sendNotificationAck(notification?.notification?.messageId, 'delivered');
432
+
433
+ // Display notification (always auto-display in foreground)
434
+ _Logger.default.log('[SwanSDK] 📱 Displaying foreground notification...');
435
+ await this.displayForegroundNotification(notification);
436
+ });
437
+ this.pushService.on('notificationOpened', async notification => {
438
+ _Logger.default.log('[SwanSDK] Notification opened:', notification);
439
+ await this.sendNotificationAck(notification?.notification?.messageId, 'clicked');
440
+ });
441
+ }
442
+
443
+ /**
444
+ * Sync push token to push-subscription API
445
+ * Now queues the call instead of making immediate network request
446
+ */
447
+ async syncPushSubscription(token) {
448
+ try {
449
+ const credentials = await this.getStoredCredentials();
450
+ if (!credentials?.deviceId) {
451
+ _Logger.default.warn('[SwanSDK] Cannot sync push subscription - device not registered');
452
+ return;
453
+ }
454
+ _Logger.default.log('[SwanSDK] Queueing push subscription sync...');
455
+
456
+ // Queue push subscribe call instead of direct network call
457
+ const queuedEvent = {
458
+ id: String(_reactNativeUuid.default.v4()),
459
+ eventName: 'PUSH_SUBSCRIBE',
460
+ // Special eventName for routing
461
+ eventData: {
462
+ pushNotificationToken: token,
463
+ subscribedAt: credentials.subscribedAt || new Date().toISOString()
464
+ },
465
+ timestamp: Date.now(),
466
+ priority: 0,
467
+ // Non-critical, can be queued
468
+ retryCount: 0,
469
+ status: 'pending',
470
+ createdAt: Date.now()
471
+ };
472
+ await this.eventQueueManager?.enqueue(queuedEvent);
473
+
474
+ // Enforce queue size limit
475
+ await this.eventQueueManager?.enforceQueueLimit(this.batchConfig.maxQueueSize);
476
+ _Logger.default.log('[SwanSDK] Push subscription queued successfully');
477
+
478
+ // Update stored credentials immediately (optimistic update)
479
+ // Don't wait for network - credentials updated now for immediate use
480
+ await this.saveCredentials({
481
+ ...credentials,
482
+ subscribedAt: credentials.subscribedAt || new Date().toISOString(),
483
+ lastSyncedPushToken: token,
484
+ pushNotificationToken: token // Store token locally
485
+ });
486
+
487
+ // Check if flush needed (triggers if batchSize reached)
488
+ await this.flushManager?.checkFlushNeeded();
489
+ } catch (error) {
490
+ _Logger.default.error('[SwanSDK] Failed to queue push subscription:', error);
491
+ throw error;
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Sync push unsubscription to push-subscription API
497
+ * Now queues the call instead of making immediate network request
498
+ */
499
+ async syncPushUnsubscription() {
500
+ try {
501
+ const credentials = await this.getStoredCredentials();
502
+ if (!credentials?.deviceId) {
503
+ _Logger.default.warn('[SwanSDK] Cannot sync push unsubscription - device not registered');
504
+ return;
505
+ }
506
+ _Logger.default.log('[SwanSDK] Queueing push unsubscription sync...');
507
+
508
+ // Queue push unsubscribe call instead of direct network call
509
+ const queuedEvent = {
510
+ id: String(_reactNativeUuid.default.v4()),
511
+ eventName: 'PUSH_UNSUBSCRIBE',
512
+ // Special eventName for routing
513
+ eventData: {
514
+ unSubscribedAt: new Date().toISOString()
515
+ },
516
+ timestamp: Date.now(),
517
+ priority: 0,
518
+ // Non-critical, can be queued
519
+ retryCount: 0,
520
+ status: 'pending',
521
+ createdAt: Date.now()
522
+ };
523
+ await this.eventQueueManager?.enqueue(queuedEvent);
524
+
525
+ // Enforce queue size limit
526
+ await this.eventQueueManager?.enforceQueueLimit(this.batchConfig.maxQueueSize);
527
+ _Logger.default.log('[SwanSDK] Push unsubscription queued successfully');
528
+
529
+ // Update stored credentials immediately (optimistic update)
530
+ await this.saveCredentials({
531
+ ...credentials,
532
+ pushNotificationToken: undefined,
533
+ subscribedAt: null,
534
+ unSubscribedAt: new Date().toISOString(),
535
+ lastSyncedPushToken: null
536
+ });
537
+
538
+ // Check if flush needed
539
+ await this.flushManager?.checkFlushNeeded();
540
+ } catch (error) {
541
+ _Logger.default.error('[SwanSDK] Failed to queue push unsubscription:', error);
542
+ throw error;
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Re-sync push subscription after profile switch (login/logout)
548
+ * Updates the CDID associated with the push token on the backend
549
+ */
550
+ async resyncPushSubscriptionAfterProfileSwitch() {
551
+ try {
552
+ const credentials = await this.getStoredCredentials();
553
+
554
+ // Only re-sync if there's an active push token
555
+ if (!credentials?.pushNotificationToken) {
556
+ _Logger.default.log('[SwanSDK] No active push token, skipping re-sync after profile switch');
557
+ return;
558
+ }
559
+ _Logger.default.log('[SwanSDK] Re-syncing push subscription with new CDID after profile switch...');
560
+
561
+ // Re-sync with the current CDID (logged-in or anonymous)
562
+ await this.syncPushSubscription(credentials.pushNotificationToken);
563
+ _Logger.default.log('[SwanSDK] Push subscription re-synced successfully with new profile');
564
+ } catch (error) {
565
+ _Logger.default.error('[SwanSDK] Failed to re-sync push subscription after profile switch:', error);
566
+ // Don't throw - this shouldn't block login/logout
567
+ }
568
+ }
569
+ async ensureInitialized() {
570
+ await this.initializationPromise;
571
+ }
572
+ addListener(event, callback) {
573
+ if (!this.listeners[event]) {
574
+ this.listeners[event] = [];
575
+ }
576
+ this.listeners[event].push(callback);
577
+
578
+ // Return a subscription object for easy cleanup
579
+ return {
580
+ remove: () => {
581
+ if (this.listeners[event]) {
582
+ // This comparison now has "overlap" because both are EventCallback
583
+ this.listeners[event] = this.listeners[event].filter(l => l !== callback);
584
+ }
585
+ }
586
+ };
587
+ }
588
+
589
+ // Internal method to trigger the update
590
+ emit(event, data) {
591
+ const eventListeners = this.listeners[event];
592
+ if (eventListeners) {
593
+ // Use a spread to avoid issues if a listener is removed during the loop
594
+ [...eventListeners].forEach(callback => callback(data));
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Emit notification opened event
600
+ * @internal - Used by module-level notification handlers
601
+ */
602
+ emitNotificationOpened(payload) {
603
+ this.emit(SwanSDK.EVENTS.NOTIFICATION_OPENED, payload);
604
+ }
605
+ static setLoggingEnabled(enabled) {
606
+ _Logger.default.enableLogs(enabled);
607
+ }
608
+ buttonClickHandler = null;
609
+ setButtonClickHandler(handler) {
610
+ this.buttonClickHandler = handler;
611
+ }
612
+ notifyButtonClick(event) {
613
+ this.buttonClickHandler?.(event);
614
+ this.sendNotificationAck(event.commId, event.event);
615
+ }
616
+ notificationShowHandler = null;
617
+ async setNotificationShowHandler(handler) {
618
+ this.notificationShowHandler = handler;
619
+ this.getNotificationComponent();
620
+ }
621
+ notifyNotificationShow(notification) {
622
+ this.notificationShowHandler?.(notification);
623
+ }
624
+ async initializeDatabase() {
625
+ try {
626
+ this.db = _reactNativeSqlite.default.openDatabase('test.db', '1.0', '', 1);
627
+ // Create tables after successful database open
628
+ await this.createTable('Notifications');
629
+ if (!this.isDatabaseConfigured) {
630
+ this.isDatabaseConfigured = true;
631
+
632
+ //Listen for network state changes
633
+ _netinfo.default.addEventListener(state => {
634
+ if (state.isConnected) {
635
+ _Logger.default.log('Network connected');
636
+ }
637
+ });
638
+ }
639
+ return this.db;
640
+ } catch (error) {
641
+ _Logger.default.error('Detailed Database Initialization Error:', error);
642
+ }
643
+ }
644
+ async initializeEventQueue() {
645
+ try {
646
+ // Initialize EventQueueManager with maxRetries from config
647
+ this.eventQueueManager = new _EventQueueManager.EventQueueManager(this.db, this.batchConfig.maxRetries);
648
+ await this.eventQueueManager.createTable();
649
+
650
+ // Recover stale events (from app crashes during flush)
651
+ await this.eventQueueManager.recoverStaleEvents();
652
+
653
+ // Initialize NetworkMonitor
654
+ this.networkMonitor = new _NetworkMonitor.NetworkMonitor();
655
+ this.networkMonitor.start();
656
+
657
+ // Add listener for network state changes to retry device registration
658
+ this.networkMonitor.addListener(isOnline => {
659
+ if (isOnline && !this.deviceId) {
660
+ _Logger.default.log('[SwanSDK] Network restored and device not registered, retrying device registration...');
661
+ this.deviceStateMachine.register(() => this.deviceService.registerDevice()).then(async credentials => {
662
+ if (credentials?.deviceId) {
663
+ this.deviceId = credentials.deviceId;
664
+ }
665
+ this.authStateMachine.restoreState(!!credentials?.currentCDID);
666
+ _Logger.default.log('[SwanSDK] Device registered successfully after network restore:', this.deviceId);
667
+ this.emit('deviceRegistered', credentials);
668
+
669
+ // Flush queued events
670
+ if (this.flushManager) {
671
+ await this.flushManager.flush().catch(err => {
672
+ _Logger.default.warn('[SwanSDK] Failed to flush events after retry:', err);
673
+ });
674
+ }
675
+ }).catch(error => {
676
+ _Logger.default.warn('[SwanSDK] Device registration retry failed:', error);
677
+ });
678
+ }
679
+ });
680
+
681
+ // Initialize FlushManager
682
+ this.flushManager = new _FlushManager.FlushManager(this.eventQueueManager, this.networkMonitor, this.batchConfig, this.sendEventBatch.bind(this));
683
+ this.flushManager.start();
684
+
685
+ // Track initial app launch event
686
+ _Logger.default.log('[SwanSDK] Tracking initial app launch event...');
687
+ this.appLaunched();
688
+
689
+ // AppState listener setup (always runs)
690
+ _Logger.default.log('[SwanSDK] Setting up AppState listener...');
691
+ this.setupAppStateListener();
692
+ _Logger.default.log('Event queue system initialized successfully');
693
+ } catch (error) {
694
+ _Logger.default.error('Failed to initialize event queue:', error);
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Set up AppState listener to track app lifecycle events
700
+ */
701
+ setupAppStateListener() {
702
+ // Clean up existing listener if any
703
+ if (this.appStateSubscription) {
704
+ this.appStateSubscription.remove();
705
+ }
706
+ this.appStateSubscription = _reactNative.AppState.addEventListener('change', nextAppState => {
707
+ _Logger.default.log('[SwanSDK] AppState changed to:', nextAppState);
708
+
709
+ // Track app launched event when app comes to foreground
710
+ if (nextAppState === 'active') {
711
+ _Logger.default.log('[SwanSDK] App came to foreground, tracking appLaunched event');
712
+ this.appLaunched();
713
+ }
714
+ });
715
+ _Logger.default.log('[SwanSDK] AppState listener set up successfully');
716
+ }
717
+ createTable(tableName) {
718
+ return new Promise((resolve, reject) => {
719
+ if (!this.db) {
720
+ return reject(new Error('Database not initialized'));
721
+ }
722
+ try {
723
+ this.db.transaction(function (txn) {
724
+ txn.executeSql(`CREATE TABLE IF NOT EXISTS ${tableName} (` + 'commId TEXT PRIMARY KEY, ' + 'cdid TEXT, ' + 'content TEXT)', [], function () {
725
+ resolve(); // Resolve the promise on successful table creation
726
+ }, function (_, error) {
727
+ _Logger.default.error('Table creation error:', error);
728
+ reject(error); // Reject the promise on SQL execution error
729
+ });
730
+ });
731
+ } catch (error) {
732
+ _Logger.default.error('Table creation error:', error);
733
+ reject(error); // Reject the promise on general error
734
+ }
735
+ });
736
+ }
737
+
738
+ // Ensure database is ready before operations
739
+ async ensureDatabaseReady() {
740
+ if (!this.db) {
741
+ _Logger.default.log('Database not initialized, attempting to initialize...');
742
+ await this.initializeDatabase();
743
+ }
744
+ return this.db;
745
+ }
746
+
747
+ //Insert notification into the table
748
+ async insertNotification(notification, tableName) {
749
+ return new Promise(async (resolve, reject) => {
750
+ const {
751
+ commId,
752
+ ...content
753
+ } = notification; // Extract `commId` and content
754
+
755
+ try {
756
+ await this.ensureDatabaseReady();
757
+ this.db.transaction(function (txn) {
758
+ txn.executeSql(`INSERT OR REPLACE INTO ${tableName} (commId, content) VALUES (?, ?);`, [commId, JSON.stringify(content)], function () {
759
+ resolve(); // Resolve the promise on successful insertion
760
+ }, function (_, error) {
761
+ _Logger.default.error('Error executing SQL:', error);
762
+ reject(error); // Reject the promise on SQL execution error
763
+ });
764
+ });
765
+ } catch (error) {
766
+ _Logger.default.error('Error inserting notification:', error);
767
+ reject(error); // Reject the promise on general error
768
+ }
769
+ });
770
+ }
771
+
772
+ //Fetch all notifications
773
+ async selectAllFromTable(tableName) {
774
+ return new Promise(async (resolve, reject) => {
775
+ try {
776
+ await this.ensureDatabaseReady();
777
+ let rows = [];
778
+ this.db.transaction(function (txn) {
779
+ txn.executeSql(`SELECT * FROM ${tableName};`, [], function (_, res) {
780
+ for (let i = 0; i < res.rows.length; ++i) {
781
+ rows.push(res.rows.item(i));
782
+ }
783
+ resolve(rows); // Resolve the promise with the collected rows
784
+ }, function (_, error) {
785
+ _Logger.default.error('Error executing SQL:', error);
786
+ reject(error); // Reject the promise on SQL execution error
787
+ });
788
+ });
789
+ } catch (error) {
790
+ _Logger.default.error('Error fetching notifications:', error);
791
+ reject(error); // Reject the promise on general error
792
+ }
793
+ });
794
+ }
795
+
796
+ //Delete notification by commId
797
+ async deleteFromTableByValue(tableName, key, value) {
798
+ return new Promise(async (resolve, reject) => {
799
+ try {
800
+ await this.ensureDatabaseReady();
801
+ this.db.transaction(function (txn) {
802
+ txn.executeSql(`DELETE FROM ${tableName} WHERE ${key} = ?;`, [value], function () {
803
+ resolve(); // Resolve the promise on successful deletion
804
+ }, function (_, error) {
805
+ _Logger.default.error('Error executing SQL:', error);
806
+ reject(error); // Reject the promise on SQL execution error
807
+ });
808
+ });
809
+ } catch (error) {
810
+ _Logger.default.error('Error deleting record:', error);
811
+ reject(error); // Reject the promise on general error
812
+ }
813
+ });
814
+ }
815
+ lzw64_encode(s) {
816
+ if (!s) return s;
817
+ var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
818
+ var d = new Map();
819
+ var s = unescape(encodeURIComponent(s)).split('');
820
+ var word = s[0];
821
+ var num = 256;
822
+ var key;
823
+ var o = [];
824
+ function out(word) {
825
+ key = word.length > 1 ? d.get(word) : word.charCodeAt(0);
826
+ o.push(b64[key & 0x3f]);
827
+ o.push(b64[key >> 6 & 0x3f]);
828
+ o.push(b64[key >> 12 & 0x3f]);
829
+ }
830
+ for (var i = 1; i < s.length; i++) {
831
+ var c = s[i];
832
+ if (d.has(word + c)) {
833
+ word += c;
834
+ } else {
835
+ d.set(word + c, num++);
836
+ out(word);
837
+ word = c;
838
+ if (num == (1 << 18) - 1) {
839
+ d.clear();
840
+ num = 256;
841
+ }
842
+ }
843
+ }
844
+ out(word);
845
+ return o.join('');
846
+ }
847
+ setCurrentScreenName(screenName) {
848
+ this.currentScreenName = screenName;
849
+ }
850
+ setCountry(country) {
851
+ this.country = country;
852
+ }
853
+ setCurrency(currency) {
854
+ this.currency = currency;
855
+ }
856
+ setBusinessUnit(businessUnit) {
857
+ this.businessUnit = businessUnit;
858
+ }
859
+ static generateId() {
860
+ return Math.random().toString(36).substr(2, 9);
861
+ }
862
+ async sendToSwan(url, data, encode = false) {
863
+ let timeoutId = null;
864
+ const isSdkAckUrl = url.toLowerCase().includes('post-in-app-notification-sdk-ack');
865
+ _Logger.default.log('isSdkAckUrl', isSdkAckUrl);
866
+ _Logger.default.log('url', url);
867
+ try {
868
+ const controller = new AbortController();
869
+ timeoutId = setTimeout(() => controller.abort(), 40000); // 10s timeout (reduced from 30s for better UX)
870
+
871
+ const deviceIdHeader = encode ? this.lzw64_encode(this.deviceId) : this.deviceId;
872
+ const bodyData = encode ? this.lzw64_encode(JSON.stringify(data)) : JSON.stringify(data);
873
+ _Logger.default.log('[SwanSDK] Sending request:', {
874
+ url,
875
+ encode,
876
+ deviceId: this.deviceId,
877
+ deviceIdHeader,
878
+ bodyLength: bodyData?.length || 0
879
+ });
880
+
881
+ // Log actual payload for debugging
882
+ _Logger.default.log('[SwanSDK] Request payload:', data);
883
+ const response = await fetch(url, {
884
+ method: 'POST',
885
+ headers: {
886
+ 'Content-Type': 'application/json',
887
+ 'X-Swan-Device-Id': deviceIdHeader
888
+ },
889
+ body: bodyData,
890
+ signal: controller.signal
891
+ });
892
+ clearTimeout(timeoutId);
893
+
894
+ // Handle HTTP errors
895
+ if (!response.ok) {
896
+ _Logger.default.error(`Swan API failed: ${response.status} ${response.statusText}`);
897
+ return null;
898
+ }
899
+ try {
900
+ return await response.json();
901
+ } catch (error) {
902
+ _Logger.default.error('Swan API response is not JSON', error, response);
903
+ // Non-JSON response (allowed)
904
+ return null;
905
+ }
906
+ } catch (error) {
907
+ if (error?.name === 'AbortError') {
908
+ _Logger.default.error('Swan API request timed out');
909
+ } else {
910
+ _Logger.default.error('Error sending data to Swan:', error);
911
+ }
912
+ return null;
913
+ } finally {
914
+ if (timeoutId) {
915
+ clearTimeout(timeoutId);
916
+ }
917
+ }
918
+ }
919
+
920
+ /**
921
+ * Send a single event directly to the server (SYNCHRONOUS - bypasses queue)
922
+ * Used for critical events like login/logout that require immediate processing
923
+ * @param eventName Event name
924
+ * @param eventData Event data
925
+ * @returns Server response with CDID and profileSwitched info
926
+ */
927
+ async sendEventDirectly(eventName, eventData) {
928
+ try {
929
+ const decodedCredentials = await this.getStoredCredentials();
930
+ if (!decodedCredentials) {
931
+ throw new Error('Credentials not found');
932
+ }
933
+ const currentCDID = decodedCredentials.currentCDID || null;
934
+ const generatedCDID = decodedCredentials.generatedCDID || null;
935
+ const deviceId = decodedCredentials.deviceId || this.deviceId;
936
+
937
+ // Build the single event payload (same structure as batch but with one event)
938
+ const batchPayload = {
939
+ common: {
940
+ appId: this.appId,
941
+ deviceId: deviceId,
942
+ sdkVersion: this.SDK_VERSION,
943
+ platform: _reactNative.Platform.OS
944
+ },
945
+ events: [{
946
+ id: String(_reactNativeUuid.default.v4()),
947
+ name: eventName,
948
+ timestamp: Date.now(),
949
+ data: eventData,
950
+ userId: currentCDID || generatedCDID,
951
+ currentCDID: currentCDID,
952
+ generatedCDID: generatedCDID
953
+ }],
954
+ isBatch: false // Single event, not a batch
955
+ };
956
+ _Logger.default.log(`Sending ${eventName} directly (synchronous, bypassing queue)...`);
957
+
958
+ // Send directly to server
959
+ const responseData = await this.sendToSwan(`${_ApiUrls.default.ECOM_TRACK_EVENT_URL[this.isProduction]}?appId=${this.appId}`, batchPayload, this.batchConfig.enableCompression);
960
+ if (responseData && responseData.results && responseData.results.length > 0) {
961
+ const result = responseData.results[0];
962
+ _Logger.default.log(`Direct send successful:`, result);
963
+ return {
964
+ success: result.success,
965
+ CDID: result.CDID,
966
+ profileSwitched: result.profileSwitched
967
+ };
968
+ }
969
+ throw new Error('Direct send failed - no response from server');
970
+ } catch (error) {
971
+ _Logger.default.error(`Error in sendEventDirectly for ${eventName}:`, error);
972
+ throw error;
973
+ }
974
+ }
975
+
976
+ /**
977
+ * Send batch of events to backend
978
+ * Routes different call types to appropriate endpoints
979
+ * Called by FlushManager when flushing queue
980
+ * @param events Array of events to send (includes standard events, push calls, enrichment, acks)
981
+ */
982
+ async sendEventBatch(events) {
983
+ try {
984
+ const decodedCredentials = await this.getStoredCredentials();
985
+ if (!decodedCredentials) {
986
+ throw new Error('Credentials not found');
987
+ }
988
+ const deviceId = decodedCredentials.deviceId || this.deviceId;
989
+ const results = [];
990
+
991
+ // Separate events by type using eventName
992
+ const ackEvents = events.filter(e => e.eventName === 'SWAN_NOTIFICATION_ACK');
993
+ const pushSubscribeEvents = events.filter(e => e.eventName === 'PUSH_SUBSCRIBE');
994
+ const pushUnsubscribeEvents = events.filter(e => e.eventName === 'PUSH_UNSUBSCRIBE');
995
+ const enrichProfileEvents = events.filter(e => e.eventName === 'PROFILE_ENRICH');
996
+ const standardEvents = events.filter(e => !['SWAN_NOTIFICATION_ACK', 'PUSH_SUBSCRIBE', 'PUSH_UNSUBSCRIBE', 'PROFILE_ENRICH'].includes(e.eventName));
997
+
998
+ // 1. Process Standard Events (Batch to /api/v2/trackEvent)
999
+ if (standardEvents.length > 0) {
1000
+ const batchPayload = {
1001
+ common: {
1002
+ appId: this.appId,
1003
+ deviceId: deviceId,
1004
+ sdkVersion: this.SDK_VERSION,
1005
+ platform: _reactNative.Platform.OS
1006
+ },
1007
+ events: standardEvents.map(e => ({
1008
+ id: e.id,
1009
+ name: e.eventName,
1010
+ timestamp: e.timestamp,
1011
+ data: e.eventData.data,
1012
+ userId: e.eventData.userId,
1013
+ currentCDID: e.eventData.currentCDID,
1014
+ generatedCDID: e.eventData.generatedCDID
1015
+ })),
1016
+ isBatch: true
1017
+ };
1018
+ const responseData = await this.sendToSwan(`${_ApiUrls.default.ECOM_TRACK_EVENT_URL[this.isProduction]}?appId=${this.appId}`, batchPayload, this.batchConfig.enableCompression);
1019
+ if (responseData && responseData.results) {
1020
+ results.push(...responseData.results);
1021
+ } else if (!responseData) {
1022
+ throw new Error('Batch request failed');
1023
+ }
1024
+ }
1025
+
1026
+ // 2. Process Push Subscribe Events (Individual to /api/device/push-subscription)
1027
+ if (pushSubscribeEvents.length > 0) {
1028
+ const pushPromises = pushSubscribeEvents.map(async event => {
1029
+ const {
1030
+ pushNotificationToken,
1031
+ subscribedAt
1032
+ } = event.eventData;
1033
+ const requestBody = {
1034
+ subscription: {
1035
+ pushNotificationToken,
1036
+ subscribed: true,
1037
+ lastLoginPlatform: _reactNative.Platform.OS,
1038
+ // SDK capabilities - tells backend what this SDK version supports
1039
+ sdkCapabilities: {
1040
+ dataOnlyPush: true,
1041
+ // This SDK handles data-only FCM messages
1042
+ version: this.SDK_VERSION
1043
+ }
1044
+ },
1045
+ status: 'updated',
1046
+ subscribedAt: subscribedAt || new Date().toISOString(),
1047
+ unSubscribedAt: null,
1048
+ linkedAt: decodedCredentials.deviceActivatedAt || null,
1049
+ CDID: decodedCredentials.currentCDID || decodedCredentials.generatedCDID,
1050
+ device: 'mobile'
1051
+ };
1052
+ const response = await this.sendToSwan(`${_ApiUrls.default.ECOM_PUSH_SUBSCRIPTION_URL[this.isProduction]}?appId=${this.appId}`, requestBody, true // enable compression
1053
+ );
1054
+ return {
1055
+ id: event.id,
1056
+ success: !!response,
1057
+ error: response ? null : 'Failed to sync push subscription'
1058
+ };
1059
+ });
1060
+ const pushResults = await Promise.all(pushPromises);
1061
+ results.push(...pushResults);
1062
+ }
1063
+
1064
+ // 3. Process Push Unsubscribe Events (Individual to /api/device/push-subscription)
1065
+ if (pushUnsubscribeEvents.length > 0) {
1066
+ const unpushPromises = pushUnsubscribeEvents.map(async event => {
1067
+ const {
1068
+ unSubscribedAt
1069
+ } = event.eventData;
1070
+ const requestBody = {
1071
+ subscription: null,
1072
+ status: 'revoked',
1073
+ subscribedAt: null,
1074
+ unSubscribedAt: unSubscribedAt || new Date().toISOString(),
1075
+ linkedAt: decodedCredentials.deviceActivatedAt || null,
1076
+ CDID: decodedCredentials.currentCDID || decodedCredentials.generatedCDID,
1077
+ device: 'mobile'
1078
+ };
1079
+ const response = await this.sendToSwan(`${_ApiUrls.default.ECOM_PUSH_SUBSCRIPTION_URL[this.isProduction]}?appId=${this.appId}`, requestBody, true);
1080
+ return {
1081
+ id: event.id,
1082
+ success: !!response,
1083
+ error: response ? null : 'Failed to sync push unsubscription'
1084
+ };
1085
+ });
1086
+ const unpushResults = await Promise.all(unpushPromises);
1087
+ results.push(...unpushResults);
1088
+ }
1089
+
1090
+ // 4. Process Profile Enrichment Events (Individual to /api/v2/customer/enrich-profile)
1091
+ if (enrichProfileEvents.length > 0) {
1092
+ const enrichPromises = enrichProfileEvents.map(async event => {
1093
+ const {
1094
+ profileData
1095
+ } = event.eventData;
1096
+ const requestBody = {
1097
+ ...profileData,
1098
+ CDID: decodedCredentials.currentCDID || decodedCredentials.generatedCDID
1099
+ };
1100
+ const response = await this.sendToSwan(`${_ApiUrls.default.ECOM_ENRICH_PROFILE_URL[this.isProduction]}?appId=${this.appId}`, requestBody, true);
1101
+ return {
1102
+ id: event.id,
1103
+ success: !!response,
1104
+ error: response ? null : 'Failed to enrich profile'
1105
+ };
1106
+ });
1107
+ const enrichResults = await Promise.all(enrichPromises);
1108
+ results.push(...enrichResults);
1109
+ }
1110
+
1111
+ // 5. Process Ack Events (Individual to /post-in-app-notification-sdk-ack)
1112
+ if (ackEvents.length > 0) {
1113
+ const ackPromises = ackEvents.map(async event => {
1114
+ const {
1115
+ commId,
1116
+ event: ackType
1117
+ } = event.eventData.data;
1118
+ const payload = {
1119
+ commId,
1120
+ appId: this.appId,
1121
+ CDID: event.eventData.currentCDID || event.eventData.generatedCDID,
1122
+ event: ackType,
1123
+ deviceId
1124
+ };
1125
+ const response = await this.sendToSwan(_ApiUrls.default.WEBHOOK_MOBILE_PUSH_URL[this.isProduction], payload);
1126
+ return {
1127
+ id: event.id,
1128
+ success: !!response,
1129
+ error: response ? null : 'Failed to send ACK'
1130
+ };
1131
+ });
1132
+ const ackResults = await Promise.all(ackPromises);
1133
+ results.push(...ackResults);
1134
+ }
1135
+ return {
1136
+ results
1137
+ };
1138
+ } catch (error) {
1139
+ _Logger.default.error('Error sending event batch:', error);
1140
+ throw error;
1141
+ }
1142
+ }
1143
+ async getSwanIdentifier() {
1144
+ await this.ensureInitialized();
1145
+ const decodedCredentials = await this.getStoredCredentials();
1146
+ return decodedCredentials?.currentCDID || decodedCredentials?.generatedCDID;
1147
+ }
1148
+
1149
+ // Low-level Storage Helpers
1150
+ async getStoredCredentials(key = 'swanCredentials') {
1151
+ const encoded = await _asyncStorage.default.getItem(key);
1152
+ return encoded ? JSON.parse(_reactNativeBase.default.decode(encoded)) : null;
1153
+ }
1154
+ async saveCredentials(data, key = 'swanCredentials') {
1155
+ const encoded = _reactNativeBase.default.encode(JSON.stringify(data));
1156
+ await _asyncStorage.default.setItem(key, encoded);
1157
+ this.emit('deviceInfoChanged', data);
1158
+
1159
+ // Save to iOS App Group for Notification Service Extension (iOS only)
1160
+ if (key === 'swanCredentials' && _reactNative.Platform.OS === 'ios') {
1161
+ await _SharedCredentialsManager.SharedCredentialsManager.saveToAppGroup({
1162
+ appId: this.appId,
1163
+ deviceId: data.deviceId,
1164
+ cdid: data.currentCDID || data.generatedCDID,
1165
+ ackUrl: _ApiUrls.default.WEBHOOK_MOBILE_PUSH_URL[this.isProduction],
1166
+ isProduction: this.isProduction
1167
+ });
1168
+ }
1169
+ }
1170
+ async hasLocationPermission(checkOnly = false) {
1171
+ // --- ANDROID CHECK ---
1172
+ if (_reactNative.Platform.OS === 'android') {
1173
+ const status = await _reactNative.PermissionsAndroid.check(_reactNative.PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
1174
+ if (status) return true;
1175
+
1176
+ // If checkOnly mode, don't request permission (used during initialization to avoid blocking)
1177
+ if (checkOnly) {
1178
+ return false;
1179
+ }
1180
+ const request = await _reactNative.PermissionsAndroid.request(_reactNative.PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
1181
+ return request === _reactNative.PermissionsAndroid.RESULTS.GRANTED;
1182
+ }
1183
+
1184
+ // --- iOS CHECK ---
1185
+ if (_reactNative.Platform.OS === 'ios') {
1186
+ // If checkOnly mode, don't request (used during initialization)
1187
+ if (checkOnly) {
1188
+ return false; // Just return false, don't trigger permission dialog
1189
+ }
1190
+
1191
+ // This will trigger the Info.plist strings we discussed
1192
+ // It handles the 'Not Determined' -> 'Requested' flow automatically
1193
+ try {
1194
+ _geolocation.default.requestAuthorization();
1195
+ return true; // iOS handles the dialog; if denied, getCurrentPosition will trigger the error callback
1196
+ } catch (e) {
1197
+ return false;
1198
+ }
1199
+ }
1200
+ return false;
1201
+ }
1202
+
1203
+ /**
1204
+ * Get current device location
1205
+ * Returns location data with latitude, longitude, accuracy, and timestamp
1206
+ * Returns null if location permission is not granted or location fetch fails
1207
+ *
1208
+ * @param checkOnly - If true, only checks for existing permission without requesting (non-blocking)
1209
+ */
1210
+ async getDeviceLocation(checkOnly = false) {
1211
+ const hasPermission = await this.hasLocationPermission(checkOnly);
1212
+ if (!hasPermission) {
1213
+ _Logger.default.log('[SwanSDK] Location permission not granted (checkOnly mode), skipping location');
1214
+ return null;
1215
+ }
1216
+ return new Promise(resolve => {
1217
+ _geolocation.default.getCurrentPosition(position => {
1218
+ _Logger.default.log(position);
1219
+ const {
1220
+ latitude,
1221
+ longitude,
1222
+ accuracy
1223
+ } = position.coords;
1224
+ const locationData = {
1225
+ latitude,
1226
+ longitude,
1227
+ accuracy,
1228
+ // Useful to know if you can trust this for specific tax/zip math
1229
+ timestamp: position.timestamp
1230
+ };
1231
+ _Logger.default.log('Ecom Location Captured:', locationData);
1232
+ resolve(locationData);
1233
+ }, error => {
1234
+ _Logger.default.warn('Location Access Denied or Failed:', error.message);
1235
+ resolve(null);
1236
+ }, {
1237
+ enableHighAccuracy: false,
1238
+ // Better for battery; uses WiFi/Cell which is enough for E-com
1239
+ timeout: 10000 // 10s is plenty for a retail app
1240
+ });
1241
+ });
1242
+ }
1243
+
1244
+ /**
1245
+ * Register device (simplified)
1246
+ * Now delegates to DeviceStateMachine and DeviceRegistrationService
1247
+ *
1248
+ * @deprecated This method is kept for backward compatibility but is no longer needed
1249
+ * in most cases as device registration happens automatically during SDK initialization.
1250
+ */
1251
+ async registerDevice() {
1252
+ // Use state machine to register device
1253
+ await this.deviceStateMachine.register(() => this.deviceService.registerDevice());
1254
+ }
1255
+
1256
+ /**
1257
+ * Enriches an existing customer profile with additional data.
1258
+ * All fields are validated against metastore custom attributes
1259
+ * Note: Profile creation happens automatically during device registration.
1260
+ * This method only updates existing profiles.
1261
+ *
1262
+ * The CDID is automatically retrieved from AsyncStorage:
1263
+ * - Uses currentCDID (credentials) if user is logged in
1264
+ * - Uses generatedCDID if user is anonymous
1265
+ *
1266
+ * BREAKING CHANGE: Now returns void instead of server response
1267
+ * Enrichment is processed asynchronously via queue
1268
+ *
1269
+ * @param profileData - Customer profile data (CDID not required, automatically retrieved)
1270
+ * @returns Promise<void> - No return value (async processing)
1271
+ */
1272
+ async enrichProfile(profileData) {
1273
+ try {
1274
+ const decodedCredentials = await this.getStoredCredentials();
1275
+ if (!decodedCredentials) {
1276
+ throw new Error('Credential not found! Please wait for Swan to register the device!');
1277
+ }
1278
+ _Logger.default.log('[SwanSDK] Queueing profile enrichment...');
1279
+
1280
+ // Queue enrichment call instead of direct network call
1281
+ const queuedEvent = {
1282
+ id: String(_reactNativeUuid.default.v4()),
1283
+ eventName: 'PROFILE_ENRICH',
1284
+ // Special eventName for routing
1285
+ eventData: {
1286
+ profileData // Store profileData in eventData for sendEventBatch
1287
+ },
1288
+ timestamp: Date.now(),
1289
+ priority: 0,
1290
+ // Non-critical, can be queued
1291
+ retryCount: 0,
1292
+ status: 'pending',
1293
+ createdAt: Date.now()
1294
+ };
1295
+ await this.eventQueueManager?.enqueue(queuedEvent);
1296
+
1297
+ // Enforce queue size limit
1298
+ await this.eventQueueManager?.enforceQueueLimit(this.batchConfig.maxQueueSize);
1299
+ _Logger.default.log('[SwanSDK] Profile enrichment queued successfully');
1300
+
1301
+ // Check if flush needed
1302
+ await this.flushManager?.checkFlushNeeded();
1303
+
1304
+ // NOTE: Return type changed from Promise<any> to Promise<void>
1305
+ // Callers no longer receive immediate response - async handling only
1306
+ } catch (error) {
1307
+ _Logger.default.error('[SwanSDK] Failed to queue profile enrichment:', error);
1308
+ throw error;
1309
+ }
1310
+ }
1311
+ async switchProfile(cdid, options) {
1312
+ try {
1313
+ let decodedCredentials = await this.getStoredCredentials();
1314
+ if (!decodedCredentials) {
1315
+ throw new Error('Credential not found! Please wait for Swan to register the device!');
1316
+ }
1317
+ if (options?.timeOfLogin) {
1318
+ decodedCredentials.timeOfLogin = options.timeOfLogin;
1319
+ }
1320
+ decodedCredentials = {
1321
+ ...decodedCredentials,
1322
+ currentCDID: cdid,
1323
+ profileSwitchedAt: new Date().toISOString()
1324
+ };
1325
+ await this.saveCredentials(decodedCredentials);
1326
+ } catch (error) {
1327
+ _Logger.default.error('Error in switching profile:', error);
1328
+ }
1329
+ }
1330
+ async getSessionId() {
1331
+ try {
1332
+ // Retrieve the session from AsyncStorage
1333
+ const sessionBase64 = await _asyncStorage.default.getItem('_swanSessionId');
1334
+ if (sessionBase64) {
1335
+ const sessionDoc = JSON.parse(_reactNativeBase.default.decode(sessionBase64));
1336
+ const lastActiveTime = new Date(sessionDoc.lastActiveTime).getTime();
1337
+ const currentDate = new Date();
1338
+ const currentTime = currentDate.getTime();
1339
+ const sessionDuration = 20 * 60 * 1000; // 20 minutes in milliseconds
1340
+
1341
+ // Check if the session is still active
1342
+ if (currentTime - lastActiveTime < sessionDuration) {
1343
+ // Update last active time and save the session back
1344
+ sessionDoc.lastActiveTime = currentDate.toISOString();
1345
+ const updatedSessionDocBase64 = _reactNativeBase.default.encode(JSON.stringify(sessionDoc));
1346
+ await _asyncStorage.default.setItem('_swanSessionId', updatedSessionDocBase64);
1347
+ return sessionDoc.sessionId;
1348
+ }
1349
+ }
1350
+
1351
+ // Create a new session if no valid session exists
1352
+ const newSessionDoc = {
1353
+ sessionId: _reactNativeUuid.default.v4(),
1354
+ // Use UUID for session ID
1355
+ lastActiveTime: new Date().toISOString()
1356
+ };
1357
+ const newSessionDocBase64 = _reactNativeBase.default.encode(JSON.stringify(newSessionDoc));
1358
+ await _asyncStorage.default.setItem('_swanSessionId', newSessionDocBase64);
1359
+ return newSessionDoc.sessionId;
1360
+ } catch (error) {
1361
+ _Logger.default.error('Error in getSessionId:', error);
1362
+ return null; // Return null in case of error
1363
+ }
1364
+ }
1365
+ async trackEvent(eventName, eventData) {
1366
+ try {
1367
+ await this.ensureInitialized();
1368
+
1369
+ // Wait for device registration if it's in progress
1370
+ // This is non-blocking from the host app's perspective since trackEvent returns immediately
1371
+ // The event will be queued once device registration completes
1372
+ let decodedCredentials = await this.getStoredCredentials();
1373
+ if (!decodedCredentials) {
1374
+ _Logger.default.log('[SwanSDK] Device not registered yet, waiting for registration to complete...');
1375
+
1376
+ // Wait for device registration (either in progress or will retry on network restore)
1377
+ // If registration fails, we'll retry when the network comes back
1378
+ const maxWaitTime = 15000; // Wait max 15 seconds
1379
+ const startTime = Date.now();
1380
+ while (!decodedCredentials && Date.now() - startTime < maxWaitTime) {
1381
+ await new Promise(resolve => setTimeout(resolve, 500)); // Check every 500ms
1382
+ decodedCredentials = await this.getStoredCredentials();
1383
+ }
1384
+ if (!decodedCredentials) {
1385
+ throw new Error('Device registration is taking longer than expected. Please check your network connection. Events will be queued and sent when device is registered.');
1386
+ }
1387
+ }
1388
+
1389
+ // Extract all ID fields from credentials
1390
+ const currentCDID = decodedCredentials.currentCDID || null;
1391
+ const generatedCDID = decodedCredentials.generatedCDID || null;
1392
+ const deviceId = decodedCredentials.deviceId || this.deviceId;
1393
+
1394
+ // Get device info if not already set
1395
+ if (!this.deviceModel || !this.deviceBrand) {
1396
+ this.deviceModel = _reactNativeDeviceInfo.default.getModel();
1397
+ this.deviceBrand = _reactNativeDeviceInfo.default.getBrand();
1398
+ }
1399
+
1400
+ // Build event payload with metadata
1401
+ const enrichedEventData = {
1402
+ ...eventData,
1403
+ platform: _reactNative.Platform.OS,
1404
+ osModal: _reactNative.Platform.Version,
1405
+ deviceModal: this.deviceModel,
1406
+ deviceBrand: this.deviceBrand,
1407
+ country: this.country,
1408
+ currency: this.currency,
1409
+ businessUnit: this.businessUnit,
1410
+ deviceId: deviceId,
1411
+ sessionId: await this.getSessionId()
1412
+ };
1413
+
1414
+ // Construct the complete event payload
1415
+ const payload = {
1416
+ userId: currentCDID || generatedCDID,
1417
+ currentCDID: currentCDID,
1418
+ generatedCDID: generatedCDID,
1419
+ deviceId: deviceId,
1420
+ name: eventName,
1421
+ data: enrichedEventData
1422
+ };
1423
+
1424
+ // Create queued event object
1425
+ const queuedEvent = {
1426
+ id: String(_reactNativeUuid.default.v4()),
1427
+ eventName,
1428
+ eventData: payload,
1429
+ timestamp: Date.now(),
1430
+ priority: 0,
1431
+ // All events queued normally (use sendEventDirectly for synchronous)
1432
+ retryCount: 0,
1433
+ status: 'pending',
1434
+ createdAt: Date.now()
1435
+ };
1436
+
1437
+ // Enqueue the event
1438
+ await this.eventQueueManager?.enqueue(queuedEvent);
1439
+
1440
+ // Enforce queue size limit
1441
+ await this.eventQueueManager?.enforceQueueLimit(this.batchConfig.maxQueueSize);
1442
+ _Logger.default.log(`Event queued: ${eventName}`);
1443
+
1444
+ // Check if we need to flush based on queue size
1445
+ await this.flushManager?.checkFlushNeeded();
1446
+ return {
1447
+ queued: true,
1448
+ eventId: queuedEvent.id
1449
+ };
1450
+ } catch (error) {
1451
+ _Logger.default.error('Error in trackEvent:', error);
1452
+ throw error;
1453
+ }
1454
+ }
1455
+
1456
+ /**
1457
+ * Track custom event
1458
+ * To use in segments, journeys it needs to be added to metastore
1459
+ * @param name Event name
1460
+ * @param data Event data
1461
+ */
1462
+ customEvent(name, data) {
1463
+ this.trackEvent(name, data || {});
1464
+ }
1465
+
1466
+ /**
1467
+ * Track app launched event
1468
+ * Automatically triggered when app comes to foreground from background
1469
+ * Can also be called manually if needed
1470
+ * @param {any} data - Optional event data
1471
+ */
1472
+ appLaunched(data) {
1473
+ this.trackEvent(ECOM_EVENTS.APP_LAUNCHED, data || {});
1474
+ }
1475
+
1476
+ /**
1477
+ * @param { { success: boolean } } data
1478
+ */
1479
+ forgotPassword(data) {
1480
+ this.trackEvent(ECOM_EVENTS.FORGOT_PASSWORD, data);
1481
+ }
1482
+ /**
1483
+ * @param { { searchKeyword: string } } data
1484
+ */
1485
+ search(data) {
1486
+ this.trackEvent(ECOM_EVENTS.SEARCH, data);
1487
+ }
1488
+ /**
1489
+ * @param { { productId: string } } data
1490
+ */
1491
+ productViewed(data) {
1492
+ this.trackEvent(ECOM_EVENTS.PRODUCT_VIEWED, data);
1493
+ }
1494
+ /**
1495
+ * @param { { productId: string } } data
1496
+ */
1497
+ productClicked(data) {
1498
+ this.trackEvent(ECOM_EVENTS.PRODUCT_CLICKED, data);
1499
+ }
1500
+ /**
1501
+ * @param { { productListId: string } } data
1502
+ */
1503
+ productListViewed(data) {
1504
+ this.trackEvent(ECOM_EVENTS.PRODUCT_LIST_VIEWED, data);
1505
+ }
1506
+ /**
1507
+ * @param { { productId: string, quantity: string } } data
1508
+ */
1509
+ productAddedToAddTocart(data) {
1510
+ this.trackEvent(ECOM_EVENTS.PRODUCT_ADDED_TO_ADD_TO_CART, data);
1511
+ }
1512
+ /**
1513
+ * @param { { productId: string, quantity: string } } data
1514
+ */
1515
+ productRemovedFromAddToCart(data) {
1516
+ this.trackEvent(ECOM_EVENTS.PRODUCT_REMOVED_FROM_ADD_TO_CART, data);
1517
+ }
1518
+ clearCart() {
1519
+ this.trackEvent(ECOM_EVENTS.CLEAR_CART, {});
1520
+ }
1521
+ /**
1522
+ * @param { { categoryId: string } } data
1523
+ */
1524
+ selectCategory(data) {
1525
+ this.trackEvent(ECOM_EVENTS.SELECT_CATEGORY, data);
1526
+ }
1527
+ /**
1528
+ * @param { { categoryId: string } } data
1529
+ */
1530
+ categoryViewedPage(data) {
1531
+ this.trackEvent(ECOM_EVENTS.CATEGORY_VIEWED_PAGE, data);
1532
+ }
1533
+ /**
1534
+ * @param { { productId: string } } data
1535
+ */
1536
+ productAddedToWishlist(data) {
1537
+ this.trackEvent(ECOM_EVENTS.PRODUCT_ADDED_TO_WISHLIST, data);
1538
+ }
1539
+ /**
1540
+ * @param { { productId: string } } data
1541
+ */
1542
+ productRemovedFromWishlist(data) {
1543
+ this.trackEvent(ECOM_EVENTS.PRODUCT_REMOVED_FROM_WISHLIST, data);
1544
+ }
1545
+ /**
1546
+ * @param { { productId: string, rateValue: string, rateSubjectId: string } } data
1547
+ */
1548
+ productRatedOrReviewed(data) {
1549
+ this.trackEvent(ECOM_EVENTS.PRODUCT_RATED_OR_REVIEWED, data);
1550
+ }
1551
+ /**
1552
+ * @param { { productIds: string[] } } data
1553
+ */
1554
+ cartViewed(data) {
1555
+ this.trackEvent(ECOM_EVENTS.CART_VIEWED, data);
1556
+ }
1557
+ /**
1558
+ * @param { { couponCode: string, orderId: string, expiryDate: Date } } data
1559
+ */
1560
+ offerAvailed(data) {
1561
+ this.trackEvent(ECOM_EVENTS.OFFER_AVAILED, data);
1562
+ }
1563
+ /**
1564
+ * @param { { checkoutId: string, orderId: string, totalAmount: float, productIds: {productId: string, quantity: float, price: float}[] } } data
1565
+ */
1566
+ checkoutStarted(data) {
1567
+ this.trackEvent(ECOM_EVENTS.CHECKOUT_STARTED, data);
1568
+ }
1569
+ /**
1570
+ * @param { { checkoutId: string, orderId: string, totalAmount: float, productIds: {productId: string, quantity: float, price: float}[] } } data
1571
+ */
1572
+ checkoutCompleted(data) {
1573
+ this.trackEvent(ECOM_EVENTS.CHECKOUT_COMPLETED, data);
1574
+ }
1575
+ /**
1576
+ * @param { { checkoutId: string, orderId: string, productIds: {productId: string, quantity: float, price: float}[] } } data
1577
+ */
1578
+ checkoutCanceled(data) {
1579
+ this.trackEvent(ECOM_EVENTS.CHECKOUT_CANCELED, data);
1580
+ }
1581
+ /**
1582
+ * @param { { currency: string, value: string, productIds: string[], paymentType: string } } data
1583
+ */
1584
+ paymentInfoEntered(data) {
1585
+ this.trackEvent(ECOM_EVENTS.PAYMENT_INFO_ENTERED, data);
1586
+ }
1587
+ /**
1588
+ * @param { { orderId: string, totalAmount: float, productIds: {productId: string, quantity: float, price: float}[] } } data
1589
+ */
1590
+ orderCompleted(data) {
1591
+ this.trackEvent(ECOM_EVENTS.ORDER_COMPLETED, data);
1592
+ }
1593
+ /**
1594
+ * @param { { orderId: string, totalAmount: float, productIds: {productId: string, quantity: float, price: float}[] } } data
1595
+ */
1596
+ orderRefunded(data) {
1597
+ this.trackEvent(ECOM_EVENTS.ORDER_REFUNDED, data);
1598
+ }
1599
+ /**
1600
+ * @param { { orderId: string, totalAmount: float, productIds: {productId: string, quantity: float, price: float}[] } } data
1601
+ */
1602
+ orderCancelled(data) {
1603
+ this.trackEvent(ECOM_EVENTS.ORDER_CANCELLED, data);
1604
+ }
1605
+ /**
1606
+ * @param { { orderId: string, rateValue: float, rateSubjectId: string } } data
1607
+ */
1608
+ orderExperianceRating(data) {
1609
+ this.trackEvent(ECOM_EVENTS.ORDER_EXPERIANCE_RATING, data);
1610
+ }
1611
+ /**
1612
+ * @param { { productId: string, deliveryType: string, extraNote: string, rateValue: string, rateSubjectId: string } } data
1613
+ */
1614
+ productReview(data) {
1615
+ this.trackEvent(ECOM_EVENTS.PRODUCT_REVIEW, data);
1616
+ }
1617
+ /**
1618
+ * @param { { orderId: string, brandId: string, sku: string, purchaseDate: string, orderCreatedDate: string } } data
1619
+ */
1620
+ purchased(data) {
1621
+ this.trackEvent(ECOM_EVENTS.PURCHASED, data);
1622
+ }
1623
+ /**
1624
+ * @param { { appVersion: string, updateType: string, previousVersion: string, updateId: string } } data
1625
+ */
1626
+ appUpdated(data) {
1627
+ this.trackEvent(ECOM_EVENTS.APP_UPDATED, data);
1628
+ }
1629
+ /**
1630
+ * @param { { apiCode: string, success: boolean, comment: string } } data
1631
+ */
1632
+ accountDeletion(data) {
1633
+ this.trackEvent(ECOM_EVENTS.ACCOUNT_DELETION, data);
1634
+ }
1635
+ /**
1636
+ * @param { { productId: string } } data
1637
+ */
1638
+ share(data) {
1639
+ this.trackEvent(ECOM_EVENTS.SHARE, data);
1640
+ }
1641
+ /**
1642
+ * @param { { screenName: string } } data
1643
+ */
1644
+ screen(data) {
1645
+ this.trackEvent(ECOM_EVENTS.SCREEN, data);
1646
+ }
1647
+ /**
1648
+ * @param { { wishlistId: string, productId: string } } data
1649
+ */
1650
+ wishlistProductAddedToCart(data) {
1651
+ this.trackEvent(ECOM_EVENTS.WISHLIST_PRODUCT_ADDED_TO_CART, data);
1652
+ }
1653
+ /**
1654
+ * @param { { productId: string, orderId: string, price: float, postalCode: string } } data
1655
+ */
1656
+ shipped(data) {
1657
+ this.trackEvent(ECOM_EVENTS.SHIPPED, data);
1658
+ }
1659
+ /**
1660
+ * @param { { quantity: string, productId: string } } data
1661
+ */
1662
+ productQuantitySelected(data) {
1663
+ this.trackEvent(ECOM_EVENTS.PRODUCT_QUANTITY_SELECTED, data);
1664
+ }
1665
+
1666
+ /**
1667
+ * Track user login event and handle profile switching if mobile is provided
1668
+ * @param identifier - identifier to check for profile switching
1669
+ * @param data - Optional customer profile data
1670
+ * @returns Promise with profile switch information if applicable
1671
+ */
1672
+ /**
1673
+ * Track user login event (SYNCHRONOUS - bypasses queue)
1674
+ * Uses AuthStateMachine to prevent concurrent login calls
1675
+ * Sends directly to server for immediate profile switching
1676
+ */
1677
+ async login(identifier, data) {
1678
+ return await this.authStateMachine.login(async () => {
1679
+ await this.ensureInitialized();
1680
+
1681
+ // Get device info if not already set
1682
+ if (!this.deviceModel || !this.deviceBrand) {
1683
+ this.deviceModel = _reactNativeDeviceInfo.default.getModel();
1684
+ this.deviceBrand = _reactNativeDeviceInfo.default.getBrand();
1685
+ }
1686
+
1687
+ // Build event data with all required metadata
1688
+ const eventData = {
1689
+ timeOfLogin: new Date().toISOString(),
1690
+ identifier,
1691
+ profileData: data,
1692
+ platform: _reactNative.Platform.OS,
1693
+ osModal: _reactNative.Platform.Version,
1694
+ deviceModal: this.deviceModel,
1695
+ deviceBrand: this.deviceBrand,
1696
+ country: this.country,
1697
+ currency: this.currency,
1698
+ businessUnit: this.businessUnit,
1699
+ sessionId: await this.getSessionId()
1700
+ };
1701
+
1702
+ // CRITICAL: Flush existing queue BEFORE login to avoid CDID mismatch
1703
+ _Logger.default.log('Login: Flushing existing queue before profile switch...');
1704
+ await this.flushManager?.flush(true);
1705
+
1706
+ // Send login event DIRECTLY (synchronous, bypasses queue)
1707
+ const response = await this.sendEventDirectly(ECOM_EVENTS.USER_LOGIN, eventData);
1708
+
1709
+ // Handle profile switching if backend switched profiles
1710
+ if (response && response.profileSwitched) {
1711
+ _Logger.default.log(`Profile switched on login to ${response.CDID}. Updating stored credentials...`);
1712
+
1713
+ // Update stored credentials to use the new CDID
1714
+ await this.switchProfile(response.CDID, {
1715
+ timeOfLogin: eventData.timeOfLogin
1716
+ });
1717
+
1718
+ // Re-sync push subscription with new logged-in CDID
1719
+ await this.resyncPushSubscriptionAfterProfileSwitch();
1720
+
1721
+ // emit swan identifier change
1722
+ this.emit('swanIdentifierChanged', await this.getSwanIdentifier());
1723
+ _Logger.default.log('Credentials updated with new CDID:', response.CDID);
1724
+ }
1725
+ return response;
1726
+ });
1727
+ }
1728
+
1729
+ /**
1730
+ * Track user logout event and switch back to anonymous profile (generatedCDID)
1731
+ * Uses AuthStateMachine to prevent concurrent logout calls (SYNCHRONOUS - bypasses queue)
1732
+ * Sends directly to server for immediate profile switching
1733
+ * @returns Promise with logout response
1734
+ */
1735
+ async logout() {
1736
+ return await this.authStateMachine.logout(async () => {
1737
+ await this.ensureInitialized();
1738
+ const decodedCredentials = await this.getStoredCredentials();
1739
+ if (!decodedCredentials) {
1740
+ throw new Error('Credential not found! Please wait for Swan to register the device!');
1741
+ }
1742
+
1743
+ // Get device info if not already set
1744
+ if (!this.deviceModel || !this.deviceBrand) {
1745
+ this.deviceModel = _reactNativeDeviceInfo.default.getModel();
1746
+ this.deviceBrand = _reactNativeDeviceInfo.default.getBrand();
1747
+ }
1748
+
1749
+ // Build event data with all required metadata
1750
+ const eventData = {
1751
+ timeOfLogin: decodedCredentials.timeOfLogin,
1752
+ platform: _reactNative.Platform.OS,
1753
+ osModal: _reactNative.Platform.Version,
1754
+ deviceModal: this.deviceModel,
1755
+ deviceBrand: this.deviceBrand,
1756
+ country: this.country,
1757
+ currency: this.currency,
1758
+ businessUnit: this.businessUnit,
1759
+ sessionId: await this.getSessionId()
1760
+ };
1761
+
1762
+ // CRITICAL: Flush existing queue BEFORE logout to avoid CDID mismatch
1763
+ _Logger.default.log('Logout: Flushing existing queue before profile switch...');
1764
+ await this.flushManager?.flush(true);
1765
+
1766
+ // Try to send logout event to server (non-blocking)
1767
+ // If this fails, user is already logged out locally
1768
+ let response = null;
1769
+ try {
1770
+ response = await this.sendEventDirectly(ECOM_EVENTS.USER_LOGOUT, eventData);
1771
+ _Logger.default.log('Logout event sent to server successfully');
1772
+ } catch (error) {
1773
+ _Logger.default.warn('Failed to send logout event to server (user is logged out locally):', error);
1774
+ }
1775
+
1776
+ // Even if server call fails, user should be logged out locally
1777
+ decodedCredentials.currentCDID = null;
1778
+ decodedCredentials.timeOfLogin = null;
1779
+
1780
+ // Save updated credentials back to AsyncStorage
1781
+ await this.saveCredentials(decodedCredentials);
1782
+ _Logger.default.log(`User logged out locally. Switched to anonymous profile: ${decodedCredentials.generatedCDID}`);
1783
+
1784
+ // Re-sync push subscription with anonymous CDID
1785
+ await this.resyncPushSubscriptionAfterProfileSwitch();
1786
+
1787
+ // Emit swan identifier change
1788
+ this.emit('swanIdentifierChanged', await this.getSwanIdentifier());
1789
+ return response;
1790
+ });
1791
+ }
1792
+
1793
+ // /**
1794
+ // * Create table for storing pending notification ACKs
1795
+ // */
1796
+
1797
+ /**
1798
+ * Send notification ACK (delivered/clicked) to Swan API
1799
+ * ACKs are queued and sent with other events
1800
+ */
1801
+ async sendNotificationAck(messageId, event) {
1802
+ try {
1803
+ if (!messageId || !event) {
1804
+ _Logger.default.warn('[SwanSDK] sendNotificationAck: missing messageId or event');
1805
+ return;
1806
+ }
1807
+
1808
+ // Backend expects commId field - we send Firebase messageId as the value
1809
+ const ackData = {
1810
+ commId: messageId,
1811
+ event,
1812
+ timestamp: Date.now()
1813
+ };
1814
+
1815
+ // Enqueue as a special SWAN_NOTIFICATION_ACK event
1816
+ await this.trackEvent('SWAN_NOTIFICATION_ACK', ackData);
1817
+ _Logger.default.log('[SwanSDK] Notification ACK queued:', messageId, event);
1818
+ } catch (error) {
1819
+ _Logger.default.error('[SwanSDK] Error queueing notification ACK:', error);
1820
+ }
1821
+ }
1822
+
1823
+ /**
1824
+ * Get notification priority from payload
1825
+ * Maps priority string to AndroidPriority constants
1826
+ * Used for Android 7.1 and below (Android 8+ uses channel importance)
1827
+ */
1828
+ getPriorityFromPayload(remoteMessage) {
1829
+ // Check payload for priority
1830
+ const priorityStr = remoteMessage?.data?.priority || remoteMessage?.data?.notification_priority || remoteMessage?.notification?.android?.priority;
1831
+ if (!priorityStr) return undefined;
1832
+
1833
+ // Map string to notifee AndroidPriority constants
1834
+ // https://notifee.app/react-native/reference/notificationandroidpriority
1835
+ const priorityMap = {
1836
+ max: -2,
1837
+ // AndroidPriority.MAX
1838
+ high: -1,
1839
+ // AndroidPriority.HIGH
1840
+ default: 0,
1841
+ // AndroidPriority.DEFAULT
1842
+ low: 1,
1843
+ // AndroidPriority.LOW
1844
+ min: 2 // AndroidPriority.MIN
1845
+ };
1846
+ return priorityMap[priorityStr.toLowerCase()];
1847
+ }
1848
+
1849
+ /**
1850
+ * Auto-display notification in foreground using notifee
1851
+ * This provides automatic notification handling like CleverTap, OneSignal, etc.
1852
+ */
1853
+ async displayForegroundNotification(notification) {
1854
+ try {
1855
+ const remoteMessage = notification?.notification;
1856
+ if (!remoteMessage) {
1857
+ _Logger.default.warn('[SwanSDK] No notification data to display');
1858
+ return;
1859
+ }
1860
+
1861
+ // Extract notification data (supports both notification and data-only messages)
1862
+ const title = remoteMessage?.notification?.title || remoteMessage?.data?.title || 'Notification';
1863
+ const body = remoteMessage?.notification?.body || remoteMessage?.data?.body || '';
1864
+ const imageUrl = remoteMessage?.notification?.imageUrl || remoteMessage?.notification?.image || remoteMessage?.data?.image || remoteMessage?.data?.fcm_options?.image || '';
1865
+ _Logger.default.log('[SwanSDK] Auto-displaying foreground notification:', {
1866
+ title,
1867
+ body,
1868
+ imageUrl,
1869
+ hasPayloadChannel: !!(remoteMessage?.notification?.android?.channelId || remoteMessage?.data?.channelId || remoteMessage?.data?.channel_id),
1870
+ hasPayloadPriority: !!(remoteMessage?.data?.priority || remoteMessage?.data?.notification_priority || remoteMessage?.notification?.android?.priority)
1871
+ });
1872
+
1873
+ // Dynamically import notifee (optional dependency)
1874
+ let notifee;
1875
+ try {
1876
+ notifee = require('@notifee/react-native').default;
1877
+ } catch (error) {
1878
+ _Logger.default.warn('[SwanSDK] @notifee/react-native not installed. Install it to enable auto-display:', 'npm install @notifee/react-native');
1879
+ // Emit event so app can handle display if notifee not available
1880
+ this.emit('notificationDisplayFailed', {
1881
+ reason: 'notifee_not_installed',
1882
+ notification: remoteMessage
1883
+ });
1884
+ return;
1885
+ }
1886
+
1887
+ // Get notification channel ID with priority: payload → default
1888
+ const channelId =
1889
+ // 1. Check FCM notification payload (Firebase standard)
1890
+ remoteMessage?.notification?.android?.channelId ||
1891
+ // 2. Check data payload (Swan Builder custom field - recommended)
1892
+ remoteMessage?.data?.channelId || remoteMessage?.data?.channel_id ||
1893
+ // 3. Final fallback to default channel
1894
+ SWAN_NOTIFICATION_CHANNELS.DEFAULT;
1895
+
1896
+ // Get priority from payload (for Android 7.1 and below)
1897
+ const priority = this.getPriorityFromPayload(remoteMessage);
1898
+
1899
+ // Build notification config
1900
+ const notificationConfig = {
1901
+ id: remoteMessage?.messageId || String(Date.now()),
1902
+ // Use Firebase messageId as notification ID
1903
+ title,
1904
+ body,
1905
+ data: {
1906
+ // Preserve ALL original FCM data for click handler
1907
+ ...(remoteMessage.data || {}),
1908
+ // Store messageId for click ACK (must be string for Notifee)
1909
+ ...(remoteMessage.messageId && {
1910
+ messageId: String(remoteMessage.messageId)
1911
+ })
1912
+ },
1913
+ android: {
1914
+ channelId,
1915
+ smallIcon: 'ic_launcher',
1916
+ // App should provide this
1917
+ ...(priority !== undefined && {
1918
+ priority
1919
+ }),
1920
+ // Add priority if specified (Android 7.1-)
1921
+ pressAction: {
1922
+ id: 'default'
1923
+ }
1924
+ },
1925
+ ios: {
1926
+ foregroundPresentationOptions: {
1927
+ alert: true,
1928
+ badge: true,
1929
+ sound: true
1930
+ }
1931
+ }
1932
+ };
1933
+
1934
+ // Add image if present
1935
+ if (imageUrl) {
1936
+ // Dynamically import AndroidStyle to avoid import errors
1937
+ const {
1938
+ AndroidStyle
1939
+ } = require('@notifee/react-native');
1940
+ notificationConfig.android.style = {
1941
+ type: AndroidStyle.BIGPICTURE,
1942
+ picture: imageUrl
1943
+ };
1944
+ notificationConfig.ios.attachments = [{
1945
+ url: imageUrl
1946
+ }];
1947
+ }
1948
+
1949
+ // Display notification
1950
+ _Logger.default.log('[SwanSDK] Displaying notification on channel:', channelId, priority !== undefined ? `with priority: ${priority}` : '(using channel importance)');
1951
+ _Logger.default.log('[SwanSDK] Notification config:', JSON.stringify(notificationConfig));
1952
+ const notificationId = await notifee.displayNotification(notificationConfig);
1953
+ _Logger.default.log('[SwanSDK] Notification displayed successfully with ID:', notificationId);
1954
+ } catch (error) {
1955
+ _Logger.default.error('[SwanSDK] Error displaying foreground notification:', error);
1956
+ // Emit event so app can handle display on error
1957
+ this.emit('notificationDisplayFailed', {
1958
+ reason: 'display_error',
1959
+ error: error?.message || 'Unknown error',
1960
+ notification: notification?.notification
1961
+ });
1962
+ }
1963
+ }
1964
+
1965
+ // Method to show popup and manage modal instances
1966
+ showPopUp({
1967
+ designConfig
1968
+ }) {
1969
+ designConfig = this.flattenDesignConfig(this.notificationToShow);
1970
+ this.notificationToShow = null;
1971
+
1972
+ // Generate unique ID for this modal
1973
+ const modalId = SwanSDK.generateId();
1974
+
1975
+ // Create modal component with visibility management
1976
+ const ModalWrapper = () => {
1977
+ const [isVisible, setIsVisible] = (0, _react.useState)(true);
1978
+ const handleClose = () => {
1979
+ // Find the modal in the instances array and mark as invisible
1980
+ const index = SwanSDK.modalInstances.findIndex(m => m.id === modalId);
1981
+ if (index !== -1 && SwanSDK.modalInstances[index]) {
1982
+ SwanSDK.modalInstances[index].visible = false;
1983
+ }
1984
+
1985
+ // Update local state to hide the modal
1986
+ setIsVisible(false);
1987
+ };
1988
+ const handleButtonClick = event => {
1989
+ this.notifyButtonClick(event);
1990
+ // Find the modal in the instances array and mark as invisible
1991
+ const index = SwanSDK.modalInstances.findIndex(m => m.id === modalId);
1992
+ if (index !== -1 && SwanSDK.modalInstances[index]) {
1993
+ SwanSDK.modalInstances[index].visible = false;
1994
+ }
1995
+
1996
+ // Update local state to hide the modal
1997
+ setIsVisible(false);
1998
+ };
1999
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
2000
+ animationType: "fade",
2001
+ transparent: true,
2002
+ visible: isVisible,
2003
+ onRequestClose: handleClose,
2004
+ hardwareAccelerated: true,
2005
+ statusBarTranslucent: true,
2006
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
2007
+ style: SwanSDK.styles.modalBackground,
2008
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
2009
+ style: SwanSDK.styles.popUpModalContent,
2010
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_PopUpView.default, {
2011
+ designConfig: designConfig,
2012
+ onClose: handleClose,
2013
+ onButtonClick: handleButtonClick
2014
+ })
2015
+ })
2016
+ })
2017
+ });
2018
+ };
2019
+
2020
+ // Create modal instance object
2021
+ const modalInstance = {
2022
+ id: modalId,
2023
+ component: /*#__PURE__*/(0, _jsxRuntime.jsx)(ModalWrapper, {}, modalId),
2024
+ visible: true
2025
+ };
2026
+
2027
+ // Add to modal instances array
2028
+ SwanSDK.modalInstances.push(modalInstance);
2029
+
2030
+ // Return the modal component
2031
+ return modalInstance.component;
2032
+ }
2033
+ async processNotification(notificationList) {
2034
+ if (!notificationList || notificationList.length < 1) return null;
2035
+ this.notificationToShow = notificationList.filter(e => e.displayIn.toLowerCase() === 'all' || e.displayIn.toLowerCase() === this.currentScreenName.toLowerCase())[0];
2036
+ if (!this.notificationToShow) {
2037
+ return null;
2038
+ }
2039
+ notificationList.splice(notificationList.indexOf(this.notificationToShow), 1);
2040
+ const {
2041
+ subType,
2042
+ displayIn,
2043
+ expiresAt,
2044
+ displayLimit,
2045
+ displayUnlimited
2046
+ } = this.notificationToShow;
2047
+ if (displayIn && displayIn.toLowerCase() != 'all' && displayIn.toLowerCase() !== this.currentScreenName.toLowerCase()) {
2048
+ return null;
2049
+ }
2050
+ await this.deleteFromTableByValue('Notifications', 'commId', this.notificationToShow.commId);
2051
+ if (!displayUnlimited && displayLimit < 1) {
2052
+ this.processNotification(notificationList);
2053
+ return null;
2054
+ }
2055
+ if (expiresAt && new Date(new Date(expiresAt).setHours(0, 0, 0, 0)) < new Date(new Date().setHours(0, 0, 0, 0))) {
2056
+ this.processNotification(notificationList);
2057
+ return null;
2058
+ }
2059
+ const newDisplayLimit = displayLimit - 1;
2060
+ if (displayUnlimited || newDisplayLimit > 0) {
2061
+ await this.insertNotification({
2062
+ ...this.notificationToShow,
2063
+ displayLimit: newDisplayLimit
2064
+ }, 'Notifications');
2065
+ }
2066
+ try {
2067
+ await _asyncStorage.default.setItem('lastNotificationFetchFromDbDate', new Date().toISOString());
2068
+ } catch (e) {
2069
+ _Logger.default.log('error in setting lastNotificationFetchFromDbDate', e);
2070
+ }
2071
+ this.sendNotificationAck(this.notificationToShow.commId, 'showed');
2072
+ switch (subType) {
2073
+ case 'popup':
2074
+ return this.showPopUp({
2075
+ designConfig: this.notificationToShow
2076
+ });
2077
+ case 'header':
2078
+ return this.showHeader({
2079
+ designConfig: this.notificationToShow
2080
+ });
2081
+ case 'footer':
2082
+ return this.showFooter({
2083
+ designConfig: this.notificationToShow
2084
+ });
2085
+ case 'fullscreen':
2086
+ return this.showFullScreen({
2087
+ designConfig: this.notificationToShow
2088
+ });
2089
+ }
2090
+ return null;
2091
+ }
2092
+ showHeader({
2093
+ designConfig
2094
+ }) {
2095
+ designConfig = this.flattenDesignConfig(this.notificationToShow);
2096
+ this.notificationToShow = null;
2097
+
2098
+ // Generate unique ID for this modal
2099
+ const modalId = SwanSDK.generateId();
2100
+
2101
+ // Create modal component with visibility management
2102
+ const ModalWrapper = () => {
2103
+ const [isVisible, setIsVisible] = (0, _react.useState)(true);
2104
+ const handleClose = () => {
2105
+ // Find the modal in the instances array and mark as invisible
2106
+ const index = SwanSDK.modalInstances.findIndex(m => m.id === modalId);
2107
+ if (index !== -1 && SwanSDK.modalInstances[index]) {
2108
+ SwanSDK.modalInstances[index].visible = false;
2109
+ }
2110
+
2111
+ // Update local state to hide the modal
2112
+ setIsVisible(false);
2113
+ };
2114
+ const handleButtonClick = event => {
2115
+ this.notifyButtonClick(event);
2116
+ // Find the modal in the instances array and mark as invisible
2117
+ const index = SwanSDK.modalInstances.findIndex(m => m.id === modalId);
2118
+ if (index !== -1 && SwanSDK.modalInstances[index]) {
2119
+ SwanSDK.modalInstances[index].visible = false;
2120
+ }
2121
+
2122
+ // Update local state to hide the modal
2123
+ setIsVisible(false);
2124
+ };
2125
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
2126
+ animationType: "fade",
2127
+ transparent: true,
2128
+ visible: isVisible,
2129
+ onRequestClose: handleClose,
2130
+ hardwareAccelerated: true,
2131
+ statusBarTranslucent: true,
2132
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
2133
+ style: SwanSDK.styles.modalBackground,
2134
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
2135
+ style: SwanSDK.styles.headerModalContent,
2136
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_HeaderView.default, {
2137
+ designConfig: designConfig,
2138
+ onClose: handleClose,
2139
+ onButtonClick: handleButtonClick
2140
+ })
2141
+ })
2142
+ })
2143
+ });
2144
+ };
2145
+
2146
+ // Create modal instance object
2147
+ const modalInstance = {
2148
+ id: modalId,
2149
+ component: /*#__PURE__*/(0, _jsxRuntime.jsx)(ModalWrapper, {}, modalId),
2150
+ visible: true
2151
+ };
2152
+
2153
+ // Add to modal instances array
2154
+ SwanSDK.modalInstances.push(modalInstance);
2155
+
2156
+ // Return the modal component
2157
+ return modalInstance.component;
2158
+ }
2159
+ showFooter({
2160
+ designConfig
2161
+ }) {
2162
+ designConfig = this.flattenDesignConfig(this.notificationToShow);
2163
+ this.notificationToShow = null;
2164
+
2165
+ // Generate unique ID for this modal
2166
+ const modalId = SwanSDK.generateId();
2167
+
2168
+ // Create modal component with visibility management
2169
+ const ModalWrapper = () => {
2170
+ const [isVisible, setIsVisible] = (0, _react.useState)(true);
2171
+ const handleClose = () => {
2172
+ // Find the modal in the instances array and mark as invisible
2173
+ const index = SwanSDK.modalInstances.findIndex(m => m.id === modalId);
2174
+ if (index !== -1 && SwanSDK.modalInstances[index]) {
2175
+ SwanSDK.modalInstances[index].visible = false;
2176
+ }
2177
+
2178
+ // Update local state to hide the modal
2179
+ setIsVisible(false);
2180
+ };
2181
+ const handleButtonClick = event => {
2182
+ this.notifyButtonClick(event);
2183
+ // Find the modal in the instances array and mark as invisible
2184
+ const index = SwanSDK.modalInstances.findIndex(m => m.id === modalId);
2185
+ if (index !== -1 && SwanSDK.modalInstances[index]) {
2186
+ SwanSDK.modalInstances[index].visible = false;
2187
+ }
2188
+
2189
+ // Update local state to hide the modal
2190
+ setIsVisible(false);
2191
+ };
2192
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
2193
+ animationType: "fade",
2194
+ transparent: true,
2195
+ visible: isVisible,
2196
+ onRequestClose: handleClose,
2197
+ hardwareAccelerated: true,
2198
+ statusBarTranslucent: true,
2199
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
2200
+ style: SwanSDK.styles.modalBackground,
2201
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
2202
+ style: SwanSDK.styles.footerModalContent,
2203
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_FooterView.default, {
2204
+ designConfig: designConfig,
2205
+ onClose: handleClose,
2206
+ onButtonClick: handleButtonClick
2207
+ })
2208
+ })
2209
+ })
2210
+ });
2211
+ };
2212
+
2213
+ // Create modal instance object
2214
+ const modalInstance = {
2215
+ id: modalId,
2216
+ component: /*#__PURE__*/(0, _jsxRuntime.jsx)(ModalWrapper, {}, modalId),
2217
+ visible: true
2218
+ };
2219
+
2220
+ // Add to modal instances array
2221
+ SwanSDK.modalInstances.push(modalInstance);
2222
+
2223
+ // Return the modal component
2224
+ return modalInstance.component;
2225
+ }
2226
+ showFullScreen({
2227
+ designConfig
2228
+ }) {
2229
+ designConfig = this.flattenDesignConfig(this.notificationToShow);
2230
+ this.notificationToShow = null;
2231
+
2232
+ // Generate unique ID for this modal
2233
+ const modalId = SwanSDK.generateId();
2234
+
2235
+ // Create modal component with visibility management
2236
+ const ModalWrapper = () => {
2237
+ const [isVisible, setIsVisible] = (0, _react.useState)(true);
2238
+ const handleClose = () => {
2239
+ // Find the modal in the instances array and mark as invisible
2240
+ const index = SwanSDK.modalInstances.findIndex(m => m.id === modalId);
2241
+ if (index !== -1 && SwanSDK.modalInstances[index]) {
2242
+ SwanSDK.modalInstances[index].visible = false;
2243
+ }
2244
+
2245
+ // Update local state to hide the modal
2246
+ setIsVisible(false);
2247
+ };
2248
+ const handleButtonClick = event => {
2249
+ this.notifyButtonClick(event);
2250
+ // Find the modal in the instances array and mark as invisible
2251
+ const index = SwanSDK.modalInstances.findIndex(m => m.id === modalId);
2252
+ if (index !== -1 && SwanSDK.modalInstances[index]) {
2253
+ SwanSDK.modalInstances[index].visible = false;
2254
+ }
2255
+
2256
+ // Update local state to hide the modal
2257
+ setIsVisible(false);
2258
+ };
2259
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
2260
+ animationType: "fade",
2261
+ transparent: true,
2262
+ visible: isVisible,
2263
+ onRequestClose: handleClose,
2264
+ hardwareAccelerated: true,
2265
+ statusBarTranslucent: true,
2266
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
2267
+ style: SwanSDK.styles.modalBackground,
2268
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
2269
+ style: SwanSDK.styles.footerModalContent,
2270
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_FullScreenView.default, {
2271
+ designConfig: designConfig,
2272
+ onClose: handleClose,
2273
+ onButtonClick: handleButtonClick
2274
+ })
2275
+ })
2276
+ })
2277
+ });
2278
+ };
2279
+
2280
+ // Create modal instance object
2281
+ const modalInstance = {
2282
+ id: modalId,
2283
+ component: /*#__PURE__*/(0, _jsxRuntime.jsx)(ModalWrapper, {}, modalId),
2284
+ visible: true
2285
+ };
2286
+
2287
+ // Add to modal instances array
2288
+ SwanSDK.modalInstances.push(modalInstance);
2289
+
2290
+ // Return the modal component
2291
+ return modalInstance.component;
2292
+ }
2293
+ async fetchNotificationFromAPI() {
2294
+ try {
2295
+ const CDID = await this.getSwanIdentifier();
2296
+ _Logger.default.log('URL', `${_ApiUrls.default.NOTIFICATION_GET_URL[this.isProduction]}?appId=${this.appId}&CDID=${CDID}`);
2297
+ const response = await fetch(`${_ApiUrls.default.NOTIFICATION_GET_URL[this.isProduction]}?appId=${this.appId}&CDID=${CDID}`, {
2298
+ method: 'GET',
2299
+ headers: {
2300
+ 'Content-Type': 'application/json',
2301
+ 'X-Swan-Device-Id': this.deviceId
2302
+ }
2303
+ });
2304
+ let responseData = null;
2305
+ try {
2306
+ responseData = await response.json();
2307
+ _Logger.default.log('IN APP NOTIFICATION', responseData);
2308
+ await _asyncStorage.default.setItem('lastNotificationFetchFromApiDate', new Date().toISOString());
2309
+ } catch (err) {}
2310
+ if (responseData && responseData.notifications && responseData.notifications.length > 0) {
2311
+ responseData.notifications.forEach(async notification => {
2312
+ await this.insertNotification(notification, 'Notifications');
2313
+ this.sendNotificationAck(notification.commId, 'delivered');
2314
+ });
2315
+ }
2316
+ return responseData;
2317
+ } catch (error) {
2318
+ _Logger.default.error('Error fetching data from Swan:', error);
2319
+ }
2320
+ }
2321
+ async fetchNotificationFromDB() {
2322
+ try {
2323
+ const rows = await this.selectAllFromTable('Notifications');
2324
+ if (!rows || rows.length < 1) return [];
2325
+ const notificationList = [];
2326
+ for (let i = 0; i < rows.length; i++) {
2327
+ const content = JSON.parse(rows[i].content);
2328
+ notificationList.push({
2329
+ ...content.design,
2330
+ ...content,
2331
+ commId: rows[i].commId,
2332
+ design: null
2333
+ });
2334
+ }
2335
+ return notificationList;
2336
+ } catch (error) {
2337
+ _Logger.default.error('Error fetching data from DB:', error);
2338
+ return [];
2339
+ }
2340
+ }
2341
+ flattenDesignConfig(designConfig) {
2342
+ const {
2343
+ colors = {},
2344
+ buttons = [],
2345
+ ...restConfig
2346
+ } = designConfig;
2347
+ const primaryButton = buttons.find(button => button.type === 'primary') || {};
2348
+ const secondaryButton = buttons.find(button => button.type === 'secondary') || {};
2349
+ return {
2350
+ themeBackgroundColor: colors.themeBackground || '#f5f5f5',
2351
+ crossButtonColor: colors.crossButton || '#000',
2352
+ imageURL: restConfig.imageUrl || undefined,
2353
+ iconURL: restConfig.iconURL || undefined,
2354
+ title: restConfig.title || 'Title',
2355
+ titleColor: colors.title || '#333',
2356
+ onClickAction: restConfig.onClickAction || '',
2357
+ onClickActionType: restConfig.onClickActionType || '',
2358
+ description: restConfig.description || 'Description',
2359
+ descriptionColor: colors.description || '#666',
2360
+ primaryButtonSwitch: primaryButton.primaryButtonSwitch !== undefined ? primaryButton.primaryButtonSwitch : false,
2361
+ primaryButtonColor: colors.primaryButtonBackground || '#007BFF',
2362
+ primaryButtonFontColor: colors.primaryButtonFont || '#FFF',
2363
+ primaryButtonText: primaryButton.label || 'Primary Action',
2364
+ primaryButtonAction: primaryButton.action || '',
2365
+ primaryButtonActionType: primaryButton.actionType || '',
2366
+ secondaryButtonSwitch: secondaryButton.secondaryButtonSwitch !== undefined ? secondaryButton.secondaryButtonSwitch : false,
2367
+ secondaryButtonColor: colors.secondaryButtonBackground || '#6C757D',
2368
+ secondaryButtonFontColor: colors.secondaryButtonFont || '#FFF',
2369
+ secondaryButtonText: secondaryButton.label || 'Secondary Action',
2370
+ secondaryButtonAction: secondaryButton.action || '',
2371
+ secondaryButtonActionType: secondaryButton.actionType || '',
2372
+ backgroundImageURL: restConfig.backgroundImageUrl || undefined,
2373
+ commId: restConfig.commId || undefined
2374
+ };
2375
+ }
2376
+ async getNotificationComponent() {
2377
+ const lastNotificationFetchFromApi = await _asyncStorage.default.getItem('lastNotificationFetchFromApiDate');
2378
+ if (!lastNotificationFetchFromApi || (await this.isGreaterThanTimePeriod(lastNotificationFetchFromApi, 5))) {
2379
+ this.fetchNotificationFromAPI();
2380
+ }
2381
+ const lastNotificationFetchFromDB = await _asyncStorage.default.getItem('lastNotificationFetchFromDbDate');
2382
+ let notificationList = [];
2383
+ if (!lastNotificationFetchFromDB || (await this.isGreaterThanTimePeriod(lastNotificationFetchFromDB, 30))) {
2384
+ notificationList = await this.fetchNotificationFromDB();
2385
+ }
2386
+ if (notificationList && notificationList.length > 0) {
2387
+ const notificationComponent = await this.processNotification(notificationList);
2388
+ this.notifyNotificationShow(notificationComponent);
2389
+ return notificationComponent;
2390
+ }
2391
+ return null;
2392
+ }
2393
+ async isGreaterThanTimePeriod(lastFetchTimeString, timePeriod) {
2394
+ const lastFetchTime = new Date(lastFetchTimeString);
2395
+ const currentTime = new Date();
2396
+ const timeDifference = currentTime - lastFetchTime;
2397
+ if (timeDifference > timePeriod * 60 * 1000) {
2398
+ _Logger.default.log('More than 30 mins');
2399
+ return true;
2400
+ } else {
2401
+ _Logger.default.log('More than 30 mins');
2402
+ //need to set false
2403
+ return false;
2404
+ }
2405
+ }
2406
+
2407
+ // ==========================================
2408
+ // Firebase Notification Manager Integration
2409
+ // ==========================================
2410
+
2411
+ // Expose Event Constants (inline to avoid module-level import)
2412
+ static EVENTS = {
2413
+ PUSH_NOTIFICATION_RECEIVED: 'swnPushNotificationReceived',
2414
+ NOTIFICATION_OPENED: 'swanTapPushNotifiactioncallback',
2415
+ TOKEN_RECEIVED: 'swnTokenReceived',
2416
+ TOKEN_REFRESH: 'swnTokenRefresh',
2417
+ PERMISSION_CHANGED: 'swnPermissionChanged'
2418
+ };
2419
+
2420
+ /**
2421
+ * Intialize Firebase Notification Manager
2422
+ * This sets up FCM token generation and notification handling
2423
+ */
2424
+ async initializeFirebase() {
2425
+ try {
2426
+ const {
2427
+ default: FirebaseNotificationManager
2428
+ } = require('./utils/FirebaseNotificationManager');
2429
+ this.firebaseManager = FirebaseNotificationManager.getInstance();
2430
+
2431
+ // IMPORTANT: Setup event listeners BEFORE initialize() so we don't miss TOKEN_RECEIVED event
2432
+ this.setupFirebaseEventListeners();
2433
+
2434
+ // Setup notifee event listeners for foreground notification clicks
2435
+ this.setupNotifeeEventListeners();
2436
+
2437
+ // firebaseManager.initialize() already handles permission request and token retrieval
2438
+ // No need to call requestNotificationPermission() again here
2439
+ const initialized = await this.firebaseManager.initialize();
2440
+ _Logger.default.log('SwanSDK: Firebase initialized:', initialized);
2441
+ } catch (error) {
2442
+ _Logger.default.error('Error initializing Firebase:', error);
2443
+ }
2444
+ }
2445
+
2446
+ /**
2447
+ * Setup Notifee event listeners for notification clicks
2448
+ * Handles both foreground clicks and initial notification (app opened from quit state)
2449
+ */
2450
+ async setupNotifeeEventListeners() {
2451
+ try {
2452
+ const notifee = require('@notifee/react-native').default;
2453
+
2454
+ // NOTE: Foreground event listener (onForegroundEvent) is registered at module level
2455
+ // in index.js using createNotifeeForegroundHandler(). This is required by Notifee,
2456
+ // similar to how Firebase requires onMessage() at module level.
2457
+
2458
+ // Check if app was opened by tapping a Notifee-displayed notification (data-only architecture)
2459
+ const initialNotification = await notifee.getInitialNotification();
2460
+ if (initialNotification && !this.initialNotificationHandled) {
2461
+ const messageId = initialNotification?.notification?.id;
2462
+ const notificationData = initialNotification.data || {};
2463
+ if (messageId) {
2464
+ _Logger.default.log('[SwanSDK] App opened from Notifee notification tap:', messageId);
2465
+ this.initialNotificationHandled = true;
2466
+ await this.sendNotificationAck(messageId, 'clicked');
2467
+ }
2468
+ // Extract deep link information
2469
+ const deepLinkPayload = {
2470
+ route: notificationData.route,
2471
+ data: notificationData,
2472
+ title: initialNotification.notification?.title || notificationData.title,
2473
+ body: initialNotification.notification?.body || notificationData.body
2474
+ };
2475
+
2476
+ // Emit notificationOpened event for host app to handle deep linking
2477
+ this.emitNotificationOpened(deepLinkPayload);
2478
+ }
2479
+
2480
+ // Also check Firebase's getInitialNotification for iOS auto-displayed notifications
2481
+ // (notification+data payload with NES architecture)
2482
+ if (!this.initialNotificationHandled) {
2483
+ await this.checkFirebaseInitialNotification();
2484
+ }
2485
+ _Logger.default.log('[SwanSDK] ✓ Notifee event listeners setup complete');
2486
+ } catch (error) {
2487
+ _Logger.default.log('[SwanSDK] Notifee not available, skipping notification setup');
2488
+ }
2489
+ }
2490
+
2491
+ /**
2492
+ * Check if app was opened from killed state via Firebase notification tap
2493
+ * This handles notifications auto-displayed by iOS (notification+data payload with NES)
2494
+ * For data-only notifications displayed via Notifee, use notifee.getInitialNotification() instead
2495
+ */
2496
+ async checkFirebaseInitialNotification() {
2497
+ try {
2498
+ const messaging = require('@react-native-firebase/messaging').default;
2499
+ const initialNotification = await messaging().getInitialNotification();
2500
+ if (initialNotification && !this.initialNotificationHandled) {
2501
+ _Logger.default.log('[SwanSDK] App opened from Firebase notification (killed state):', initialNotification.messageId);
2502
+
2503
+ // Mark as handled to prevent duplicate ACKs
2504
+ this.initialNotificationHandled = true;
2505
+
2506
+ // Get messageId and notification data
2507
+ const messageId = initialNotification.messageId;
2508
+ const notificationData = initialNotification.data || {};
2509
+
2510
+ // Extract deep link information
2511
+ const deepLinkPayload = {
2512
+ route: notificationData.route,
2513
+ data: notificationData,
2514
+ title: initialNotification.notification?.title || notificationData.title,
2515
+ body: initialNotification.notification?.body || notificationData.body
2516
+ };
2517
+
2518
+ // Emit notificationOpened event for host app to handle deep linking
2519
+ this.emitNotificationOpened(deepLinkPayload);
2520
+
2521
+ // Send click ACK
2522
+ if (messageId) {
2523
+ await this.sendNotificationAck(messageId, 'clicked');
2524
+ _Logger.default.log('[SwanSDK] ✅ Firebase initial notification click ACK sent');
2525
+ }
2526
+ }
2527
+ } catch (error) {
2528
+ _Logger.default.log('[SwanSDK] Firebase messaging not available for getInitialNotification:', error);
2529
+ }
2530
+ }
2531
+
2532
+ /**
2533
+ * Setup Firebase event listeners
2534
+ */
2535
+ setupFirebaseEventListeners() {
2536
+ if (!this.firebaseManager) return;
2537
+ this.firebaseManager.addEventListener(SwanSDK.EVENTS.TOKEN_RECEIVED, async token => {
2538
+ _Logger.default.log('FCM Token received:', token);
2539
+ await this.syncPushSubscription(token);
2540
+ });
2541
+ this.firebaseManager.addEventListener(SwanSDK.EVENTS.TOKEN_REFRESH, async token => {
2542
+ _Logger.default.log('FCM Token refreshed:', token);
2543
+ await this.syncPushSubscription(token);
2544
+ });
2545
+
2546
+ // NOTE: With data-only push messages, all notifications are displayed via Notifee.
2547
+ // Click tracking is handled by Notifee event handlers (createNotifeeForegroundHandler/createNotifeeBackgroundHandler).
2548
+ // Firebase's NOTIFICATION_OPENED event is not needed since Firebase doesn't auto-display data-only messages.
2549
+ }
2550
+
2551
+ /**
2552
+ * Get the Firebase Manager instance
2553
+ */
2554
+ getFirebaseManager() {
2555
+ return this.firebaseManager;
2556
+ }
2557
+
2558
+ /**
2559
+ * Check if push notifications are enabled in SDK config
2560
+ * Used internally by handler functions to verify push should be processed
2561
+ */
2562
+ isPushEnabled() {
2563
+ return this.config.pushNotifications?.enabled === true;
2564
+ }
2565
+
2566
+ /**
2567
+ * Check if SDK is fully initialized and ready to send events
2568
+ * Used by background handlers to determine if SDK instance can send ACKs
2569
+ */
2570
+ isReady() {
2571
+ return !!this.deviceId;
2572
+ }
2573
+
2574
+ /**
2575
+ * Get current push token
2576
+ */
2577
+ async getPushToken() {
2578
+ // Use new PushTokenService if available
2579
+ if (this.pushService) {
2580
+ const token = await this.pushService.getToken();
2581
+ _Logger.default.log('[SwanSDK] getPushToken:', token);
2582
+ return token;
2583
+ }
2584
+
2585
+ // Fallback to firebaseManager for backward compatibility
2586
+ if (this.firebaseManager) {
2587
+ const token = await this.firebaseManager.getToken();
2588
+ _Logger.default.log('[SwanSDK] getPushToken (fallback):', token);
2589
+ return token;
2590
+ }
2591
+ _Logger.default.warn('[SwanSDK] Push notifications not initialized');
2592
+ return null;
2593
+ }
2594
+
2595
+ /**
2596
+ * Delete Firebase token and sync unsubscription to backend
2597
+ */
2598
+ async unsubscribePush() {
2599
+ if (!this.pushService) {
2600
+ _Logger.default.warn('[SwanSDK] Push notifications not initialized');
2601
+ return false;
2602
+ }
2603
+ try {
2604
+ _Logger.default.log('[SwanSDK] Unsubscribing from push notifications...');
2605
+
2606
+ // Delete token via push service
2607
+ const deleted = await this.pushService.deleteToken();
2608
+ if (deleted) {
2609
+ // Sync unsubscription to backend
2610
+ await this.syncPushUnsubscription();
2611
+
2612
+ // Update push state machine
2613
+ this.pushStateMachine.transition(_PushStateMachine.PushState.DISABLED);
2614
+ _Logger.default.log('[SwanSDK] Push token deleted and unsubscription synced');
2615
+ }
2616
+ return deleted;
2617
+ } catch (error) {
2618
+ _Logger.default.error('[SwanSDK] Failed to unsubscribe from push:', error);
2619
+ return false;
2620
+ }
2621
+ }
2622
+
2623
+ /**
2624
+ * Check if notification permission is granted
2625
+ */
2626
+ async hasNotificationPermission() {
2627
+ // Use new PushTokenService if available
2628
+ if (this.pushService) {
2629
+ const hasPermission = await this.pushService.hasPermission();
2630
+ _Logger.default.log('[SwanSDK] hasNotificationPermission:', hasPermission);
2631
+ return hasPermission;
2632
+ }
2633
+
2634
+ // Fallback to firebaseManager for backward compatibility
2635
+ if (this.firebaseManager) {
2636
+ const hasPermission = await this.firebaseManager.hasPermission();
2637
+ _Logger.default.log('[SwanSDK] hasNotificationPermission (fallback):', hasPermission);
2638
+ return hasPermission;
2639
+ }
2640
+ _Logger.default.warn('[SwanSDK] Push notifications not initialized');
2641
+ return false;
2642
+ }
2643
+
2644
+ /**
2645
+ * Request notification permission
2646
+ */
2647
+ /**
2648
+ * Request notification permission
2649
+ * Uses the new PushTokenService if available, falls back to firebaseManager for backward compatibility
2650
+ */
2651
+ async requestNotificationPermission() {
2652
+ _Logger.default.log('[SwanSDK] requestNotificationPermission called');
2653
+
2654
+ // Android 13+ requires POST_NOTIFICATIONS permission
2655
+ if (_reactNative.Platform.OS === 'android' && _reactNative.Platform.Version >= 33) {
2656
+ const permission = _reactNative.PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS;
2657
+ if (permission) {
2658
+ const granted = await _reactNative.PermissionsAndroid.request(permission);
2659
+ _Logger.default.log('[SwanSDK] Android 13+ Notification Permission:', granted);
2660
+ if (granted !== _reactNative.PermissionsAndroid.RESULTS.GRANTED) {
2661
+ _Logger.default.error('[SwanSDK] Notification permission denied');
2662
+ return false;
2663
+ }
2664
+ }
2665
+ }
2666
+
2667
+ // Use new PushTokenService if available (refactored architecture)
2668
+ if (this.pushService) {
2669
+ _Logger.default.log('[SwanSDK] Using PushTokenService to request permission');
2670
+ const granted = await this.pushService.requestPermission();
2671
+ _Logger.default.log('[SwanSDK] Permission result:', granted);
2672
+ return granted;
2673
+ }
2674
+
2675
+ // Fallback to firebaseManager for backward compatibility
2676
+ if (this.firebaseManager) {
2677
+ _Logger.default.log('[SwanSDK] Falling back to firebaseManager');
2678
+ const granted = await this.firebaseManager.requestPermission();
2679
+ _Logger.default.log('[SwanSDK] Permission result:', granted);
2680
+ return granted;
2681
+ }
2682
+ _Logger.default.warn('[SwanSDK] Push notifications not initialized');
2683
+ return false;
2684
+ }
2685
+
2686
+ /**
2687
+ * Create a Notification Channel (Android only)
2688
+ * @param channelName - User visible name
2689
+ * @param importance - Importance level (default: 4 - High)
2690
+ * @param description - User visible description
2691
+ * @param channelId - Custom Channel ID (defaults to App ID)
2692
+ */
2693
+ async createNotificationChannel(channelName = 'General Notifications', importance = 4, description, channelId) {
2694
+ // Use provided channelId or fallback to appId
2695
+ const id = channelId || this.appId;
2696
+
2697
+ // Use new PushTokenService if available
2698
+ if (this.pushService) {
2699
+ return await this.pushService.createNotificationChannel(id, channelName, importance, description);
2700
+ }
2701
+
2702
+ // Fallback to firebaseManager for backward compatibility
2703
+ if (this.firebaseManager) {
2704
+ return await this.firebaseManager.createNotificationChannel(id, channelName, importance, description);
2705
+ }
2706
+ _Logger.default.warn('[SwanSDK] Push notifications not initialized');
2707
+ return null;
2708
+ }
2709
+
2710
+ /**
2711
+ * Delete a Notification Channel (Android only)
2712
+ */
2713
+ async deleteNotificationChannel(channelId) {
2714
+ // Use new PushTokenService if available
2715
+ if (this.pushService) {
2716
+ return await this.pushService.deleteNotificationChannel(channelId);
2717
+ }
2718
+
2719
+ // Fallback to firebaseManager for backward compatibility
2720
+ if (this.firebaseManager) {
2721
+ return await this.firebaseManager.deleteNotificationChannel(channelId);
2722
+ }
2723
+ _Logger.default.warn('[SwanSDK] Push notifications not initialized');
2724
+ return false;
2725
+ }
2726
+
2727
+ /**
2728
+ * Get the Notification Channel ID
2729
+ * Returns the App ID as the channel ID
2730
+ */
2731
+ getNotificationChannelId() {
2732
+ return this.appId;
2733
+ }
2734
+
2735
+ /**
2736
+ * Get app badge count
2737
+ */
2738
+ async getBadgeCount() {
2739
+ // Use new PushTokenService if available
2740
+ if (this.pushService) {
2741
+ const count = await this.pushService.getBadgeCount();
2742
+ _Logger.default.log('[SwanSDK] getBadgeCount:', count);
2743
+ return count;
2744
+ }
2745
+
2746
+ // Fallback to firebaseManager for backward compatibility
2747
+ if (this.firebaseManager) {
2748
+ const count = await this.firebaseManager.getBadgeCount();
2749
+ _Logger.default.log('[SwanSDK] getBadgeCount (fallback):', count);
2750
+ return count;
2751
+ }
2752
+ _Logger.default.warn('[SwanSDK] Push notifications not initialized');
2753
+ return 0;
2754
+ }
2755
+
2756
+ /**
2757
+ * Set app badge count
2758
+ */
2759
+ async setBadgeCount(count) {
2760
+ _Logger.default.log('[SwanSDK] setBadgeCount:', count);
2761
+
2762
+ // Use new PushTokenService if available
2763
+ if (this.pushService) {
2764
+ return await this.pushService.setBadgeCount(count);
2765
+ }
2766
+
2767
+ // Fallback to firebaseManager for backward compatibility
2768
+ if (this.firebaseManager) {
2769
+ return await this.firebaseManager.setBadgeCount(count);
2770
+ }
2771
+ _Logger.default.warn('[SwanSDK] Push notifications not initialized');
2772
+ return false;
2773
+ }
2774
+
2775
+ /**
2776
+ * Add listener for push notification events
2777
+ * Listeners registered before push initialization are queued and attached when ready
2778
+ */
2779
+ addEventListener(eventName, callback) {
2780
+ // Use new PushTokenService if available
2781
+ if (this.pushService) {
2782
+ this.pushService.addEventListener(eventName, callback);
2783
+ return;
2784
+ }
2785
+
2786
+ // Fallback to firebaseManager for backward compatibility
2787
+ if (this.firebaseManager) {
2788
+ this.firebaseManager.addEventListener(eventName, callback);
2789
+ return;
2790
+ }
2791
+
2792
+ // Queue the listener to be attached when push notifications are ready
2793
+ _Logger.default.log('[SwanSDK] Queueing event listener for later (push not initialized yet):', eventName);
2794
+ this.pendingPushEventListeners.push({
2795
+ eventName,
2796
+ callback
2797
+ });
2798
+ }
2799
+
2800
+ /**
2801
+ * Attach pending event listeners after push service initialization
2802
+ * Called automatically when push notifications are ready
2803
+ */
2804
+ attachPendingEventListeners() {
2805
+ if (this.pendingPushEventListeners.length === 0) {
2806
+ return;
2807
+ }
2808
+ _Logger.default.log(`[SwanSDK] Attaching ${this.pendingPushEventListeners.length} pending event listeners...`);
2809
+ for (const {
2810
+ eventName,
2811
+ callback
2812
+ } of this.pendingPushEventListeners) {
2813
+ if (this.pushService) {
2814
+ this.pushService.addEventListener(eventName, callback);
2815
+ } else if (this.firebaseManager) {
2816
+ this.firebaseManager.addEventListener(eventName, callback);
2817
+ }
2818
+ }
2819
+
2820
+ // Clear the queue
2821
+ this.pendingPushEventListeners = [];
2822
+ _Logger.default.log('[SwanSDK] All pending event listeners attached');
2823
+ }
2824
+
2825
+ /**
2826
+ * Remove listener for push notification events
2827
+ */
2828
+ removeEventListener(eventName, callback) {
2829
+ // Use new PushTokenService if available
2830
+ if (this.pushService) {
2831
+ this.pushService.removeEventListener(eventName, callback);
2832
+ return;
2833
+ }
2834
+
2835
+ // Fallback to firebaseManager for backward compatibility
2836
+ if (this.firebaseManager) {
2837
+ this.firebaseManager.removeEventListener(eventName, callback);
2838
+ return;
2839
+ }
2840
+ _Logger.default.warn('[SwanSDK] Push notifications not initialized');
2841
+ }
2842
+
2843
+ // ==========================================
2844
+ // Logger Integration
2845
+ // ==========================================
2846
+
2847
+ /**
2848
+ * Log a message using SwanSDK Logger
2849
+ * @param message - Message to log
2850
+ * @param args - Additional arguments
2851
+ */
2852
+ log(message, ...args) {
2853
+ _Logger.default.log(message, ...args);
2854
+ }
2855
+
2856
+ /**
2857
+ * Log a warning using SwanSDK Logger
2858
+ * @param message - Warning message
2859
+ * @param args - Additional arguments
2860
+ */
2861
+ warn(message, ...args) {
2862
+ _Logger.default.warn(message, ...args);
2863
+ }
2864
+
2865
+ /**
2866
+ * Log an error using SwanSDK Logger
2867
+ * @param message - Error message
2868
+ * @param args - Additional arguments
2869
+ */
2870
+ error(message, ...args) {
2871
+ _Logger.default.error(message, ...args);
2872
+ }
2873
+
2874
+ /**
2875
+ * Enable or disable SDK logs
2876
+ * @param enabled - boolean to enable/disable logs
2877
+ */
2878
+ enableLogs(enabled) {
2879
+ _Logger.default.enableLogs(enabled);
2880
+ }
2881
+
2882
+ // ==========================================
2883
+ // Device Info Helper
2884
+ // ==========================================
2885
+
2886
+ /**
2887
+ * Get device information from AsyncStorage
2888
+ * @returns Device info including deviceId, generatedCDID, and currentCDID (credentials)
2889
+ */
2890
+ async getDeviceInfo() {
2891
+ try {
2892
+ return await this.getStoredCredentials();
2893
+ } catch (error) {
2894
+ _Logger.default.error('Error fetching device info:', error);
2895
+ return null;
2896
+ }
2897
+ }
2898
+
2899
+ // ==========================================
2900
+ // Event Queue Management
2901
+ // ==========================================
2902
+
2903
+ /**
2904
+ * Manually flush pending events
2905
+ * Forces immediate send of queued events regardless of batch size or timer
2906
+ * Useful before critical navigation or app closure
2907
+ * @returns Promise that resolves when flush is complete
2908
+ */
2909
+ async flushEvents() {
2910
+ try {
2911
+ _Logger.default.log('Manual flush triggered');
2912
+ await this.flushManager?.flush(true);
2913
+ } catch (error) {
2914
+ _Logger.default.error('Error flushing events:', error);
2915
+ throw error;
2916
+ }
2917
+ }
2918
+
2919
+ /**
2920
+ * Get current queue size
2921
+ * Returns number of pending events waiting to be sent
2922
+ * @returns Number of pending events in queue
2923
+ */
2924
+ async getQueueSize() {
2925
+ try {
2926
+ return (await this.eventQueueManager?.getQueueSize()) || 0;
2927
+ } catch (error) {
2928
+ _Logger.default.error('Error getting queue size:', error);
2929
+ return 0;
2930
+ }
2931
+ }
2932
+
2933
+ // ==========================================
2934
+ // Location Management
2935
+ // ==========================================
2936
+
2937
+ /**
2938
+ * Update device location
2939
+ * Call this method when location permission is granted to update device location
2940
+ * This fetches current location and updates device details on the backend
2941
+ * @returns Promise that resolves when location is updated
2942
+ */
2943
+ async updateLocation() {
2944
+ try {
2945
+ await this.deviceService.updateLocation();
2946
+ } catch (error) {
2947
+ _Logger.default.error('[SwanSDK] Error updating location:', error);
2948
+ throw error;
2949
+ }
2950
+ }
2951
+
2952
+ // ==========================================
2953
+ // Push Notification Handler (Internal)
2954
+ // ==========================================
2955
+
2956
+ /**
2957
+ * Handle background notification delivery ACK
2958
+ * Uses Firebase messageId for tracking
2959
+ * @internal
2960
+ */
2961
+ async handleBackgroundNotification(remoteMessage) {
2962
+ const messageId = remoteMessage?.messageId;
2963
+ _Logger.default.log('[SwanSDK] Processing background notification, messageId:', messageId);
2964
+
2965
+ // Send delivery ACK using Firebase messageId
2966
+ if (messageId) {
2967
+ await this.sendNotificationAck(messageId, 'delivered');
2968
+ _Logger.default.log('[SwanSDK] Background notification ACK sent');
2969
+ } else {
2970
+ _Logger.default.warn('[SwanSDK] No messageId in notification, skipping delivery ACK');
2971
+ }
2972
+ }
2973
+
2974
+ /**
2975
+ * Check if a push notification is silent (should not display UI)
2976
+ * @param remoteMessage FCM remote message
2977
+ * @returns true if silent push
2978
+ */
2979
+ isSilentPush(remoteMessage) {
2980
+ const silent = remoteMessage?.data?.silent;
2981
+ return silent === 'true' || silent === true;
2982
+ }
2983
+
2984
+ /**
2985
+ * Handle foreground notification (called from createForegroundMessageHandler)
2986
+ * Expects data-only FCM payload
2987
+ * Supports silent push notifications (no UI display)
2988
+ * @internal
2989
+ */
2990
+ async handleForegroundNotification(remoteMessage) {
2991
+ const messageId = remoteMessage?.messageId;
2992
+ const isSilent = this.isSilentPush(remoteMessage);
2993
+ _Logger.default.log('[SwanSDK] Processing foreground notification, messageId:', messageId, 'silent:', isSilent);
2994
+
2995
+ // Handle silent push (no UI display)
2996
+ if (isSilent) {
2997
+ _Logger.default.log('[SwanSDK] Silent push received, skipping notification display');
2998
+ return;
2999
+ }
3000
+
3001
+ // Check if iOS Notification Service Extension is active
3002
+ // NES handles delivery ACK, but iOS does NOT auto-display notifications in foreground
3003
+ // So we must still display via Notifee (which handles image download), but skip the delivery ACK
3004
+ const isNESActive = await _SharedCredentialsManager.SharedCredentialsManager.isNotificationServiceExtensionActive();
3005
+
3006
+ // Send delivery ACK using Firebase messageId
3007
+ if (messageId && !isNESActive) {
3008
+ await this.sendNotificationAck(messageId, 'delivered');
3009
+ } else {
3010
+ _Logger.default.warn('[SwanSDK] No messageId / NES active in notification, skipping delivery ACK');
3011
+ }
3012
+
3013
+ // Display notification via Notifee (non-silent push)
3014
+ await this.displayForegroundNotification({
3015
+ notification: remoteMessage,
3016
+ state: 'foreground'
3017
+ });
3018
+ }
3019
+ }
3020
+
3021
+ /**
3022
+ * Foreground Message Handler for index.js
3023
+ *
3024
+ * Handles data-only FCM messages when app is in foreground.
3025
+ * React Native Firebase requires onMessage() to be registered at module level in index.js.
3026
+ *
3027
+ * NOTE: This SDK expects DATA-ONLY FCM payloads (no "notification" field).
3028
+ * Backend should set sdkCapabilities.dataOnlyPush check before sending.
3029
+ *
3030
+ * @example
3031
+ * ```javascript
3032
+ * // index.js
3033
+ * import messaging from '@react-native-firebase/messaging';
3034
+ * import { createForegroundMessageHandler } from 'swan-react-native-sdk';
3035
+ *
3036
+ * messaging().onMessage(createForegroundMessageHandler());
3037
+ * ```
3038
+ */
3039
+ exports.SwanEcomSDK = exports.default = SwanSDK;
3040
+ function createForegroundMessageHandler() {
3041
+ return async remoteMessage => {
3042
+ _Logger.default.log('[SwanSDK] Foreground notification received:', remoteMessage?.messageId);
3043
+ try {
3044
+ const sdkInstance = SwanSDK.getCurrentInstance();
3045
+ if (!sdkInstance) {
3046
+ _Logger.default.warn('[SwanSDK] SDK not initialized, cannot process foreground notification');
3047
+ return;
3048
+ }
3049
+ if (!sdkInstance.isPushEnabled()) {
3050
+ _Logger.default.log('[SwanSDK] Push notifications disabled, ignoring');
3051
+ return;
3052
+ }
3053
+ await sdkInstance.handleForegroundNotification(remoteMessage);
3054
+ } catch (error) {
3055
+ _Logger.default.error('[SwanSDK] Error handling foreground message:', error);
3056
+ }
3057
+ };
3058
+ }
3059
+
3060
+ /**
3061
+ * Background Message Handler for index.js
3062
+ *
3063
+ * Handles data-only FCM messages when app is in background or killed state.
3064
+ * React Native Firebase requires setBackgroundMessageHandler() to be registered at module level.
3065
+ *
3066
+ * NOTE: This SDK expects DATA-ONLY FCM payloads (no "notification" field).
3067
+ * Backend should set sdkCapabilities.dataOnlyPush check before sending.
3068
+ *
3069
+ * @example
3070
+ * ```javascript
3071
+ * // index.js
3072
+ * import messaging from '@react-native-firebase/messaging';
3073
+ * import { createBackgroundMessageHandler } from 'swan-react-native-sdk';
3074
+ *
3075
+ * messaging().setBackgroundMessageHandler(createBackgroundMessageHandler());
3076
+ * ```
3077
+ */
3078
+ function createBackgroundMessageHandler() {
3079
+ return async remoteMessage => {
3080
+ const messageId = remoteMessage?.messageId;
3081
+ const isSilent = remoteMessage?.data?.silent === 'true' || remoteMessage?.data?.silent === true;
3082
+ _Logger.default.log('[SwanSDK] Background notification received:', messageId, 'silent:', isSilent);
3083
+
3084
+ // Check if push notifications are enabled
3085
+ const sdkInstance = SwanSDK.getCurrentInstance();
3086
+ if (sdkInstance && !sdkInstance.isPushEnabled()) {
3087
+ _Logger.default.log('[SwanSDK] Push notifications disabled, ignoring');
3088
+ return;
3089
+ }
3090
+
3091
+ // Check if iOS Notification Service Extension is active then skip delivery handling
3092
+ const isNESActive = await _SharedCredentialsManager.SharedCredentialsManager.isNotificationServiceExtensionActive();
3093
+ if (isNESActive) {
3094
+ console.log('[SwanSDK] ✅ iOS Notification Service Extension is active, skipping delivery ACK (NES handles it)');
3095
+ return;
3096
+ }
3097
+
3098
+ // Handle silent push (no UI display)
3099
+ if (isSilent) {
3100
+ console.log('[SwanSDK] Silent push received in background, skipping notification display');
3101
+ console.log('[SwanSDK] ✅ Silent push handled');
3102
+ return;
3103
+ }
3104
+ if (sdkInstance && sdkInstance.isReady()) {
3105
+ await sdkInstance.sendNotificationAck(messageId, 'delivered');
3106
+ }
3107
+ try {
3108
+ // Import notifee dynamically to avoid issues if not installed
3109
+ let notifee;
3110
+ try {
3111
+ notifee = require('@notifee/react-native').default;
3112
+ } catch (e) {
3113
+ _Logger.default.error('[SwanSDK] Notifee not installed - required for data-only push notifications');
3114
+ return;
3115
+ }
3116
+
3117
+ // IMPORTANT: Display notification FIRST, then send ACK
3118
+ // Android may kill headless JS task during network requests
3119
+ // So we must show notification before any network calls
3120
+ const messageId = remoteMessage?.messageId;
3121
+ console.log('[SwanSDK] Background handler - messageId:', messageId);
3122
+
3123
+ // Extract notification data from data payload (data-only messages)
3124
+ console.log('[SwanSDK] Extracting notification data from payload...');
3125
+ const title = remoteMessage.data?.title || 'Notification';
3126
+ const body = remoteMessage.data?.body || 'New message';
3127
+ const imageUrl = remoteMessage.data?.image || remoteMessage.data?.fcm_options?.image || '';
3128
+ console.log('[SwanSDK] Notification content - title:', title, 'body:', body);
3129
+
3130
+ // Get notification channel ID from data payload
3131
+ const channelId = remoteMessage?.data?.channelId || remoteMessage?.data?.channel_id || SWAN_NOTIFICATION_CHANNELS.DEFAULT;
3132
+
3133
+ // Get notification priority from payload (for Android 7.1 and below)
3134
+ const getPriority = () => {
3135
+ const priorityStr = remoteMessage?.data?.priority;
3136
+ if (!priorityStr) return undefined;
3137
+ const priorityMap = {
3138
+ max: -2,
3139
+ high: -1,
3140
+ default: 0,
3141
+ low: 1,
3142
+ min: 2
3143
+ };
3144
+ return priorityMap[priorityStr.toLowerCase()];
3145
+ };
3146
+ const priority = getPriority();
3147
+
3148
+ // Use messageId as notification ID for click tracking
3149
+ const notificationId = messageId || `swan_bg_${Date.now()}`;
3150
+ const notificationConfig = {
3151
+ id: notificationId,
3152
+ title,
3153
+ body,
3154
+ data: {
3155
+ ...(remoteMessage.data || {}),
3156
+ // Store messageId for click ACK (must be string for Notifee)
3157
+ ...(messageId && {
3158
+ messageId: String(messageId)
3159
+ })
3160
+ },
3161
+ android: {
3162
+ channelId,
3163
+ smallIcon: 'ic_launcher',
3164
+ ...(priority !== undefined && {
3165
+ priority
3166
+ }),
3167
+ pressAction: {
3168
+ id: 'default'
3169
+ }
3170
+ }
3171
+ };
3172
+
3173
+ // Add image if present
3174
+ if (imageUrl) {
3175
+ const {
3176
+ AndroidStyle
3177
+ } = require('@notifee/react-native');
3178
+ notificationConfig.android.style = {
3179
+ type: AndroidStyle.BIGPICTURE,
3180
+ picture: imageUrl
3181
+ };
3182
+ // iOS: Add image as attachment
3183
+ // Note: For best results on iOS, implement a Notification Service Extension
3184
+ notificationConfig.ios = {
3185
+ attachments: [{
3186
+ url: imageUrl
3187
+ }]
3188
+ };
3189
+ }
3190
+
3191
+ // STEP 1: Display notification FIRST (fast local operation)
3192
+ console.log('[SwanSDK] STEP 1: Displaying notification...');
3193
+ await notifee.displayNotification(notificationConfig);
3194
+ console.log('[SwanSDK] ✅ Notification displayed successfully');
3195
+
3196
+ // STEP 2: Send delivery ACK (network operation - may be killed by OS)
3197
+ console.log('[SwanSDK] STEP 2: Sending delivery ACK...');
3198
+ const isSDKReady = sdkInstance && sdkInstance.isReady();
3199
+ if (isSDKReady) {
3200
+ console.log('[SwanSDK] Using SDK instance for delivery ACK');
3201
+ await sdkInstance.sendNotificationAck(messageId, 'delivered');
3202
+ } else if (messageId) {
3203
+ // SDK not fully initialized (app was killed) - send delivery ACK directly via fetch
3204
+ console.log('[SwanSDK] SDK not ready, sending delivery ACK via direct fetch');
3205
+ // Fire and forget - don't await since OS may kill us
3206
+ sendDirectNotificationAck(messageId, 'delivered').catch(err => {
3207
+ console.log('[SwanSDK] Direct delivery ACK failed:', err);
3208
+ });
3209
+ }
3210
+ console.log('[SwanSDK] ✅ Background handler completed');
3211
+ } catch (error) {
3212
+ console.log('[SwanSDK] ❌ Error handling background message:', error?.message || error);
3213
+ console.log('[SwanSDK] Error stack:', error?.stack);
3214
+ }
3215
+ };
3216
+ }
3217
+ function createNotificationOpenedHandler() {
3218
+ return async event => {
3219
+ try {
3220
+ _Logger.default.log('[SwanSDK] Notification opened Handler:', event);
3221
+ const sdkInstance = SwanSDK.getCurrentInstance();
3222
+ if (!sdkInstance) {
3223
+ _Logger.default.warn('[SwanSDK] SDK not initialized, cannot process notification click');
3224
+ return;
3225
+ }
3226
+ if (!sdkInstance.isPushEnabled()) {
3227
+ return;
3228
+ }
3229
+
3230
+ // Check if iOS Notification Service Extension is active then skip delivery handling
3231
+ const isNESActive = await _SharedCredentialsManager.SharedCredentialsManager.isNotificationServiceExtensionActive();
3232
+ if (!isNESActive && _reactNative.Platform.OS === 'ios') {
3233
+ _Logger.default.log('[SwanSDK] ✅ iOS Notification Service Extension is inactive, will be handled by notifee handlers for click tracking');
3234
+ return;
3235
+ }
3236
+ _Logger.default.log('[SwanSDK] ✅ iOS Notification Service Extension is active, click tracking will be handled now');
3237
+
3238
+ // Get messageId and notification data
3239
+ const messageId = event?.messageId;
3240
+ const notificationData = event?.data || {};
3241
+
3242
+ // Extract deep link information
3243
+ // Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
3244
+ const deepLinkPayload = {
3245
+ route: notificationData.route,
3246
+ data: notificationData,
3247
+ title: notificationData.title,
3248
+ body: notificationData.body
3249
+ };
3250
+ _Logger.default.log('[SwanSDK] Foreground notification clicked:', {
3251
+ messageId,
3252
+ route: deepLinkPayload.route
3253
+ });
3254
+
3255
+ // Emit notificationOpened event for host app to handle
3256
+ sdkInstance.emitNotificationOpened(deepLinkPayload);
3257
+
3258
+ // Send click ACK
3259
+ if (messageId) {
3260
+ await sdkInstance.sendNotificationAck(messageId, 'clicked');
3261
+ } else {
3262
+ _Logger.default.warn('[SwanSDK] No messageId in notification data, skipping click ACK');
3263
+ }
3264
+ } catch (error) {
3265
+ _Logger.default.error('[SwanSDK] Error handling Notifee foreground event:', error);
3266
+ }
3267
+ };
3268
+ }
3269
+
3270
+ /**
3271
+ * Notifee Foreground Event Handler for index.js
3272
+ *
3273
+ * Handles notification clicks when app is in foreground.
3274
+ * Notifee requires onForegroundEvent() to be registered at module level in index.js.
3275
+ *
3276
+ * @example
3277
+ * ```javascript
3278
+ * // index.js
3279
+ * import notifee from '@notifee/react-native';
3280
+ * import { createNotifeeForegroundHandler } from 'swan-react-native-sdk';
3281
+ *
3282
+ * notifee.onForegroundEvent(createNotifeeForegroundHandler());
3283
+ * ```
3284
+ */
3285
+ function createNotifeeForegroundHandler() {
3286
+ return async event => {
3287
+ try {
3288
+ const {
3289
+ EventType
3290
+ } = require('@notifee/react-native');
3291
+
3292
+ // Only handle PRESS events (notification clicks)
3293
+ if (event?.type !== EventType.PRESS) {
3294
+ return;
3295
+ }
3296
+ const sdkInstance = SwanSDK.getCurrentInstance();
3297
+ if (!sdkInstance) {
3298
+ _Logger.default.warn('[SwanSDK] SDK not initialized, cannot process notification click');
3299
+ return;
3300
+ }
3301
+ if (!sdkInstance.isPushEnabled()) {
3302
+ return;
3303
+ }
3304
+
3305
+ // NOTE: Even when NES is active, foreground notifications are displayed via Notifee
3306
+ // (because iOS doesn't auto-display in foreground). So this handler MUST handle clicks
3307
+ // for foreground notifications regardless of NES status.
3308
+ // NES only handles delivery ACK, not click ACK.
3309
+
3310
+ // Get messageId and notification data
3311
+ const messageId = event?.detail?.notification?.data?.messageId;
3312
+ const notificationData = event?.detail?.notification?.data || {};
3313
+
3314
+ // Extract deep link information
3315
+ // Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
3316
+ const deepLinkPayload = {
3317
+ route: notificationData.route,
3318
+ data: notificationData,
3319
+ title: event?.detail?.notification?.title,
3320
+ body: event?.detail?.notification?.body
3321
+ };
3322
+ _Logger.default.log('[SwanSDK] Foreground notification clicked (Notifee):', {
3323
+ messageId,
3324
+ route: deepLinkPayload.route
3325
+ });
3326
+
3327
+ // Emit notificationOpened event for host app to handle
3328
+ sdkInstance.emitNotificationOpened(deepLinkPayload);
3329
+
3330
+ // Send click ACK
3331
+ if (messageId) {
3332
+ await sdkInstance.sendNotificationAck(messageId, 'clicked');
3333
+ } else {
3334
+ _Logger.default.warn('[SwanSDK] No messageId in notification data, skipping click ACK');
3335
+ }
3336
+ } catch (error) {
3337
+ _Logger.default.error('[SwanSDK] Error handling Notifee foreground event:', error);
3338
+ }
3339
+ };
3340
+ }
3341
+
3342
+ /**
3343
+ * Notifee Background Event Handler
3344
+ *
3345
+ * Handles notification clicks when app is in background or killed state.
3346
+ * Must be registered at module level in index.js.
3347
+ *
3348
+ * @example
3349
+ * ```javascript
3350
+ * // index.js
3351
+ * import notifee from '@notifee/react-native';
3352
+ * import { createNotifeeBackgroundHandler } from 'swan-react-native-sdk';
3353
+ *
3354
+ * notifee.onBackgroundEvent(createNotifeeBackgroundHandler());
3355
+ * ```
3356
+ */
3357
+ function createNotifeeBackgroundHandler() {
3358
+ return async ({
3359
+ type,
3360
+ detail
3361
+ }) => {
3362
+ try {
3363
+ const {
3364
+ EventType
3365
+ } = require('@notifee/react-native');
3366
+
3367
+ // Only handle PRESS events (notification clicks)
3368
+ if (type !== EventType.PRESS) {
3369
+ return;
3370
+ }
3371
+
3372
+ // Check if iOS Notification Service Extension is active then skip delivery handling
3373
+ const isNESActive = await _SharedCredentialsManager.SharedCredentialsManager.isNotificationServiceExtensionActive();
3374
+ if (isNESActive) {
3375
+ _Logger.default.log('[SwanSDK] ✅ iOS Notification Service Extension is active, skipping delivery ACK (NES handles it)');
3376
+ return;
3377
+ }
3378
+
3379
+ // Get messageId and notification data
3380
+ const messageId = detail?.notification?.data?.messageId;
3381
+ const notificationData = detail?.notification?.data || {};
3382
+
3383
+ // Extract deep link information
3384
+ // Note: route field can contain either a path (/products/123) or full URL (myapp://products/123)
3385
+ const deepLinkPayload = {
3386
+ route: notificationData.route,
3387
+ data: notificationData,
3388
+ title: detail?.notification?.title,
3389
+ body: detail?.notification?.body
3390
+ };
3391
+ _Logger.default.log('[SwanSDK] Background notification clicked:', {
3392
+ messageId,
3393
+ route: deepLinkPayload.route
3394
+ });
3395
+
3396
+ // Try to use SDK instance if available and ready (has deviceId)
3397
+ const sdkInstance = SwanSDK.getCurrentInstance();
3398
+ const isSDKReady = sdkInstance && sdkInstance.isReady();
3399
+ if (isSDKReady) {
3400
+ if (!sdkInstance.isPushEnabled()) {
3401
+ return;
3402
+ }
3403
+
3404
+ // Emit notificationOpened event for host app to handle
3405
+ sdkInstance.emitNotificationOpened(deepLinkPayload);
3406
+
3407
+ // Send click ACK
3408
+ if (messageId) {
3409
+ await sdkInstance.sendNotificationAck(messageId, 'clicked');
3410
+ _Logger.default.log('[SwanSDK] Click ACK sent via SDK');
3411
+ }
3412
+ return;
3413
+ }
3414
+
3415
+ // SDK not ready (app was killed) - send ACK directly via fetch
3416
+ _Logger.default.log('[SwanSDK] SDK not ready, sending click ACK via direct fetch');
3417
+ if (messageId) {
3418
+ await sendDirectNotificationAck(messageId, 'clicked');
3419
+ }
3420
+ } catch (error) {
3421
+ _Logger.default.error('[SwanSDK] Error in background click handler:', error);
3422
+ }
3423
+ };
3424
+ }
3425
+
3426
+ /**
3427
+ * Send notification ACK directly when SDK is not initialized (app was killed)
3428
+ * Used for both delivery and click ACKs when SDK instance is not available
3429
+ * Uses Firebase messageId for tracking (sent as commId to backend)
3430
+ * @internal
3431
+ */
3432
+ async function sendDirectNotificationAck(messageId, event) {
3433
+ // Use console.log directly since Logger may be disabled in headless context
3434
+ console.log(`[SwanSDK] sendDirectNotificationAck called - event: ${event}, messageId: ${messageId}`);
3435
+ try {
3436
+ const AsyncStorageModule = require('@react-native-async-storage/async-storage').default;
3437
+ const Base64Module = require('react-native-base64').default;
3438
+
3439
+ // Read credentials from AsyncStorage
3440
+ console.log('[SwanSDK] Reading credentials from AsyncStorage...');
3441
+ const encoded = await AsyncStorageModule.getItem('swanCredentials');
3442
+ if (!encoded) {
3443
+ console.log(`[SwanSDK] No credentials found, cannot send ${event} ACK`);
3444
+ return;
3445
+ }
3446
+ console.log('[SwanSDK] Credentials found, decoding...');
3447
+ const credentials = JSON.parse(Base64Module.decode(encoded));
3448
+ const {
3449
+ appId,
3450
+ deviceId,
3451
+ currentCDID,
3452
+ generatedCDID,
3453
+ isProduction
3454
+ } = credentials;
3455
+ console.log(`[SwanSDK] Credentials - appId: ${appId}, deviceId: ${deviceId}, CDID: ${currentCDID || generatedCDID}, isProduction: ${isProduction}`);
3456
+ if (!appId || !deviceId) {
3457
+ console.log(`[SwanSDK] Invalid credentials, cannot send ${event} ACK`);
3458
+ return;
3459
+ }
3460
+
3461
+ // Backend expects commId field - we send Firebase messageId as the value
3462
+ const payload = {
3463
+ commId: messageId,
3464
+ appId,
3465
+ CDID: currentCDID || generatedCDID,
3466
+ event,
3467
+ deviceId
3468
+ };
3469
+ const urlType = isProduction === 'PROD' ? 'PROD' : 'STAGE';
3470
+ console.log(`[SwanSDK] Sending ${event} ACK to: ${_ApiUrls.default.WEBHOOK_MOBILE_PUSH_URL[urlType]}`);
3471
+ console.log('[SwanSDK] Payload:', JSON.stringify(payload));
3472
+ const controller = new AbortController();
3473
+ const timeoutId = setTimeout(() => {
3474
+ console.log('[SwanSDK] ACK request timeout triggered (10s)');
3475
+ controller.abort();
3476
+ }, 10000);
3477
+ console.log('[SwanSDK] Starting fetch...');
3478
+ const response = await fetch(_ApiUrls.default.WEBHOOK_MOBILE_PUSH_URL[urlType], {
3479
+ method: 'POST',
3480
+ headers: {
3481
+ 'Content-Type': 'application/json',
3482
+ 'X-Swan-Device-Id': deviceId
3483
+ },
3484
+ body: JSON.stringify(payload),
3485
+ signal: controller.signal
3486
+ });
3487
+ console.log('[SwanSDK] Fetch completed, status:', response.status);
3488
+ clearTimeout(timeoutId);
3489
+ if (response.ok) {
3490
+ console.log(`[SwanSDK] ✅ ${event} ACK sent successfully via direct fetch`);
3491
+ } else {
3492
+ const responseText = await response.text();
3493
+ console.log(`[SwanSDK] ${event} ACK failed - HTTP ${response.status}: ${responseText}`);
3494
+ }
3495
+ } catch (error) {
3496
+ console.log('[SwanSDK] Caught error in sendDirectNotificationAck');
3497
+ if (error?.name === 'AbortError') {
3498
+ console.log(`[SwanSDK] ${event} ACK timed out (AbortError)`);
3499
+ } else {
3500
+ console.log(`[SwanSDK] Error sending ${event} ACK:`, error?.message || error);
3501
+ }
3502
+ }
3503
+ }
3504
+
3505
+ // Named exports for convenience
3506
+ //# sourceMappingURL=index.js.map