react-native-inapp-inspector 2.0.2 → 2.0.3
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/dist/commonjs/components/AppHeaderLogo.js +21 -2
- package/dist/commonjs/components/Inspector/InspectorHeader.js +84 -11
- package/dist/commonjs/components/Inspector/MainScreen.js +6 -2
- package/dist/commonjs/components/Inspector/NpmUpdateToast.d.ts +3 -0
- package/dist/commonjs/components/Inspector/NpmUpdateToast.js +288 -0
- package/dist/commonjs/components/Inspector/SettingsPanel.js +192 -1
- package/dist/commonjs/components/Inspector/TelemetryConsentModal.js +218 -71
- package/dist/commonjs/constants/version.d.ts +1 -1
- package/dist/commonjs/constants/version.js +1 -1
- package/dist/commonjs/helpers/telemetry.js +4 -4
- package/dist/commonjs/index.js +14 -2
- package/dist/commonjs/types/interfaces.d.ts +5 -0
- package/dist/esm/components/AppHeaderLogo.js +21 -2
- package/dist/esm/components/Inspector/InspectorHeader.js +85 -12
- package/dist/esm/components/Inspector/MainScreen.js +6 -2
- package/dist/esm/components/Inspector/NpmUpdateToast.d.ts +3 -0
- package/dist/esm/components/Inspector/NpmUpdateToast.js +251 -0
- package/dist/esm/components/Inspector/SettingsPanel.js +192 -1
- package/dist/esm/components/Inspector/TelemetryConsentModal.js +219 -72
- package/dist/esm/constants/version.d.ts +1 -1
- package/dist/esm/constants/version.js +1 -1
- package/dist/esm/helpers/telemetry.js +4 -4
- package/dist/esm/index.js +14 -2
- package/dist/esm/types/interfaces.d.ts +5 -0
- package/package.json +1 -1
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
- package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/9.2.0/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build/reports/problems/problems-report.html +0 -659
package/dist/commonjs/index.js
CHANGED
|
@@ -68,7 +68,7 @@ const NativeInspector_2 = require("./native/NativeInspector");
|
|
|
68
68
|
const constants_1 = require("./constants");
|
|
69
69
|
// Stylesheet
|
|
70
70
|
const styles_1 = require("./styles");
|
|
71
|
-
const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigationRef, appIcon, environment, }) => {
|
|
71
|
+
const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigationRef, appIcon, environment, initialVisible, visible: controlledVisible, }) => {
|
|
72
72
|
// Set custom storage synchronously during render phase
|
|
73
73
|
(0, settingsStore_1.setCustomStorage)(storage || null);
|
|
74
74
|
const [isDark, setIsDark] = (0, react_1.useState)(false);
|
|
@@ -80,7 +80,12 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
80
80
|
const [modalHeightPercent, setModalHeightPercent] = (0, react_1.useState)(90);
|
|
81
81
|
const [modalAnimationType, setModalAnimationType] = (0, react_1.useState)('slide');
|
|
82
82
|
const [logs, setLogs] = (0, react_1.useState)([]);
|
|
83
|
-
const [visible, setVisible] = (0, react_1.useState)(false);
|
|
83
|
+
const [visible, setVisible] = (0, react_1.useState)(initialVisible ?? controlledVisible ?? false);
|
|
84
|
+
(0, react_1.useEffect)(() => {
|
|
85
|
+
if (controlledVisible !== undefined) {
|
|
86
|
+
setVisible(controlledVisible);
|
|
87
|
+
}
|
|
88
|
+
}, [controlledVisible]);
|
|
84
89
|
const [isReady, setIsReady] = (0, react_1.useState)(false);
|
|
85
90
|
const [selected, setSelected] = (0, react_1.useState)(null);
|
|
86
91
|
const [selectedLogs, setSelectedLogs] = (0, react_1.useState)(new Set());
|
|
@@ -225,6 +230,7 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
225
230
|
// #6 — tab the inspector opens on. Shown with a DEFAULT badge in Settings.
|
|
226
231
|
const [defaultTab, setDefaultTab] = (0, react_1.useState)('apis');
|
|
227
232
|
const [showDuplicateLogs, setShowDuplicateLogs] = (0, react_1.useState)(false);
|
|
233
|
+
const [showUpdateToast, setShowUpdateToast] = (0, react_1.useState)(true);
|
|
228
234
|
// Synchronize runtime background listeners with active settings
|
|
229
235
|
(0, react_1.useEffect)(() => {
|
|
230
236
|
(0, networkLogger_1.setNetworkModuleEnabled)(!!tabVisibility.apis);
|
|
@@ -284,6 +290,7 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
284
290
|
setReduxAutoRefreshState(true);
|
|
285
291
|
setReduxExpandDepth(1);
|
|
286
292
|
setShowDuplicateLogs(false);
|
|
293
|
+
setShowUpdateToast(true);
|
|
287
294
|
react_native_1.Alert.alert('Settings Reset', 'All settings have been reset to default values.');
|
|
288
295
|
};
|
|
289
296
|
// #5 — hydrate persisted settings once, then auto-save on any change.
|
|
@@ -327,6 +334,8 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
327
334
|
setReduxExpandDepth(saved.reduxExpandDepth);
|
|
328
335
|
if (saved.showDuplicateLogs != null)
|
|
329
336
|
setShowDuplicateLogs(saved.showDuplicateLogs);
|
|
337
|
+
if (saved.showUpdateToast != null)
|
|
338
|
+
setShowUpdateToast(saved.showUpdateToast);
|
|
330
339
|
if (saved.defaultTab) {
|
|
331
340
|
const dt = saved.defaultTab;
|
|
332
341
|
const vis = {
|
|
@@ -368,6 +377,7 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
368
377
|
reduxAutoRefresh,
|
|
369
378
|
reduxExpandDepth,
|
|
370
379
|
showDuplicateLogs,
|
|
380
|
+
showUpdateToast,
|
|
371
381
|
});
|
|
372
382
|
}, [
|
|
373
383
|
isDark,
|
|
@@ -1743,6 +1753,8 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
1743
1753
|
setIsDark,
|
|
1744
1754
|
showDuplicateLogs,
|
|
1745
1755
|
setShowDuplicateLogs,
|
|
1756
|
+
showUpdateToast,
|
|
1757
|
+
setShowUpdateToast,
|
|
1746
1758
|
showConsoleLevels,
|
|
1747
1759
|
setShowConsoleLevels,
|
|
1748
1760
|
resetToDefaults,
|
|
@@ -151,6 +151,7 @@ export interface PersistedSettings {
|
|
|
151
151
|
reduxAutoRefresh?: boolean;
|
|
152
152
|
reduxExpandDepth?: number;
|
|
153
153
|
showDuplicateLogs?: boolean;
|
|
154
|
+
showUpdateToast?: boolean;
|
|
154
155
|
}
|
|
155
156
|
export interface NetworkInspectorProps {
|
|
156
157
|
enabled?: boolean;
|
|
@@ -160,6 +161,8 @@ export interface NetworkInspectorProps {
|
|
|
160
161
|
navigationRef?: any;
|
|
161
162
|
appIcon?: any;
|
|
162
163
|
environment?: 'DEV' | 'UAT' | 'PrePROD' | 'PROD' | 'QA' | 'Staging' | string;
|
|
164
|
+
initialVisible?: boolean;
|
|
165
|
+
visible?: boolean;
|
|
163
166
|
}
|
|
164
167
|
export interface NavigationTrackerProps {
|
|
165
168
|
onStateChange: (state: any) => void;
|
|
@@ -301,6 +304,8 @@ export interface InspectorContextValue {
|
|
|
301
304
|
setIsDark: React.Dispatch<React.SetStateAction<boolean>>;
|
|
302
305
|
showDuplicateLogs: boolean;
|
|
303
306
|
setShowDuplicateLogs: React.Dispatch<React.SetStateAction<boolean>>;
|
|
307
|
+
showUpdateToast: boolean;
|
|
308
|
+
setShowUpdateToast: React.Dispatch<React.SetStateAction<boolean>>;
|
|
304
309
|
showConsoleLevels: {
|
|
305
310
|
info: boolean;
|
|
306
311
|
warn: boolean;
|
|
@@ -5,7 +5,23 @@ export const AppHeaderLogo = ({ size = 46, customIcon, }) => {
|
|
|
5
5
|
const [loadError, setLoadError] = useState(false);
|
|
6
6
|
// 1. If customIcon is explicitly provided, render it directly edge-to-edge
|
|
7
7
|
if (customIcon) {
|
|
8
|
-
|
|
8
|
+
if (React.isValidElement(customIcon)) {
|
|
9
|
+
const clonedIcon = React.cloneElement(customIcon, {
|
|
10
|
+
size,
|
|
11
|
+
width: size,
|
|
12
|
+
height: size,
|
|
13
|
+
});
|
|
14
|
+
return (<View style={[
|
|
15
|
+
logoStyles.container,
|
|
16
|
+
{ width: size, height: size, padding: 0, borderRadius: Math.round(size * 0.28) },
|
|
17
|
+
]}>
|
|
18
|
+
{clonedIcon}
|
|
19
|
+
</View>);
|
|
20
|
+
}
|
|
21
|
+
return (<View style={[
|
|
22
|
+
logoStyles.container,
|
|
23
|
+
{ width: size, height: size, borderRadius: Math.round(size * 0.28) },
|
|
24
|
+
]}>
|
|
9
25
|
<Image source={customIcon} style={logoStyles.image} resizeMode="cover"/>
|
|
10
26
|
</View>);
|
|
11
27
|
}
|
|
@@ -21,7 +37,10 @@ export const AppHeaderLogo = ({ size = 46, customIcon, }) => {
|
|
|
21
37
|
</View>);
|
|
22
38
|
}
|
|
23
39
|
// 3. Fallback: Inspector's Signature Owl Brand Logo
|
|
24
|
-
return (<View style={[
|
|
40
|
+
return (<View style={[
|
|
41
|
+
logoStyles.container,
|
|
42
|
+
{ width: size, height: size, padding: 0, borderRadius: Math.round(size * 0.28) },
|
|
43
|
+
]}>
|
|
25
44
|
<BrandSquareIcon size={size}/>
|
|
26
45
|
</View>);
|
|
27
46
|
};
|
|
@@ -9,10 +9,36 @@ import { AppColors } from '../../styles/AppColors';
|
|
|
9
9
|
import { AppFonts } from '../../styles/AppFonts';
|
|
10
10
|
import { METHOD_COLORS } from '../../constants';
|
|
11
11
|
import { LIB_VERSION } from '../../constants';
|
|
12
|
-
import { getStatusColor, getAppName,
|
|
12
|
+
import { getStatusColor, getAppName, formatTime, getSize } from '../../helpers';
|
|
13
|
+
import { getTelemetryConsentStatus } from '../../helpers/telemetry';
|
|
13
14
|
import { WhiteBackNavigation, TrashIcon, SettingsIcon, CloseWhite, ClockIcon, SizeIcon, AppleIcon, AndroidIcon, NpmIcon, ResetIcon, } from '../NetworkIcons';
|
|
14
15
|
const InspectorHeader = React.memo(() => {
|
|
15
|
-
const { modalHeightPercent, appIcon, selected, setSelected, selectedEvent, setSelectedEvent, selectedLog, setSelectedLog, selectedReduxSlice, setSelectedReduxSlice, selectedReduxAction, setSelectedReduxAction, reduxState, reduxLastActionMap, showHeaderInfo, setShowHeaderInfo, updateAvailable, latestNpmVersion, clearAnim, activePulseAnim, unreadPulseAnim, runClearAllWithAnimation, settingsPage, setSettingsPage, resetToDefaults, closeModal, detailTitle, activeTab, environment, selectedCrash, setSelectedCrash, } = useInspector();
|
|
16
|
+
const { modalHeightPercent, appIcon, selected, setSelected, selectedEvent, setSelectedEvent, selectedLog, setSelectedLog, selectedReduxSlice, setSelectedReduxSlice, selectedReduxAction, setSelectedReduxAction, reduxState, reduxLastActionMap, showHeaderInfo, setShowHeaderInfo, updateAvailable, latestNpmVersion, clearAnim, activePulseAnim, unreadPulseAnim, runClearAllWithAnimation, settingsPage, setSettingsPage, resetToDefaults, closeModal, detailTitle, activeTab, environment, visible, selectedCrash, setSelectedCrash, } = useInspector();
|
|
17
|
+
const [isTelemetryActive, setIsTelemetryActive] = React.useState(false);
|
|
18
|
+
const telemetryPulseAnim = React.useRef(new Animated.Value(1)).current;
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
getTelemetryConsentStatus().then(status => {
|
|
21
|
+
setIsTelemetryActive(status === 'granted');
|
|
22
|
+
});
|
|
23
|
+
}, [visible]);
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
if (!isTelemetryActive)
|
|
26
|
+
return;
|
|
27
|
+
const pulse = Animated.loop(Animated.sequence([
|
|
28
|
+
Animated.timing(telemetryPulseAnim, {
|
|
29
|
+
toValue: 2.2,
|
|
30
|
+
duration: 1500,
|
|
31
|
+
useNativeDriver: true,
|
|
32
|
+
}),
|
|
33
|
+
Animated.timing(telemetryPulseAnim, {
|
|
34
|
+
toValue: 1,
|
|
35
|
+
duration: 0,
|
|
36
|
+
useNativeDriver: true,
|
|
37
|
+
}),
|
|
38
|
+
]));
|
|
39
|
+
pulse.start();
|
|
40
|
+
return () => pulse.stop();
|
|
41
|
+
}, [isTelemetryActive, telemetryPulseAnim]);
|
|
16
42
|
const envConfig = useMemo(() => {
|
|
17
43
|
const rawEnv = (environment || (__DEV__ ? 'DEV' : 'PROD')).trim();
|
|
18
44
|
const clean = rawEnv.toUpperCase();
|
|
@@ -175,7 +201,7 @@ const InspectorHeader = React.memo(() => {
|
|
|
175
201
|
minWidth: 0,
|
|
176
202
|
marginRight: 6,
|
|
177
203
|
}}>
|
|
178
|
-
<AppHeaderLogo size={
|
|
204
|
+
<AppHeaderLogo size={46} customIcon={appIcon}/>
|
|
179
205
|
<View style={{ gap: 2.5, flex: 1, minWidth: 0 }}>
|
|
180
206
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
|
181
207
|
<Text style={[styles.headerTitle, { flexShrink: 1 }]} numberOfLines={1} ellipsizeMode="tail">
|
|
@@ -222,11 +248,11 @@ const InspectorHeader = React.memo(() => {
|
|
|
222
248
|
</Pressable>)}
|
|
223
249
|
</View>
|
|
224
250
|
|
|
225
|
-
{/*
|
|
251
|
+
{/* OS & NPM Version Representation */}
|
|
226
252
|
<View style={{
|
|
227
253
|
flexDirection: 'row',
|
|
228
254
|
alignItems: 'center',
|
|
229
|
-
gap:
|
|
255
|
+
gap: 5,
|
|
230
256
|
minWidth: 0,
|
|
231
257
|
}}>
|
|
232
258
|
<View style={{
|
|
@@ -236,21 +262,21 @@ const InspectorHeader = React.memo(() => {
|
|
|
236
262
|
borderRadius: 5,
|
|
237
263
|
paddingHorizontal: 6,
|
|
238
264
|
paddingVertical: 2,
|
|
239
|
-
gap:
|
|
265
|
+
gap: 4,
|
|
240
266
|
borderWidth: 1,
|
|
241
267
|
borderColor: `${AppColors.white}2E`,
|
|
242
|
-
flexShrink:
|
|
243
|
-
minWidth: 0,
|
|
268
|
+
flexShrink: 0,
|
|
244
269
|
}}>
|
|
245
|
-
{Platform.OS === 'ios' ? (<AppleIcon color={`${AppColors.white}
|
|
270
|
+
{Platform.OS === 'ios' ? (<AppleIcon color={`${AppColors.white}E6`} size={10}/>) : (<AndroidIcon color={`${AppColors.white}E6`} size={10}/>)}
|
|
246
271
|
<Text style={{
|
|
247
272
|
fontFamily: AppFonts.interMedium,
|
|
248
273
|
fontSize: 9.5,
|
|
249
274
|
color: `${AppColors.white}EB`,
|
|
250
275
|
letterSpacing: 0.1,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
276
|
+
}} numberOfLines={1}>
|
|
277
|
+
{Platform.OS === 'ios'
|
|
278
|
+
? `iOS ${Platform.Version}`
|
|
279
|
+
: `Android ${Platform.Version}`}
|
|
254
280
|
</Text>
|
|
255
281
|
</View>
|
|
256
282
|
|
|
@@ -279,6 +305,53 @@ const InspectorHeader = React.memo(() => {
|
|
|
279
305
|
v{LIB_VERSION}
|
|
280
306
|
</Text>
|
|
281
307
|
</Pressable>
|
|
308
|
+
|
|
309
|
+
{isTelemetryActive && (<Pressable onPress={() => Alert.alert('Anonymous Telemetry Active', 'Anonymous diagnostics (such as React Native version, JavaScript engine, and device model) are currently enabled to help improve library tooling.\n\nNo user data or network payloads are captured. You can toggle this anytime in Settings.', [{ text: 'Got it' }])} style={{
|
|
310
|
+
flexDirection: 'row',
|
|
311
|
+
alignItems: 'center',
|
|
312
|
+
backgroundColor: 'rgba(16, 185, 129, 0.22)',
|
|
313
|
+
borderRadius: 5,
|
|
314
|
+
paddingHorizontal: 5.5,
|
|
315
|
+
paddingVertical: 2,
|
|
316
|
+
gap: 4.5,
|
|
317
|
+
borderWidth: 1,
|
|
318
|
+
borderColor: 'rgba(52, 211, 153, 0.45)',
|
|
319
|
+
flexShrink: 0,
|
|
320
|
+
}}>
|
|
321
|
+
<View style={{
|
|
322
|
+
width: 6,
|
|
323
|
+
height: 6,
|
|
324
|
+
alignItems: 'center',
|
|
325
|
+
justifyContent: 'center',
|
|
326
|
+
}}>
|
|
327
|
+
<Animated.View style={{
|
|
328
|
+
position: 'absolute',
|
|
329
|
+
width: 6,
|
|
330
|
+
height: 6,
|
|
331
|
+
borderRadius: 3,
|
|
332
|
+
backgroundColor: '#34D399',
|
|
333
|
+
opacity: telemetryPulseAnim.interpolate({
|
|
334
|
+
inputRange: [1, 2.2],
|
|
335
|
+
outputRange: [0.8, 0],
|
|
336
|
+
}),
|
|
337
|
+
transform: [{ scale: telemetryPulseAnim }],
|
|
338
|
+
}}/>
|
|
339
|
+
<View style={{
|
|
340
|
+
width: 4,
|
|
341
|
+
height: 4,
|
|
342
|
+
borderRadius: 2,
|
|
343
|
+
backgroundColor: '#10B981',
|
|
344
|
+
}}/>
|
|
345
|
+
</View>
|
|
346
|
+
<Text style={{
|
|
347
|
+
fontFamily: AppFonts.interBold,
|
|
348
|
+
fontSize: 9,
|
|
349
|
+
color: '#6EE7B7',
|
|
350
|
+
letterSpacing: 0.2,
|
|
351
|
+
}}>
|
|
352
|
+
LIVE
|
|
353
|
+
</Text>
|
|
354
|
+
</Pressable>)}
|
|
282
355
|
</View>
|
|
283
356
|
</View>
|
|
284
357
|
</View>) : null}
|
|
@@ -19,6 +19,7 @@ import CrashTab from './CrashTab';
|
|
|
19
19
|
import CrashDetail from './CrashDetail';
|
|
20
20
|
import SettingsPanel from './SettingsPanel';
|
|
21
21
|
import TelemetryConsentModal from './TelemetryConsentModal';
|
|
22
|
+
import NpmUpdateToast from './NpmUpdateToast';
|
|
22
23
|
import Toast from '../Toast';
|
|
23
24
|
import styles from '../../styles';
|
|
24
25
|
import { AppColors } from '../../styles/AppColors';
|
|
@@ -168,13 +169,16 @@ const MainScreen = () => {
|
|
|
168
169
|
{/* Bottom floating toast notification */}
|
|
169
170
|
<Toast />
|
|
170
171
|
|
|
171
|
-
{/*
|
|
172
|
-
<
|
|
172
|
+
{/* NPM Version Update Toast with timeout progress bar */}
|
|
173
|
+
<NpmUpdateToast />
|
|
173
174
|
</View>
|
|
174
175
|
</View>
|
|
175
176
|
</ErrorBoundary>)}
|
|
176
177
|
{hasNavigationContext && (<NavigationTracker onStateChange={setNavState}/>)}
|
|
177
178
|
</Modal>
|
|
179
|
+
|
|
180
|
+
{/* Anonymous Telemetry Consent Dialog at root viewport */}
|
|
181
|
+
<TelemetryConsentModal />
|
|
178
182
|
</>);
|
|
179
183
|
};
|
|
180
184
|
export default MainScreen;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, Linking, Platform, StyleSheet, Text, TouchableOpacity, View, } from 'react-native';
|
|
3
|
+
import Svg, { Path } from 'react-native-svg';
|
|
4
|
+
import { useInspector } from './InspectorContext';
|
|
5
|
+
import { LIB_VERSION } from '../../constants';
|
|
6
|
+
import { NpmIcon } from '../NetworkIcons';
|
|
7
|
+
const CloseSvg = ({ size = 13, color = '#94A3B8' }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
8
|
+
<Path d="M18 6L6 18M6 6l12 12" stroke={color} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/>
|
|
9
|
+
</Svg>);
|
|
10
|
+
const ExternalLinkSvg = ({ size = 12, color = '#FFFFFF' }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
11
|
+
<Path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
12
|
+
<Path d="M15 3h6v6" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
13
|
+
<Path d="M10 14L21 3" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
14
|
+
</Svg>);
|
|
15
|
+
const TOAST_TIMEOUT_MS = 6500;
|
|
16
|
+
export const NpmUpdateToast = () => {
|
|
17
|
+
const { updateAvailable, latestNpmVersion, showUpdateToast } = useInspector();
|
|
18
|
+
const [dismissed, setDismissed] = useState(false);
|
|
19
|
+
const [visible, setVisible] = useState(false);
|
|
20
|
+
const translateYAnim = useRef(new Animated.Value(50)).current;
|
|
21
|
+
const opacityAnim = useRef(new Animated.Value(0)).current;
|
|
22
|
+
const progressAnim = useRef(new Animated.Value(1)).current;
|
|
23
|
+
const timerRef = useRef(null);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (showUpdateToast && updateAvailable && latestNpmVersion && !dismissed) {
|
|
26
|
+
setVisible(true);
|
|
27
|
+
// Slide and fade in
|
|
28
|
+
Animated.parallel([
|
|
29
|
+
Animated.spring(translateYAnim, {
|
|
30
|
+
toValue: 0,
|
|
31
|
+
friction: 8,
|
|
32
|
+
tension: 40,
|
|
33
|
+
useNativeDriver: true,
|
|
34
|
+
}),
|
|
35
|
+
Animated.timing(opacityAnim, {
|
|
36
|
+
toValue: 1,
|
|
37
|
+
duration: 250,
|
|
38
|
+
useNativeDriver: true,
|
|
39
|
+
}),
|
|
40
|
+
]).start();
|
|
41
|
+
// Progress bar countdown from 1 to 0
|
|
42
|
+
progressAnim.setValue(1);
|
|
43
|
+
Animated.timing(progressAnim, {
|
|
44
|
+
toValue: 0,
|
|
45
|
+
duration: TOAST_TIMEOUT_MS,
|
|
46
|
+
useNativeDriver: false,
|
|
47
|
+
}).start();
|
|
48
|
+
// Auto-hide after timeout
|
|
49
|
+
timerRef.current = setTimeout(() => {
|
|
50
|
+
handleDismiss();
|
|
51
|
+
}, TOAST_TIMEOUT_MS);
|
|
52
|
+
}
|
|
53
|
+
return () => {
|
|
54
|
+
if (timerRef.current) {
|
|
55
|
+
clearTimeout(timerRef.current);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}, [showUpdateToast, updateAvailable, latestNpmVersion, dismissed]);
|
|
59
|
+
const handleDismiss = () => {
|
|
60
|
+
Animated.parallel([
|
|
61
|
+
Animated.timing(opacityAnim, {
|
|
62
|
+
toValue: 0,
|
|
63
|
+
duration: 200,
|
|
64
|
+
useNativeDriver: true,
|
|
65
|
+
}),
|
|
66
|
+
Animated.timing(translateYAnim, {
|
|
67
|
+
toValue: 50,
|
|
68
|
+
duration: 200,
|
|
69
|
+
useNativeDriver: true,
|
|
70
|
+
}),
|
|
71
|
+
]).start(() => {
|
|
72
|
+
setVisible(false);
|
|
73
|
+
setDismissed(true);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const handleOpenNpm = () => {
|
|
77
|
+
Linking.openURL('https://www.npmjs.com/package/react-native-inapp-inspector').catch(() => { });
|
|
78
|
+
handleDismiss();
|
|
79
|
+
};
|
|
80
|
+
if (!showUpdateToast || !visible || !updateAvailable || !latestNpmVersion)
|
|
81
|
+
return null;
|
|
82
|
+
const progressWidth = progressAnim.interpolate({
|
|
83
|
+
inputRange: [0, 1],
|
|
84
|
+
outputRange: ['0%', '100%'],
|
|
85
|
+
});
|
|
86
|
+
return (<Animated.View style={[
|
|
87
|
+
styles.container,
|
|
88
|
+
{
|
|
89
|
+
opacity: opacityAnim,
|
|
90
|
+
transform: [{ translateY: translateYAnim }],
|
|
91
|
+
},
|
|
92
|
+
]}>
|
|
93
|
+
<View style={styles.contentRow}>
|
|
94
|
+
{/* Left NPM Badge Icon */}
|
|
95
|
+
<View style={styles.npmIconContainer}>
|
|
96
|
+
<NpmIcon size={14} color="#CB3837"/>
|
|
97
|
+
</View>
|
|
98
|
+
|
|
99
|
+
{/* Text Details */}
|
|
100
|
+
<View style={styles.textContainer}>
|
|
101
|
+
<View style={styles.titleRow}>
|
|
102
|
+
<Text style={styles.titleText}>Update Available</Text>
|
|
103
|
+
<View style={styles.versionPill}>
|
|
104
|
+
<Text style={styles.versionPillText}>v{latestNpmVersion}</Text>
|
|
105
|
+
</View>
|
|
106
|
+
</View>
|
|
107
|
+
<Text style={styles.subtitleText} numberOfLines={1}>
|
|
108
|
+
Installed: v{LIB_VERSION} • New version is ready
|
|
109
|
+
</Text>
|
|
110
|
+
</View>
|
|
111
|
+
|
|
112
|
+
{/* Action Button: View */}
|
|
113
|
+
<TouchableOpacity style={styles.actionButton} onPress={handleOpenNpm} activeOpacity={0.8}>
|
|
114
|
+
<Text style={styles.actionButtonText}>View</Text>
|
|
115
|
+
<ExternalLinkSvg size={10} color="#FFFFFF"/>
|
|
116
|
+
</TouchableOpacity>
|
|
117
|
+
|
|
118
|
+
{/* Close Button */}
|
|
119
|
+
<TouchableOpacity style={styles.closeButton} onPress={handleDismiss} hitSlop={8} activeOpacity={0.7}>
|
|
120
|
+
<CloseSvg size={12} color="#94A3B8"/>
|
|
121
|
+
</TouchableOpacity>
|
|
122
|
+
</View>
|
|
123
|
+
|
|
124
|
+
{/* Animated Linear Timeout Progress Bar */}
|
|
125
|
+
<View style={styles.progressBarTrack}>
|
|
126
|
+
<Animated.View style={[styles.progressBarFill, { width: progressWidth }]}/>
|
|
127
|
+
</View>
|
|
128
|
+
</Animated.View>);
|
|
129
|
+
};
|
|
130
|
+
const fontStack = Platform.select({
|
|
131
|
+
ios: {
|
|
132
|
+
fontFamily: 'System',
|
|
133
|
+
},
|
|
134
|
+
android: {
|
|
135
|
+
fontFamily: 'sans-serif',
|
|
136
|
+
},
|
|
137
|
+
default: {},
|
|
138
|
+
});
|
|
139
|
+
const styles = StyleSheet.create({
|
|
140
|
+
container: {
|
|
141
|
+
position: 'absolute',
|
|
142
|
+
bottom: 24,
|
|
143
|
+
left: 16,
|
|
144
|
+
right: 16,
|
|
145
|
+
backgroundColor: '#0F172A',
|
|
146
|
+
borderRadius: 16,
|
|
147
|
+
borderWidth: 1,
|
|
148
|
+
borderColor: '#334155',
|
|
149
|
+
shadowColor: '#000000',
|
|
150
|
+
shadowOffset: { width: 0, height: 10 },
|
|
151
|
+
shadowOpacity: 0.35,
|
|
152
|
+
shadowRadius: 20,
|
|
153
|
+
elevation: 20,
|
|
154
|
+
overflow: 'hidden',
|
|
155
|
+
zIndex: 99999,
|
|
156
|
+
},
|
|
157
|
+
contentRow: {
|
|
158
|
+
flexDirection: 'row',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
paddingVertical: 12,
|
|
161
|
+
paddingHorizontal: 14,
|
|
162
|
+
gap: 10,
|
|
163
|
+
},
|
|
164
|
+
npmIconContainer: {
|
|
165
|
+
width: 32,
|
|
166
|
+
height: 32,
|
|
167
|
+
borderRadius: 10,
|
|
168
|
+
backgroundColor: '#FFFFFF',
|
|
169
|
+
justifyContent: 'center',
|
|
170
|
+
alignItems: 'center',
|
|
171
|
+
borderWidth: 1,
|
|
172
|
+
borderColor: '#CBD5E1',
|
|
173
|
+
flexShrink: 0,
|
|
174
|
+
},
|
|
175
|
+
textContainer: {
|
|
176
|
+
flex: 1,
|
|
177
|
+
minWidth: 0,
|
|
178
|
+
},
|
|
179
|
+
titleRow: {
|
|
180
|
+
flexDirection: 'row',
|
|
181
|
+
alignItems: 'center',
|
|
182
|
+
gap: 6,
|
|
183
|
+
},
|
|
184
|
+
titleText: {
|
|
185
|
+
fontSize: 13,
|
|
186
|
+
fontWeight: '700',
|
|
187
|
+
color: '#F8FAFC',
|
|
188
|
+
letterSpacing: -0.2,
|
|
189
|
+
...fontStack,
|
|
190
|
+
},
|
|
191
|
+
versionPill: {
|
|
192
|
+
backgroundColor: '#7C3AED26',
|
|
193
|
+
paddingHorizontal: 6,
|
|
194
|
+
paddingVertical: 1.5,
|
|
195
|
+
borderRadius: 6,
|
|
196
|
+
borderWidth: 1,
|
|
197
|
+
borderColor: '#7C3AED59',
|
|
198
|
+
},
|
|
199
|
+
versionPillText: {
|
|
200
|
+
fontSize: 10.5,
|
|
201
|
+
fontWeight: '700',
|
|
202
|
+
color: '#A78BFA',
|
|
203
|
+
...fontStack,
|
|
204
|
+
},
|
|
205
|
+
subtitleText: {
|
|
206
|
+
fontSize: 11,
|
|
207
|
+
color: '#94A3B8',
|
|
208
|
+
marginTop: 1.5,
|
|
209
|
+
...fontStack,
|
|
210
|
+
},
|
|
211
|
+
actionButton: {
|
|
212
|
+
flexDirection: 'row',
|
|
213
|
+
alignItems: 'center',
|
|
214
|
+
gap: 4,
|
|
215
|
+
backgroundColor: '#7C3AED',
|
|
216
|
+
paddingVertical: 6,
|
|
217
|
+
paddingHorizontal: 10,
|
|
218
|
+
borderRadius: 8,
|
|
219
|
+
flexShrink: 0,
|
|
220
|
+
shadowColor: '#7C3AED',
|
|
221
|
+
shadowOffset: { width: 0, height: 2 },
|
|
222
|
+
shadowOpacity: 0.3,
|
|
223
|
+
shadowRadius: 4,
|
|
224
|
+
elevation: 3,
|
|
225
|
+
},
|
|
226
|
+
actionButtonText: {
|
|
227
|
+
fontSize: 11.5,
|
|
228
|
+
fontWeight: '700',
|
|
229
|
+
color: '#FFFFFF',
|
|
230
|
+
...fontStack,
|
|
231
|
+
},
|
|
232
|
+
closeButton: {
|
|
233
|
+
width: 24,
|
|
234
|
+
height: 24,
|
|
235
|
+
borderRadius: 12,
|
|
236
|
+
backgroundColor: '#1E293B',
|
|
237
|
+
justifyContent: 'center',
|
|
238
|
+
alignItems: 'center',
|
|
239
|
+
flexShrink: 0,
|
|
240
|
+
},
|
|
241
|
+
progressBarTrack: {
|
|
242
|
+
height: 3,
|
|
243
|
+
width: '100%',
|
|
244
|
+
backgroundColor: '#1E293B',
|
|
245
|
+
},
|
|
246
|
+
progressBarFill: {
|
|
247
|
+
height: '100%',
|
|
248
|
+
backgroundColor: '#7C3AED',
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
export default NpmUpdateToast;
|