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