@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.
- package/LICENSE +55 -0
- package/README.md +67 -0
- package/docs/IOS_NOTIFICATION_EXTENSION_SETUP.md +335 -0
- package/ios/README.md +64 -0
- package/ios/SwanNotificationServiceExtension/Info.plist +31 -0
- package/ios/SwanNotificationServiceExtension/NotificationService.swift +337 -0
- package/ios/SwanNotificationServiceExtension/SwanNotificationServiceExtension.entitlements +10 -0
- package/lib/commonjs/components/FooterView.js +125 -0
- package/lib/commonjs/components/FooterView.js.map +1 -0
- package/lib/commonjs/components/FullScreenView.js +172 -0
- package/lib/commonjs/components/FullScreenView.js.map +1 -0
- package/lib/commonjs/components/HeaderView.js +205 -0
- package/lib/commonjs/components/HeaderView.js.map +1 -0
- package/lib/commonjs/components/PopUpView.js +186 -0
- package/lib/commonjs/components/PopUpView.js.map +1 -0
- package/lib/commonjs/config/BatchConfig.js +53 -0
- package/lib/commonjs/config/BatchConfig.js.map +1 -0
- package/lib/commonjs/constants/ApiUrls.js +56 -0
- package/lib/commonjs/constants/ApiUrls.js.map +1 -0
- package/lib/commonjs/core/EventQueueManager.js +345 -0
- package/lib/commonjs/core/EventQueueManager.js.map +1 -0
- package/lib/commonjs/core/FlushManager.js +245 -0
- package/lib/commonjs/core/FlushManager.js.map +1 -0
- package/lib/commonjs/core/NetworkMonitor.js +97 -0
- package/lib/commonjs/core/NetworkMonitor.js.map +1 -0
- package/lib/commonjs/index.js +3506 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/providers/FirebasePushProvider.js +130 -0
- package/lib/commonjs/providers/FirebasePushProvider.js.map +1 -0
- package/lib/commonjs/providers/NullPushProvider.js +59 -0
- package/lib/commonjs/providers/NullPushProvider.js.map +1 -0
- package/lib/commonjs/providers/PushNotificationProvider.js +30 -0
- package/lib/commonjs/providers/PushNotificationProvider.js.map +1 -0
- package/lib/commonjs/services/DeviceRegistrationService.js +248 -0
- package/lib/commonjs/services/DeviceRegistrationService.js.map +1 -0
- package/lib/commonjs/services/PushTokenService.js +284 -0
- package/lib/commonjs/services/PushTokenService.js.map +1 -0
- package/lib/commonjs/state/AuthStateMachine.js +161 -0
- package/lib/commonjs/state/AuthStateMachine.js.map +1 -0
- package/lib/commonjs/state/DeviceStateMachine.js +104 -0
- package/lib/commonjs/state/DeviceStateMachine.js.map +1 -0
- package/lib/commonjs/state/PushStateMachine.js +129 -0
- package/lib/commonjs/state/PushStateMachine.js.map +1 -0
- package/lib/commonjs/types/EventQueue.js +50 -0
- package/lib/commonjs/types/EventQueue.js.map +1 -0
- package/lib/commonjs/types/SDK.js +2 -0
- package/lib/commonjs/types/SDK.js.map +1 -0
- package/lib/commonjs/utils/FirebaseNotificationManager.js +492 -0
- package/lib/commonjs/utils/FirebaseNotificationManager.js.map +1 -0
- package/lib/commonjs/utils/Logger.js +56 -0
- package/lib/commonjs/utils/Logger.js.map +1 -0
- package/lib/commonjs/utils/SharedCredentialsManager.js +146 -0
- package/lib/commonjs/utils/SharedCredentialsManager.js.map +1 -0
- package/lib/commonjs/version.js +12 -0
- package/lib/commonjs/version.js.map +1 -0
- package/lib/module/components/FooterView.js +121 -0
- package/lib/module/components/FooterView.js.map +1 -0
- package/lib/module/components/FullScreenView.js +167 -0
- package/lib/module/components/FullScreenView.js.map +1 -0
- package/lib/module/components/HeaderView.js +199 -0
- package/lib/module/components/HeaderView.js.map +1 -0
- package/lib/module/components/PopUpView.js +181 -0
- package/lib/module/components/PopUpView.js.map +1 -0
- package/lib/module/config/BatchConfig.js +49 -0
- package/lib/module/config/BatchConfig.js.map +1 -0
- package/lib/module/constants/ApiUrls.js +52 -0
- package/lib/module/constants/ApiUrls.js.map +1 -0
- package/lib/module/core/EventQueueManager.js +340 -0
- package/lib/module/core/EventQueueManager.js.map +1 -0
- package/lib/module/core/FlushManager.js +240 -0
- package/lib/module/core/FlushManager.js.map +1 -0
- package/lib/module/core/NetworkMonitor.js +92 -0
- package/lib/module/core/NetworkMonitor.js.map +1 -0
- package/lib/module/index.js +3494 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/providers/FirebasePushProvider.js +124 -0
- package/lib/module/providers/FirebasePushProvider.js.map +1 -0
- package/lib/module/providers/NullPushProvider.js +53 -0
- package/lib/module/providers/NullPushProvider.js.map +1 -0
- package/lib/module/providers/PushNotificationProvider.js +26 -0
- package/lib/module/providers/PushNotificationProvider.js.map +1 -0
- package/lib/module/services/DeviceRegistrationService.js +243 -0
- package/lib/module/services/DeviceRegistrationService.js.map +1 -0
- package/lib/module/services/PushTokenService.js +278 -0
- package/lib/module/services/PushTokenService.js.map +1 -0
- package/lib/module/state/AuthStateMachine.js +155 -0
- package/lib/module/state/AuthStateMachine.js.map +1 -0
- package/lib/module/state/DeviceStateMachine.js +98 -0
- package/lib/module/state/DeviceStateMachine.js.map +1 -0
- package/lib/module/state/PushStateMachine.js +123 -0
- package/lib/module/state/PushStateMachine.js.map +1 -0
- package/lib/module/types/EventQueue.js +46 -0
- package/lib/module/types/EventQueue.js.map +1 -0
- package/lib/module/types/SDK.js +2 -0
- package/lib/module/types/SDK.js.map +1 -0
- package/lib/module/utils/FirebaseNotificationManager.js +486 -0
- package/lib/module/utils/FirebaseNotificationManager.js.map +1 -0
- package/lib/module/utils/Logger.js +52 -0
- package/lib/module/utils/Logger.js.map +1 -0
- package/lib/module/utils/SharedCredentialsManager.js +140 -0
- package/lib/module/utils/SharedCredentialsManager.js.map +1 -0
- package/lib/module/version.js +8 -0
- package/lib/module/version.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/components/FooterView.d.ts +3 -0
- package/lib/typescript/commonjs/src/components/FooterView.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/components/FullScreenView.d.ts +3 -0
- package/lib/typescript/commonjs/src/components/FullScreenView.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/components/HeaderView.d.ts +3 -0
- package/lib/typescript/commonjs/src/components/HeaderView.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/components/PopUpView.d.ts +3 -0
- package/lib/typescript/commonjs/src/components/PopUpView.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/config/BatchConfig.d.ts +7 -0
- package/lib/typescript/commonjs/src/config/BatchConfig.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/constants/ApiUrls.d.ts +56 -0
- package/lib/typescript/commonjs/src/constants/ApiUrls.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/core/EventQueueManager.d.ts +63 -0
- package/lib/typescript/commonjs/src/core/EventQueueManager.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/core/FlushManager.d.ts +63 -0
- package/lib/typescript/commonjs/src/core/FlushManager.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/core/NetworkMonitor.d.ts +38 -0
- package/lib/typescript/commonjs/src/core/NetworkMonitor.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +663 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/providers/FirebasePushProvider.d.ts +28 -0
- package/lib/typescript/commonjs/src/providers/FirebasePushProvider.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts +25 -0
- package/lib/typescript/commonjs/src/providers/NullPushProvider.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/providers/PushNotificationProvider.d.ts +105 -0
- package/lib/typescript/commonjs/src/providers/PushNotificationProvider.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/services/DeviceRegistrationService.d.ts +60 -0
- package/lib/typescript/commonjs/src/services/DeviceRegistrationService.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/services/PushTokenService.d.ts +82 -0
- package/lib/typescript/commonjs/src/services/PushTokenService.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/state/AuthStateMachine.d.ts +61 -0
- package/lib/typescript/commonjs/src/state/AuthStateMachine.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/state/DeviceStateMachine.d.ts +51 -0
- package/lib/typescript/commonjs/src/state/DeviceStateMachine.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/state/PushStateMachine.d.ts +61 -0
- package/lib/typescript/commonjs/src/state/PushStateMachine.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types/EventQueue.d.ts +85 -0
- package/lib/typescript/commonjs/src/types/EventQueue.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types/SDK.d.ts +54 -0
- package/lib/typescript/commonjs/src/types/SDK.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts +169 -0
- package/lib/typescript/commonjs/src/utils/FirebaseNotificationManager.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/utils/Logger.d.ts +32 -0
- package/lib/typescript/commonjs/src/utils/Logger.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts +54 -0
- package/lib/typescript/commonjs/src/utils/SharedCredentialsManager.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/version.d.ts +2 -0
- package/lib/typescript/commonjs/src/version.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/components/FooterView.d.ts +3 -0
- package/lib/typescript/module/src/components/FooterView.d.ts.map +1 -0
- package/lib/typescript/module/src/components/FullScreenView.d.ts +3 -0
- package/lib/typescript/module/src/components/FullScreenView.d.ts.map +1 -0
- package/lib/typescript/module/src/components/HeaderView.d.ts +3 -0
- package/lib/typescript/module/src/components/HeaderView.d.ts.map +1 -0
- package/lib/typescript/module/src/components/PopUpView.d.ts +3 -0
- package/lib/typescript/module/src/components/PopUpView.d.ts.map +1 -0
- package/lib/typescript/module/src/config/BatchConfig.d.ts +7 -0
- package/lib/typescript/module/src/config/BatchConfig.d.ts.map +1 -0
- package/lib/typescript/module/src/constants/ApiUrls.d.ts +56 -0
- package/lib/typescript/module/src/constants/ApiUrls.d.ts.map +1 -0
- package/lib/typescript/module/src/core/EventQueueManager.d.ts +63 -0
- package/lib/typescript/module/src/core/EventQueueManager.d.ts.map +1 -0
- package/lib/typescript/module/src/core/FlushManager.d.ts +63 -0
- package/lib/typescript/module/src/core/FlushManager.d.ts.map +1 -0
- package/lib/typescript/module/src/core/NetworkMonitor.d.ts +38 -0
- package/lib/typescript/module/src/core/NetworkMonitor.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +663 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/providers/FirebasePushProvider.d.ts +28 -0
- package/lib/typescript/module/src/providers/FirebasePushProvider.d.ts.map +1 -0
- package/lib/typescript/module/src/providers/NullPushProvider.d.ts +25 -0
- package/lib/typescript/module/src/providers/NullPushProvider.d.ts.map +1 -0
- package/lib/typescript/module/src/providers/PushNotificationProvider.d.ts +105 -0
- package/lib/typescript/module/src/providers/PushNotificationProvider.d.ts.map +1 -0
- package/lib/typescript/module/src/services/DeviceRegistrationService.d.ts +60 -0
- package/lib/typescript/module/src/services/DeviceRegistrationService.d.ts.map +1 -0
- package/lib/typescript/module/src/services/PushTokenService.d.ts +82 -0
- package/lib/typescript/module/src/services/PushTokenService.d.ts.map +1 -0
- package/lib/typescript/module/src/state/AuthStateMachine.d.ts +61 -0
- package/lib/typescript/module/src/state/AuthStateMachine.d.ts.map +1 -0
- package/lib/typescript/module/src/state/DeviceStateMachine.d.ts +51 -0
- package/lib/typescript/module/src/state/DeviceStateMachine.d.ts.map +1 -0
- package/lib/typescript/module/src/state/PushStateMachine.d.ts +61 -0
- package/lib/typescript/module/src/state/PushStateMachine.d.ts.map +1 -0
- package/lib/typescript/module/src/types/EventQueue.d.ts +85 -0
- package/lib/typescript/module/src/types/EventQueue.d.ts.map +1 -0
- package/lib/typescript/module/src/types/SDK.d.ts +54 -0
- package/lib/typescript/module/src/types/SDK.d.ts.map +1 -0
- package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts +169 -0
- package/lib/typescript/module/src/utils/FirebaseNotificationManager.d.ts.map +1 -0
- package/lib/typescript/module/src/utils/Logger.d.ts +32 -0
- package/lib/typescript/module/src/utils/Logger.d.ts.map +1 -0
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts +54 -0
- package/lib/typescript/module/src/utils/SharedCredentialsManager.d.ts.map +1 -0
- package/lib/typescript/module/src/version.d.ts +2 -0
- package/lib/typescript/module/src/version.d.ts.map +1 -0
- package/package.json +230 -0
- package/scripts/generate-version.js +25 -0
- 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
|