@datalyr/react-native 1.3.0 → 1.3.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/CHANGELOG.md +19 -0
- package/README.md +145 -9
- package/ios/DatalyrSKAdNetwork.m +351 -3
- package/ios/PrivacyInfo.xcprivacy +48 -0
- package/lib/datalyr-sdk.d.ts +6 -0
- package/lib/datalyr-sdk.js +84 -27
- package/lib/index.d.ts +3 -1
- package/lib/index.js +3 -1
- package/lib/integrations/play-install-referrer.d.ts +5 -1
- package/lib/integrations/play-install-referrer.js +14 -4
- package/lib/native/SKAdNetworkBridge.d.ts +121 -0
- package/lib/native/SKAdNetworkBridge.js +276 -2
- package/lib/network-status.d.ts +84 -0
- package/lib/network-status.js +281 -0
- package/lib/types.d.ts +51 -0
- package/lib/utils.d.ts +6 -1
- package/lib/utils.js +52 -2
- package/package.json +6 -2
- package/src/datalyr-sdk.ts +96 -32
- package/src/index.ts +5 -1
- package/src/integrations/play-install-referrer.ts +19 -4
- package/src/native/SKAdNetworkBridge.ts +400 -5
- package/src/network-status.ts +312 -0
- package/src/types.ts +74 -6
- package/src/utils.ts +62 -6
|
@@ -36,7 +36,7 @@ export class SKAdNetworkBridge {
|
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
38
|
try {
|
|
39
|
-
const
|
|
39
|
+
const response = await DatalyrSKAdNetwork.updatePostbackConversionValue(result.fineValue, result.coarseValue, result.lockWindow);
|
|
40
40
|
const isSKAN4 = await this.isSKAN4Available();
|
|
41
41
|
if (isSKAN4) {
|
|
42
42
|
console.log(`[Datalyr] SKAN 4.0 postback updated: fineValue=${result.fineValue}, coarseValue=${result.coarseValue}, lockWindow=${result.lockWindow}`);
|
|
@@ -44,7 +44,7 @@ export class SKAdNetworkBridge {
|
|
|
44
44
|
else {
|
|
45
45
|
console.log(`[Datalyr] SKAN 3.0 fallback: conversionValue=${result.fineValue}`);
|
|
46
46
|
}
|
|
47
|
-
return success;
|
|
47
|
+
return response.success;
|
|
48
48
|
}
|
|
49
49
|
catch (error) {
|
|
50
50
|
console.warn('[Datalyr] Failed to update SKAdNetwork postback conversion value:', error);
|
|
@@ -75,5 +75,279 @@ export class SKAdNetworkBridge {
|
|
|
75
75
|
static isAvailable() {
|
|
76
76
|
return Platform.OS === 'ios' && !!DatalyrSKAdNetwork;
|
|
77
77
|
}
|
|
78
|
+
static async isAdAttributionKitAvailable() {
|
|
79
|
+
if (Platform.OS !== 'ios') {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (this._isAdAttributionKitAvailable !== null) {
|
|
83
|
+
return this._isAdAttributionKitAvailable;
|
|
84
|
+
}
|
|
85
|
+
if (!(DatalyrSKAdNetwork === null || DatalyrSKAdNetwork === void 0 ? void 0 : DatalyrSKAdNetwork.isAdAttributionKitAvailable)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
this._isAdAttributionKitAvailable = await DatalyrSKAdNetwork.isAdAttributionKitAvailable();
|
|
90
|
+
return this._isAdAttributionKitAvailable;
|
|
91
|
+
}
|
|
92
|
+
catch (_a) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Register for ad network attribution
|
|
98
|
+
* Uses AdAttributionKit on iOS 17.4+, SKAdNetwork on earlier versions
|
|
99
|
+
*/
|
|
100
|
+
static async registerForAttribution() {
|
|
101
|
+
if (Platform.OS !== 'ios') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (!(DatalyrSKAdNetwork === null || DatalyrSKAdNetwork === void 0 ? void 0 : DatalyrSKAdNetwork.registerForAttribution)) {
|
|
105
|
+
console.warn('[Datalyr] Attribution registration not available');
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const result = await DatalyrSKAdNetwork.registerForAttribution();
|
|
110
|
+
console.log(`[Datalyr] Registered for attribution: ${result.framework}`);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.warn('[Datalyr] Failed to register for attribution:', error);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get attribution framework info
|
|
120
|
+
* Returns details about which framework is being used and its capabilities
|
|
121
|
+
*/
|
|
122
|
+
static async getAttributionInfo() {
|
|
123
|
+
if (Platform.OS !== 'ios') {
|
|
124
|
+
return {
|
|
125
|
+
framework: 'none',
|
|
126
|
+
version: '0',
|
|
127
|
+
reengagement_available: false,
|
|
128
|
+
overlapping_windows: false,
|
|
129
|
+
fine_value_range: { min: 0, max: 0 },
|
|
130
|
+
coarse_values: [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (!(DatalyrSKAdNetwork === null || DatalyrSKAdNetwork === void 0 ? void 0 : DatalyrSKAdNetwork.getAttributionInfo)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
return await DatalyrSKAdNetwork.getAttributionInfo();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.warn('[Datalyr] Failed to get attribution info:', error);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
static async isOverlappingWindowsAvailable() {
|
|
145
|
+
if (Platform.OS !== 'ios') {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (this._isOverlappingWindowsAvailable !== null) {
|
|
149
|
+
return this._isOverlappingWindowsAvailable;
|
|
150
|
+
}
|
|
151
|
+
if (!(DatalyrSKAdNetwork === null || DatalyrSKAdNetwork === void 0 ? void 0 : DatalyrSKAdNetwork.isOverlappingWindowsAvailable)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
this._isOverlappingWindowsAvailable = await DatalyrSKAdNetwork.isOverlappingWindowsAvailable();
|
|
156
|
+
return this._isOverlappingWindowsAvailable;
|
|
157
|
+
}
|
|
158
|
+
catch (_a) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Update conversion value for re-engagement attribution (AdAttributionKit iOS 17.4+ only)
|
|
164
|
+
* Re-engagement tracks users who return to the app via an ad after initial install.
|
|
165
|
+
*
|
|
166
|
+
* @param result - Conversion result with fine value (0-63), coarse value, and lock window
|
|
167
|
+
* @returns Response with framework info, or null if not supported
|
|
168
|
+
*/
|
|
169
|
+
static async updateReengagementConversionValue(result) {
|
|
170
|
+
if (Platform.OS !== 'ios') {
|
|
171
|
+
return null; // Android doesn't support AdAttributionKit
|
|
172
|
+
}
|
|
173
|
+
// Check if AdAttributionKit is available (required for re-engagement)
|
|
174
|
+
const isAAKAvailable = await this.isAdAttributionKitAvailable();
|
|
175
|
+
if (!isAAKAvailable) {
|
|
176
|
+
console.warn('[Datalyr] Re-engagement attribution requires iOS 17.4+ (AdAttributionKit)');
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (!(DatalyrSKAdNetwork === null || DatalyrSKAdNetwork === void 0 ? void 0 : DatalyrSKAdNetwork.updateReengagementConversionValue)) {
|
|
180
|
+
console.warn('[Datalyr] Re-engagement native module not available');
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const response = await DatalyrSKAdNetwork.updateReengagementConversionValue(result.fineValue, result.coarseValue, result.lockWindow);
|
|
185
|
+
console.log(`[Datalyr] AdAttributionKit re-engagement updated: fineValue=${result.fineValue}, coarseValue=${result.coarseValue}, lockWindow=${result.lockWindow}`);
|
|
186
|
+
return response;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.warn('[Datalyr] Failed to update re-engagement conversion value:', error);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get a summary of attribution capabilities for the current device
|
|
195
|
+
*/
|
|
196
|
+
static async getCapabilitiesSummary() {
|
|
197
|
+
var _a, _b;
|
|
198
|
+
const info = await this.getAttributionInfo();
|
|
199
|
+
const isSKAN4 = await this.isSKAN4Available();
|
|
200
|
+
const isAAK = await this.isAdAttributionKitAvailable();
|
|
201
|
+
const isOverlapping = await this.isOverlappingWindowsAvailable();
|
|
202
|
+
const isGeo = await this.isGeoPostbackAvailable();
|
|
203
|
+
return {
|
|
204
|
+
skadnetwork3: Platform.OS === 'ios',
|
|
205
|
+
skadnetwork4: isSKAN4,
|
|
206
|
+
adAttributionKit: isAAK,
|
|
207
|
+
reengagement: (_a = info === null || info === void 0 ? void 0 : info.reengagement_available) !== null && _a !== void 0 ? _a : false,
|
|
208
|
+
overlappingWindows: isOverlapping,
|
|
209
|
+
geoPostback: isGeo,
|
|
210
|
+
developmentPostbacks: isGeo, // Same iOS version requirement
|
|
211
|
+
framework: (_b = info === null || info === void 0 ? void 0 : info.framework) !== null && _b !== void 0 ? _b : 'none',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
static async isGeoPostbackAvailable() {
|
|
215
|
+
if (Platform.OS !== 'ios') {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (this._isGeoPostbackAvailable !== null) {
|
|
219
|
+
return this._isGeoPostbackAvailable;
|
|
220
|
+
}
|
|
221
|
+
if (!(DatalyrSKAdNetwork === null || DatalyrSKAdNetwork === void 0 ? void 0 : DatalyrSKAdNetwork.isGeoPostbackAvailable)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
this._isGeoPostbackAvailable = await DatalyrSKAdNetwork.isGeoPostbackAvailable();
|
|
226
|
+
return this._isGeoPostbackAvailable;
|
|
227
|
+
}
|
|
228
|
+
catch (_a) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Set postback environment for testing (iOS 18.4+)
|
|
234
|
+
* Note: Actual sandbox mode requires Developer Mode enabled in iOS Settings
|
|
235
|
+
*
|
|
236
|
+
* @param environment - 'production' or 'sandbox'
|
|
237
|
+
*/
|
|
238
|
+
static async setPostbackEnvironment(environment) {
|
|
239
|
+
if (Platform.OS !== 'ios') {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
if (!(DatalyrSKAdNetwork === null || DatalyrSKAdNetwork === void 0 ? void 0 : DatalyrSKAdNetwork.setPostbackEnvironment)) {
|
|
243
|
+
console.warn('[Datalyr] Development postbacks require iOS 18.4+');
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const result = await DatalyrSKAdNetwork.setPostbackEnvironment(environment);
|
|
248
|
+
console.log(`[Datalyr] Postback environment: ${result.environment}`);
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.warn('[Datalyr] Failed to set postback environment:', error);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get enhanced attribution info including iOS 18.4+ features
|
|
258
|
+
* Returns details about geo postbacks, development mode, and all available features
|
|
259
|
+
*/
|
|
260
|
+
static async getEnhancedAttributionInfo() {
|
|
261
|
+
if (Platform.OS !== 'ios') {
|
|
262
|
+
return {
|
|
263
|
+
framework: 'none',
|
|
264
|
+
version: '0',
|
|
265
|
+
reengagement_available: false,
|
|
266
|
+
overlapping_windows: false,
|
|
267
|
+
geo_postback_available: false,
|
|
268
|
+
development_postbacks: false,
|
|
269
|
+
fine_value_range: { min: 0, max: 0 },
|
|
270
|
+
coarse_values: [],
|
|
271
|
+
features: [],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (!(DatalyrSKAdNetwork === null || DatalyrSKAdNetwork === void 0 ? void 0 : DatalyrSKAdNetwork.getEnhancedAttributionInfo)) {
|
|
275
|
+
// Fallback to basic info if enhanced not available
|
|
276
|
+
const basicInfo = await this.getAttributionInfo();
|
|
277
|
+
if (basicInfo) {
|
|
278
|
+
return {
|
|
279
|
+
...basicInfo,
|
|
280
|
+
geo_postback_available: false,
|
|
281
|
+
development_postbacks: false,
|
|
282
|
+
features: [],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
return await DatalyrSKAdNetwork.getEnhancedAttributionInfo();
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
console.warn('[Datalyr] Failed to get enhanced attribution info:', error);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Update postback with overlapping window support (iOS 18.4+)
|
|
297
|
+
* Allows tracking conversions across multiple time windows simultaneously
|
|
298
|
+
*
|
|
299
|
+
* @param result - Conversion result with fine value, coarse value, and lock window
|
|
300
|
+
* @param windowIndex - Window index: 0 (0-2 days), 1 (3-7 days), 2 (8-35 days)
|
|
301
|
+
*/
|
|
302
|
+
static async updatePostbackWithWindow(result, windowIndex) {
|
|
303
|
+
if (Platform.OS !== 'ios') {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (!(DatalyrSKAdNetwork === null || DatalyrSKAdNetwork === void 0 ? void 0 : DatalyrSKAdNetwork.updatePostbackWithWindow)) {
|
|
307
|
+
console.warn('[Datalyr] Overlapping windows require iOS 16.1+ (full support on iOS 18.4+)');
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const response = await DatalyrSKAdNetwork.updatePostbackWithWindow(result.fineValue, result.coarseValue, result.lockWindow, windowIndex);
|
|
312
|
+
console.log(`[Datalyr] Postback updated for window ${windowIndex}: fineValue=${result.fineValue}, overlapping=${response.overlappingWindows}`);
|
|
313
|
+
return response;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
console.warn('[Datalyr] Failed to update postback with window:', error);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Enable development/sandbox mode for testing attribution
|
|
322
|
+
* Convenience method that sets sandbox environment
|
|
323
|
+
*/
|
|
324
|
+
static async enableDevelopmentMode() {
|
|
325
|
+
var _a;
|
|
326
|
+
const result = await this.setPostbackEnvironment('sandbox');
|
|
327
|
+
return (_a = result === null || result === void 0 ? void 0 : result.isSandbox) !== null && _a !== void 0 ? _a : false;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Disable development mode (switch to production)
|
|
331
|
+
*/
|
|
332
|
+
static async disableDevelopmentMode() {
|
|
333
|
+
const result = await this.setPostbackEnvironment('production');
|
|
334
|
+
return result !== null && !result.isSandbox;
|
|
335
|
+
}
|
|
78
336
|
}
|
|
79
337
|
SKAdNetworkBridge._isSKAN4Available = null;
|
|
338
|
+
/**
|
|
339
|
+
* Check if AdAttributionKit is available (iOS 17.4+)
|
|
340
|
+
* AdAttributionKit is Apple's replacement for SKAdNetwork with enhanced features
|
|
341
|
+
*/
|
|
342
|
+
SKAdNetworkBridge._isAdAttributionKitAvailable = null;
|
|
343
|
+
/**
|
|
344
|
+
* Check if overlapping conversion windows are available (iOS 18.4+)
|
|
345
|
+
* Overlapping windows allow multiple conversion windows to be active simultaneously
|
|
346
|
+
*/
|
|
347
|
+
SKAdNetworkBridge._isOverlappingWindowsAvailable = null;
|
|
348
|
+
// ===== iOS 18.4+ Features =====
|
|
349
|
+
/**
|
|
350
|
+
* Check if geo-level postback data is available (iOS 18.4+)
|
|
351
|
+
* Geo postbacks include country code information for regional analytics
|
|
352
|
+
*/
|
|
353
|
+
SKAdNetworkBridge._isGeoPostbackAvailable = null;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export type NetworkState = {
|
|
2
|
+
isConnected: boolean;
|
|
3
|
+
isInternetReachable: boolean | null;
|
|
4
|
+
type: 'wifi' | 'cellular' | 'ethernet' | 'bluetooth' | 'vpn' | 'none' | 'unknown';
|
|
5
|
+
};
|
|
6
|
+
export type NetworkStateListener = (state: NetworkState) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Network status manager that detects online/offline status
|
|
9
|
+
* Uses @react-native-community/netinfo for React Native or expo-network for Expo
|
|
10
|
+
*/
|
|
11
|
+
declare class NetworkStatusManager {
|
|
12
|
+
private state;
|
|
13
|
+
private listeners;
|
|
14
|
+
private unsubscribe;
|
|
15
|
+
private initialized;
|
|
16
|
+
private netInfoModule;
|
|
17
|
+
private expoNetworkModule;
|
|
18
|
+
/**
|
|
19
|
+
* Initialize network status monitoring
|
|
20
|
+
* Call this during SDK initialization
|
|
21
|
+
*/
|
|
22
|
+
initialize(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Initialize with @react-native-community/netinfo
|
|
25
|
+
*/
|
|
26
|
+
private initializeWithNetInfo;
|
|
27
|
+
/**
|
|
28
|
+
* Update state from NetInfo response
|
|
29
|
+
*/
|
|
30
|
+
private updateStateFromNetInfo;
|
|
31
|
+
/**
|
|
32
|
+
* Map NetInfo type to our simplified type
|
|
33
|
+
*/
|
|
34
|
+
private mapNetInfoType;
|
|
35
|
+
/**
|
|
36
|
+
* Initialize with expo-network
|
|
37
|
+
*/
|
|
38
|
+
private initializeWithExpoNetwork;
|
|
39
|
+
/**
|
|
40
|
+
* Update state from expo-network response
|
|
41
|
+
*/
|
|
42
|
+
private updateStateFromExpoNetwork;
|
|
43
|
+
/**
|
|
44
|
+
* Map expo-network type to our simplified type
|
|
45
|
+
*/
|
|
46
|
+
private mapExpoNetworkType;
|
|
47
|
+
/**
|
|
48
|
+
* Poll expo-network for changes (since it doesn't have a listener API)
|
|
49
|
+
*/
|
|
50
|
+
private pollingInterval;
|
|
51
|
+
private startExpoNetworkPolling;
|
|
52
|
+
/**
|
|
53
|
+
* Notify all listeners of state change
|
|
54
|
+
*/
|
|
55
|
+
private notifyListeners;
|
|
56
|
+
/**
|
|
57
|
+
* Get current network state
|
|
58
|
+
*/
|
|
59
|
+
getState(): NetworkState;
|
|
60
|
+
/**
|
|
61
|
+
* Check if device is currently online
|
|
62
|
+
*/
|
|
63
|
+
isOnline(): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Get current network type
|
|
66
|
+
*/
|
|
67
|
+
getNetworkType(): NetworkState['type'];
|
|
68
|
+
/**
|
|
69
|
+
* Subscribe to network state changes
|
|
70
|
+
* Returns an unsubscribe function
|
|
71
|
+
*/
|
|
72
|
+
subscribe(listener: NetworkStateListener): () => void;
|
|
73
|
+
/**
|
|
74
|
+
* Refresh network state manually
|
|
75
|
+
* Useful when returning from background
|
|
76
|
+
*/
|
|
77
|
+
refresh(): Promise<NetworkState>;
|
|
78
|
+
/**
|
|
79
|
+
* Cleanup and stop monitoring
|
|
80
|
+
*/
|
|
81
|
+
destroy(): void;
|
|
82
|
+
}
|
|
83
|
+
export declare const networkStatusManager: NetworkStatusManager;
|
|
84
|
+
export default networkStatusManager;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { debugLog, errorLog } from './utils';
|
|
2
|
+
/**
|
|
3
|
+
* Network status manager that detects online/offline status
|
|
4
|
+
* Uses @react-native-community/netinfo for React Native or expo-network for Expo
|
|
5
|
+
*/
|
|
6
|
+
class NetworkStatusManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.state = {
|
|
9
|
+
isConnected: true, // Default to true until we know otherwise
|
|
10
|
+
isInternetReachable: null,
|
|
11
|
+
type: 'unknown',
|
|
12
|
+
};
|
|
13
|
+
this.listeners = new Set();
|
|
14
|
+
this.unsubscribe = null;
|
|
15
|
+
this.initialized = false;
|
|
16
|
+
this.netInfoModule = null;
|
|
17
|
+
this.expoNetworkModule = null;
|
|
18
|
+
/**
|
|
19
|
+
* Poll expo-network for changes (since it doesn't have a listener API)
|
|
20
|
+
*/
|
|
21
|
+
this.pollingInterval = null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Initialize network status monitoring
|
|
25
|
+
* Call this during SDK initialization
|
|
26
|
+
*/
|
|
27
|
+
async initialize() {
|
|
28
|
+
if (this.initialized) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Try @react-native-community/netinfo first (most common in RN apps)
|
|
32
|
+
try {
|
|
33
|
+
this.netInfoModule = require('@react-native-community/netinfo').default;
|
|
34
|
+
await this.initializeWithNetInfo();
|
|
35
|
+
this.initialized = true;
|
|
36
|
+
debugLog('Network status initialized with @react-native-community/netinfo');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
catch (_a) {
|
|
40
|
+
// Module not available, try expo-network
|
|
41
|
+
}
|
|
42
|
+
// Try expo-network (for Expo apps)
|
|
43
|
+
try {
|
|
44
|
+
this.expoNetworkModule = require('expo-network');
|
|
45
|
+
await this.initializeWithExpoNetwork();
|
|
46
|
+
this.initialized = true;
|
|
47
|
+
debugLog('Network status initialized with expo-network');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
catch (_b) {
|
|
51
|
+
// Module not available
|
|
52
|
+
}
|
|
53
|
+
// Fallback: assume online (no network monitoring available)
|
|
54
|
+
debugLog('No network status module available, defaulting to online');
|
|
55
|
+
this.state = {
|
|
56
|
+
isConnected: true,
|
|
57
|
+
isInternetReachable: true,
|
|
58
|
+
type: 'unknown',
|
|
59
|
+
};
|
|
60
|
+
this.initialized = true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Initialize with @react-native-community/netinfo
|
|
64
|
+
*/
|
|
65
|
+
async initializeWithNetInfo() {
|
|
66
|
+
const NetInfo = this.netInfoModule;
|
|
67
|
+
// Get initial state
|
|
68
|
+
try {
|
|
69
|
+
const netState = await NetInfo.fetch();
|
|
70
|
+
this.updateStateFromNetInfo(netState);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
errorLog('Failed to fetch initial network state:', error);
|
|
74
|
+
}
|
|
75
|
+
// Subscribe to changes
|
|
76
|
+
this.unsubscribe = NetInfo.addEventListener((netState) => {
|
|
77
|
+
this.updateStateFromNetInfo(netState);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Update state from NetInfo response
|
|
82
|
+
*/
|
|
83
|
+
updateStateFromNetInfo(netState) {
|
|
84
|
+
var _a;
|
|
85
|
+
const previouslyConnected = this.state.isConnected;
|
|
86
|
+
this.state = {
|
|
87
|
+
isConnected: (_a = netState.isConnected) !== null && _a !== void 0 ? _a : true,
|
|
88
|
+
isInternetReachable: netState.isInternetReachable,
|
|
89
|
+
type: this.mapNetInfoType(netState.type),
|
|
90
|
+
};
|
|
91
|
+
// Notify listeners if connection status changed
|
|
92
|
+
if (previouslyConnected !== this.state.isConnected) {
|
|
93
|
+
debugLog(`Network status changed: ${this.state.isConnected ? 'online' : 'offline'} (${this.state.type})`);
|
|
94
|
+
this.notifyListeners();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Map NetInfo type to our simplified type
|
|
99
|
+
*/
|
|
100
|
+
mapNetInfoType(type) {
|
|
101
|
+
switch (type) {
|
|
102
|
+
case 'wifi':
|
|
103
|
+
return 'wifi';
|
|
104
|
+
case 'cellular':
|
|
105
|
+
return 'cellular';
|
|
106
|
+
case 'ethernet':
|
|
107
|
+
return 'ethernet';
|
|
108
|
+
case 'bluetooth':
|
|
109
|
+
return 'bluetooth';
|
|
110
|
+
case 'vpn':
|
|
111
|
+
return 'vpn';
|
|
112
|
+
case 'none':
|
|
113
|
+
return 'none';
|
|
114
|
+
default:
|
|
115
|
+
return 'unknown';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Initialize with expo-network
|
|
120
|
+
*/
|
|
121
|
+
async initializeWithExpoNetwork() {
|
|
122
|
+
const Network = this.expoNetworkModule;
|
|
123
|
+
// Get initial state
|
|
124
|
+
try {
|
|
125
|
+
const networkState = await Network.getNetworkStateAsync();
|
|
126
|
+
this.updateStateFromExpoNetwork(networkState);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
errorLog('Failed to fetch initial network state from expo-network:', error);
|
|
130
|
+
}
|
|
131
|
+
// Note: expo-network doesn't have a listener API like netinfo
|
|
132
|
+
// We'll poll periodically or rely on app state changes
|
|
133
|
+
this.startExpoNetworkPolling();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Update state from expo-network response
|
|
137
|
+
*/
|
|
138
|
+
updateStateFromExpoNetwork(networkState) {
|
|
139
|
+
var _a, _b;
|
|
140
|
+
const previouslyConnected = this.state.isConnected;
|
|
141
|
+
this.state = {
|
|
142
|
+
isConnected: (_a = networkState.isConnected) !== null && _a !== void 0 ? _a : true,
|
|
143
|
+
isInternetReachable: (_b = networkState.isInternetReachable) !== null && _b !== void 0 ? _b : null,
|
|
144
|
+
type: this.mapExpoNetworkType(networkState.type),
|
|
145
|
+
};
|
|
146
|
+
if (previouslyConnected !== this.state.isConnected) {
|
|
147
|
+
debugLog(`Network status changed: ${this.state.isConnected ? 'online' : 'offline'} (${this.state.type})`);
|
|
148
|
+
this.notifyListeners();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Map expo-network type to our simplified type
|
|
153
|
+
*/
|
|
154
|
+
mapExpoNetworkType(type) {
|
|
155
|
+
switch (type) {
|
|
156
|
+
case 'WIFI':
|
|
157
|
+
return 'wifi';
|
|
158
|
+
case 'CELLULAR':
|
|
159
|
+
return 'cellular';
|
|
160
|
+
case 'ETHERNET':
|
|
161
|
+
return 'ethernet';
|
|
162
|
+
case 'BLUETOOTH':
|
|
163
|
+
return 'bluetooth';
|
|
164
|
+
case 'VPN':
|
|
165
|
+
return 'vpn';
|
|
166
|
+
case 'NONE':
|
|
167
|
+
return 'none';
|
|
168
|
+
default:
|
|
169
|
+
return 'unknown';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
startExpoNetworkPolling() {
|
|
173
|
+
// Poll every 5 seconds for network changes
|
|
174
|
+
this.pollingInterval = setInterval(async () => {
|
|
175
|
+
try {
|
|
176
|
+
if (this.expoNetworkModule) {
|
|
177
|
+
const networkState = await this.expoNetworkModule.getNetworkStateAsync();
|
|
178
|
+
this.updateStateFromExpoNetwork(networkState);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
// Ignore polling errors
|
|
183
|
+
}
|
|
184
|
+
}, 5000);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Notify all listeners of state change
|
|
188
|
+
*/
|
|
189
|
+
notifyListeners() {
|
|
190
|
+
this.listeners.forEach((listener) => {
|
|
191
|
+
try {
|
|
192
|
+
listener(this.state);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
errorLog('Error in network state listener:', error);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get current network state
|
|
201
|
+
*/
|
|
202
|
+
getState() {
|
|
203
|
+
return { ...this.state };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if device is currently online
|
|
207
|
+
*/
|
|
208
|
+
isOnline() {
|
|
209
|
+
// Consider online if connected OR if we're not sure about internet reachability
|
|
210
|
+
return this.state.isConnected && (this.state.isInternetReachable !== false);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get current network type
|
|
214
|
+
*/
|
|
215
|
+
getNetworkType() {
|
|
216
|
+
return this.state.type;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Subscribe to network state changes
|
|
220
|
+
* Returns an unsubscribe function
|
|
221
|
+
*/
|
|
222
|
+
subscribe(listener) {
|
|
223
|
+
this.listeners.add(listener);
|
|
224
|
+
// Immediately call with current state
|
|
225
|
+
try {
|
|
226
|
+
listener(this.state);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
errorLog('Error calling network state listener:', error);
|
|
230
|
+
}
|
|
231
|
+
// Return unsubscribe function
|
|
232
|
+
return () => {
|
|
233
|
+
this.listeners.delete(listener);
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Refresh network state manually
|
|
238
|
+
* Useful when returning from background
|
|
239
|
+
*/
|
|
240
|
+
async refresh() {
|
|
241
|
+
if (this.netInfoModule) {
|
|
242
|
+
try {
|
|
243
|
+
const netState = await this.netInfoModule.fetch();
|
|
244
|
+
this.updateStateFromNetInfo(netState);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
errorLog('Failed to refresh network state:', error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (this.expoNetworkModule) {
|
|
251
|
+
try {
|
|
252
|
+
const networkState = await this.expoNetworkModule.getNetworkStateAsync();
|
|
253
|
+
this.updateStateFromExpoNetwork(networkState);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
errorLog('Failed to refresh network state from expo-network:', error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return this.state;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Cleanup and stop monitoring
|
|
263
|
+
*/
|
|
264
|
+
destroy() {
|
|
265
|
+
if (this.unsubscribe) {
|
|
266
|
+
this.unsubscribe();
|
|
267
|
+
this.unsubscribe = null;
|
|
268
|
+
}
|
|
269
|
+
if (this.pollingInterval) {
|
|
270
|
+
clearInterval(this.pollingInterval);
|
|
271
|
+
this.pollingInterval = null;
|
|
272
|
+
}
|
|
273
|
+
this.listeners.clear();
|
|
274
|
+
this.initialized = false;
|
|
275
|
+
debugLog('Network status manager destroyed');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Export singleton instance
|
|
279
|
+
export const networkStatusManager = new NetworkStatusManager();
|
|
280
|
+
// Export for direct access
|
|
281
|
+
export default networkStatusManager;
|