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.
Files changed (42) hide show
  1. package/dist/commonjs/components/AppHeaderLogo.js +21 -2
  2. package/dist/commonjs/components/Inspector/InspectorHeader.js +84 -11
  3. package/dist/commonjs/components/Inspector/MainScreen.js +6 -2
  4. package/dist/commonjs/components/Inspector/NpmUpdateToast.d.ts +3 -0
  5. package/dist/commonjs/components/Inspector/NpmUpdateToast.js +288 -0
  6. package/dist/commonjs/components/Inspector/SettingsPanel.js +192 -1
  7. package/dist/commonjs/components/Inspector/TelemetryConsentModal.js +218 -71
  8. package/dist/commonjs/constants/version.d.ts +1 -1
  9. package/dist/commonjs/constants/version.js +1 -1
  10. package/dist/commonjs/helpers/telemetry.js +4 -4
  11. package/dist/commonjs/index.js +14 -2
  12. package/dist/commonjs/types/interfaces.d.ts +5 -0
  13. package/dist/esm/components/AppHeaderLogo.js +21 -2
  14. package/dist/esm/components/Inspector/InspectorHeader.js +85 -12
  15. package/dist/esm/components/Inspector/MainScreen.js +6 -2
  16. package/dist/esm/components/Inspector/NpmUpdateToast.d.ts +3 -0
  17. package/dist/esm/components/Inspector/NpmUpdateToast.js +251 -0
  18. package/dist/esm/components/Inspector/SettingsPanel.js +192 -1
  19. package/dist/esm/components/Inspector/TelemetryConsentModal.js +219 -72
  20. package/dist/esm/constants/version.d.ts +1 -1
  21. package/dist/esm/constants/version.js +1 -1
  22. package/dist/esm/helpers/telemetry.js +4 -4
  23. package/dist/esm/index.js +14 -2
  24. package/dist/esm/types/interfaces.d.ts +5 -0
  25. package/package.json +1 -1
  26. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  27. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  28. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  29. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  30. package/android/.gradle/8.9/executionHistory/executionHistory.lock +0 -0
  31. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  32. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  33. package/android/.gradle/8.9/gc.properties +0 -0
  34. package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
  35. package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
  36. package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
  37. package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
  38. package/android/.gradle/9.2.0/gc.properties +0 -0
  39. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  40. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  41. package/android/.gradle/vcs-1/gc.properties +0 -0
  42. package/android/build/reports/problems/problems-report.html +0 -659
@@ -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
- return (<View style={[logoStyles.container, { width: size, height: size }]}>
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={[logoStyles.container, { width: size, height: size }]}>
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, getBundleIdentifier, formatTime, getSize } from '../../helpers';
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={30} customIcon={appIcon}/>
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
- {/* Clean Bundle Identifier & Version Representation */}
251
+ {/* OS & NPM Version Representation */}
226
252
  <View style={{
227
253
  flexDirection: 'row',
228
254
  alignItems: 'center',
229
- gap: 4.5,
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: 3.5,
265
+ gap: 4,
240
266
  borderWidth: 1,
241
267
  borderColor: `${AppColors.white}2E`,
242
- flexShrink: 1,
243
- minWidth: 0,
268
+ flexShrink: 0,
244
269
  }}>
245
- {Platform.OS === 'ios' ? (<AppleIcon color={`${AppColors.white}D9`} size={9}/>) : (<AndroidIcon color={`${AppColors.white}D9`} size={9}/>)}
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
- flexShrink: 1,
252
- }} numberOfLines={1} ellipsizeMode="middle">
253
- {getBundleIdentifier()}
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
- {/* Anonymous Telemetry Consent Dialog */}
172
- <TelemetryConsentModal />
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,3 @@
1
+ import React from 'react';
2
+ export declare const NpmUpdateToast: () => React.JSX.Element;
3
+ export default NpmUpdateToast;
@@ -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;