rn-backstage 1.4.2 → 1.4.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/package.json +3 -3
- package/src/Backstage.tsx +0 -227
- package/src/ThemeContext.tsx +0 -38
- package/src/bug-report.ts +0 -184
- package/src/components/BackstagePanel.tsx +0 -349
- package/src/components/BugReportComposer.tsx +0 -559
- package/src/components/FlagsTab.tsx +0 -207
- package/src/components/FloatingPill.tsx +0 -247
- package/src/components/InfoTab.tsx +0 -231
- package/src/components/JsonTreeView.tsx +0 -239
- package/src/components/LogItem.tsx +0 -153
- package/src/components/LogsTab.tsx +0 -215
- package/src/components/NetworkItem.tsx +0 -425
- package/src/components/NetworkTab.tsx +0 -239
- package/src/components/StorageTab.tsx +0 -643
- package/src/components/TabBar.tsx +0 -154
- package/src/constants.ts +0 -207
- package/src/index.ts +0 -37
- package/src/log-interceptor.ts +0 -148
- package/src/network-interceptor.ts +0 -468
- package/src/types.ts +0 -280
- package/src/utils/formatTimestamp.ts +0 -22
- package/src/utils/stringify.ts +0 -90
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rn-backstage",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "A zero-dependency developer/QA debug panel for React Native apps",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
7
7
|
"types": "lib/typescript/index.d.ts",
|
|
8
|
-
"react-native": "
|
|
8
|
+
"react-native": "lib/module/index",
|
|
9
9
|
"source": "src/index",
|
|
10
10
|
"files": [
|
|
11
|
-
"src",
|
|
12
11
|
"lib",
|
|
12
|
+
"README.md",
|
|
13
13
|
"!**/__tests__",
|
|
14
14
|
"!**/__fixtures__",
|
|
15
15
|
"!**/__mocks__",
|
package/src/Backstage.tsx
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
forwardRef,
|
|
3
|
-
useCallback,
|
|
4
|
-
useEffect,
|
|
5
|
-
useImperativeHandle,
|
|
6
|
-
useRef,
|
|
7
|
-
useState,
|
|
8
|
-
} from 'react'
|
|
9
|
-
import { StyleSheet, View } from 'react-native'
|
|
10
|
-
import { DEFAULT_MAX_LOGS, DEFAULT_MAX_NETWORK_ENTRIES } from './constants'
|
|
11
|
-
import { installInterceptor, LogBuffer, uninstallInterceptor } from './log-interceptor'
|
|
12
|
-
import {
|
|
13
|
-
installNetworkInterceptor,
|
|
14
|
-
NetworkBuffer,
|
|
15
|
-
uninstallNetworkInterceptor,
|
|
16
|
-
} from './network-interceptor'
|
|
17
|
-
import { FloatingPill } from './components/FloatingPill'
|
|
18
|
-
import { BackstagePanel } from './components/BackstagePanel'
|
|
19
|
-
import { BackstageThemeProvider } from './ThemeContext'
|
|
20
|
-
import type { BackstageProps, BackstageRef, LogEntry, NetworkEntry } from './types'
|
|
21
|
-
|
|
22
|
-
// ─── Component ───────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
export const Backstage = forwardRef<BackstageRef, BackstageProps>(
|
|
25
|
-
(
|
|
26
|
-
{
|
|
27
|
-
visible = true,
|
|
28
|
-
theme: themePreference = 'auto',
|
|
29
|
-
appVersion,
|
|
30
|
-
buildNumber,
|
|
31
|
-
bundleId,
|
|
32
|
-
deviceInfo,
|
|
33
|
-
state,
|
|
34
|
-
quickActions,
|
|
35
|
-
featureFlags,
|
|
36
|
-
onToggleFeatureFlag,
|
|
37
|
-
storageAdapter,
|
|
38
|
-
maxLogs = DEFAULT_MAX_LOGS,
|
|
39
|
-
logFilters,
|
|
40
|
-
onCopyLogs,
|
|
41
|
-
extraTabs,
|
|
42
|
-
children,
|
|
43
|
-
styles: propStyles,
|
|
44
|
-
initialX,
|
|
45
|
-
initialY,
|
|
46
|
-
pillText,
|
|
47
|
-
pillWidth,
|
|
48
|
-
pillHeight,
|
|
49
|
-
enableNetworkInspector = true,
|
|
50
|
-
maxNetworkEntries = DEFAULT_MAX_NETWORK_ENTRIES,
|
|
51
|
-
maxNetworkBodySize,
|
|
52
|
-
networkFilters,
|
|
53
|
-
autoFilterNetworkLogs = true,
|
|
54
|
-
jsonMaxDepth,
|
|
55
|
-
bugReport: bugReportConfig,
|
|
56
|
-
},
|
|
57
|
-
ref,
|
|
58
|
-
) => {
|
|
59
|
-
const [panelVisible, setPanelVisible] = useState(false)
|
|
60
|
-
const [hasError, setHasError] = useState(false)
|
|
61
|
-
const [logs, setLogs] = useState<LogEntry[]>([])
|
|
62
|
-
const [networkEntries, setNetworkEntries] = useState<NetworkEntry[]>([])
|
|
63
|
-
|
|
64
|
-
const logBuffer = useRef(new LogBuffer(maxLogs))
|
|
65
|
-
const networkBuffer = useRef(new NetworkBuffer(maxNetworkEntries))
|
|
66
|
-
|
|
67
|
-
// ── Panel controls ────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
const openPanel = useCallback(() => {
|
|
70
|
-
setPanelVisible(true)
|
|
71
|
-
// Sync logs and network entries when opening panel
|
|
72
|
-
setLogs(logBuffer.current.getAll())
|
|
73
|
-
setNetworkEntries(networkBuffer.current.getAll())
|
|
74
|
-
}, [])
|
|
75
|
-
|
|
76
|
-
const closePanel = useCallback(() => {
|
|
77
|
-
setPanelVisible(false)
|
|
78
|
-
}, [])
|
|
79
|
-
|
|
80
|
-
const clearLogs = useCallback(() => {
|
|
81
|
-
logBuffer.current.clear()
|
|
82
|
-
setLogs([])
|
|
83
|
-
setHasError(false)
|
|
84
|
-
}, [])
|
|
85
|
-
|
|
86
|
-
// ── Refresh logs ──────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
const refreshLogs = useCallback(() => {
|
|
89
|
-
setLogs(logBuffer.current.getAll())
|
|
90
|
-
}, [])
|
|
91
|
-
|
|
92
|
-
// ── Network controls ──────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
const refreshNetwork = useCallback(() => {
|
|
95
|
-
setNetworkEntries(networkBuffer.current.getAll())
|
|
96
|
-
}, [])
|
|
97
|
-
|
|
98
|
-
const clearNetwork = useCallback(() => {
|
|
99
|
-
networkBuffer.current.clear()
|
|
100
|
-
setNetworkEntries([])
|
|
101
|
-
}, [])
|
|
102
|
-
|
|
103
|
-
// ── Install console interceptor ───────────────────────────
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
const handleLogEntry = (entry: LogEntry) => {
|
|
107
|
-
logBuffer.current.push(entry)
|
|
108
|
-
|
|
109
|
-
// Track error state for the floating pill
|
|
110
|
-
if (entry.level === 'error') {
|
|
111
|
-
setHasError(true)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
installInterceptor(handleLogEntry, logFilters, {
|
|
116
|
-
autoFilterNetworkLogs: enableNetworkInspector && autoFilterNetworkLogs,
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
return () => {
|
|
120
|
-
uninstallInterceptor()
|
|
121
|
-
}
|
|
122
|
-
}, [logFilters, enableNetworkInspector, autoFilterNetworkLogs])
|
|
123
|
-
|
|
124
|
-
// ── Install network interceptor ───────────────────────────────
|
|
125
|
-
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
if (!enableNetworkInspector) return
|
|
128
|
-
|
|
129
|
-
const handleNetworkEntry = (entry: NetworkEntry) => {
|
|
130
|
-
networkBuffer.current.upsert(entry)
|
|
131
|
-
|
|
132
|
-
// Auto-refresh if panel is open
|
|
133
|
-
if (panelVisible) {
|
|
134
|
-
setNetworkEntries(networkBuffer.current.getAll())
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
installNetworkInterceptor(handleNetworkEntry, {
|
|
139
|
-
filters: networkFilters,
|
|
140
|
-
maxBodySize: maxNetworkBodySize,
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
return () => {
|
|
144
|
-
uninstallNetworkInterceptor()
|
|
145
|
-
}
|
|
146
|
-
}, [enableNetworkInspector, networkFilters, maxNetworkBodySize, panelVisible])
|
|
147
|
-
|
|
148
|
-
// ── Expose ref methods ────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
const openBugReportRef = useRef<(() => void) | null>(null)
|
|
151
|
-
|
|
152
|
-
useImperativeHandle(ref, () => ({
|
|
153
|
-
open: openPanel,
|
|
154
|
-
close: closePanel,
|
|
155
|
-
clearLogs,
|
|
156
|
-
submitBugReport: () => {
|
|
157
|
-
if (bugReportConfig) {
|
|
158
|
-
openPanel()
|
|
159
|
-
// Small delay to let panel mount first
|
|
160
|
-
setTimeout(() => openBugReportRef.current?.(), 100)
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
}))
|
|
164
|
-
|
|
165
|
-
// ── Render ────────────────────────────────────────────────
|
|
166
|
-
|
|
167
|
-
if (!visible) return null
|
|
168
|
-
|
|
169
|
-
const displayText = pillText || (appVersion ? `v${appVersion}` : 'DEV')
|
|
170
|
-
|
|
171
|
-
return (
|
|
172
|
-
<BackstageThemeProvider preference={themePreference}>
|
|
173
|
-
<View style={styles.container} pointerEvents="box-none">
|
|
174
|
-
<FloatingPill
|
|
175
|
-
text={displayText}
|
|
176
|
-
hasError={hasError}
|
|
177
|
-
onPress={openPanel}
|
|
178
|
-
initialX={initialX}
|
|
179
|
-
initialY={initialY}
|
|
180
|
-
pillWidth={pillWidth}
|
|
181
|
-
pillHeight={pillHeight}
|
|
182
|
-
styles={propStyles}
|
|
183
|
-
/>
|
|
184
|
-
<BackstagePanel
|
|
185
|
-
visible={panelVisible}
|
|
186
|
-
onClose={closePanel}
|
|
187
|
-
appVersion={appVersion}
|
|
188
|
-
buildNumber={buildNumber}
|
|
189
|
-
bundleId={bundleId}
|
|
190
|
-
deviceInfo={deviceInfo}
|
|
191
|
-
state={state}
|
|
192
|
-
quickActions={quickActions}
|
|
193
|
-
featureFlags={featureFlags}
|
|
194
|
-
onToggleFeatureFlag={onToggleFeatureFlag}
|
|
195
|
-
logs={logs}
|
|
196
|
-
onRefreshLogs={refreshLogs}
|
|
197
|
-
onCopyLogs={onCopyLogs}
|
|
198
|
-
networkEntries={networkEntries}
|
|
199
|
-
onRefreshNetwork={refreshNetwork}
|
|
200
|
-
onClearNetwork={clearNetwork}
|
|
201
|
-
onCopyNetwork={onCopyLogs}
|
|
202
|
-
extraTabs={extraTabs}
|
|
203
|
-
jsonMaxDepth={jsonMaxDepth}
|
|
204
|
-
storageAdapter={storageAdapter}
|
|
205
|
-
bugReportConfig={bugReportConfig}
|
|
206
|
-
bugReportOpenerRef={openBugReportRef}
|
|
207
|
-
styles={propStyles}
|
|
208
|
-
>
|
|
209
|
-
{children}
|
|
210
|
-
</BackstagePanel>
|
|
211
|
-
</View>
|
|
212
|
-
</BackstageThemeProvider>
|
|
213
|
-
)
|
|
214
|
-
},
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
Backstage.displayName = 'Backstage'
|
|
218
|
-
|
|
219
|
-
// ─── Styles ──────────────────────────────────────────────────────────────────
|
|
220
|
-
|
|
221
|
-
const styles = StyleSheet.create({
|
|
222
|
-
container: {
|
|
223
|
-
...StyleSheet.absoluteFillObject,
|
|
224
|
-
zIndex: 99999,
|
|
225
|
-
elevation: 99999,
|
|
226
|
-
},
|
|
227
|
-
})
|
package/src/ThemeContext.tsx
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useMemo } from 'react'
|
|
2
|
-
import { useColorScheme } from 'react-native'
|
|
3
|
-
import { DarkTheme, LightTheme } from './constants'
|
|
4
|
-
import type { BackstageTheme } from './types'
|
|
5
|
-
|
|
6
|
-
// ─── Theme Preference ────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
export type ThemePreference = 'light' | 'dark' | 'auto'
|
|
9
|
-
|
|
10
|
-
// ─── Context ─────────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
const ThemeContext = createContext<BackstageTheme>(DarkTheme)
|
|
13
|
-
|
|
14
|
-
// ─── Provider ────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
interface ThemeProviderProps {
|
|
17
|
-
preference: ThemePreference
|
|
18
|
-
children: React.ReactNode
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const BackstageThemeProvider: React.FC<ThemeProviderProps> = ({ preference, children }) => {
|
|
22
|
-
const systemScheme = useColorScheme()
|
|
23
|
-
|
|
24
|
-
const theme = useMemo(() => {
|
|
25
|
-
if (preference === 'light') return LightTheme
|
|
26
|
-
if (preference === 'dark') return DarkTheme
|
|
27
|
-
// 'auto' — follow system, fallback to dark
|
|
28
|
-
return systemScheme === 'light' ? LightTheme : DarkTheme
|
|
29
|
-
}, [preference, systemScheme])
|
|
30
|
-
|
|
31
|
-
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ─── Hook ────────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
export function useBackstageTheme(): BackstageTheme {
|
|
37
|
-
return useContext(ThemeContext)
|
|
38
|
-
}
|
package/src/bug-report.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { Platform } from 'react-native'
|
|
2
|
-
import type {
|
|
3
|
-
AppInfoItem,
|
|
4
|
-
BugReport,
|
|
5
|
-
BugReportSeverity,
|
|
6
|
-
LogEntry,
|
|
7
|
-
NetworkEntry,
|
|
8
|
-
} from './types'
|
|
9
|
-
import { LogLevel } from './types'
|
|
10
|
-
|
|
11
|
-
// ─── Report Composition ──────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
const SEVERITY_LABELS: Record<BugReportSeverity, string> = {
|
|
14
|
-
low: '🟢 Low',
|
|
15
|
-
medium: '🟡 Medium',
|
|
16
|
-
high: '🟠 High',
|
|
17
|
-
critical: '🔴 Critical',
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function formatTimestamp(ts: number): string {
|
|
21
|
-
const d = new Date(ts)
|
|
22
|
-
return d.toISOString().replace('T', ' ').substring(0, 19)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function formatLogLevel(level: LogLevel): string {
|
|
26
|
-
switch (level) {
|
|
27
|
-
case LogLevel.error:
|
|
28
|
-
return '❌ ERROR'
|
|
29
|
-
case LogLevel.warn:
|
|
30
|
-
return '⚠️ WARN'
|
|
31
|
-
case LogLevel.info:
|
|
32
|
-
return 'ℹ️ INFO'
|
|
33
|
-
case LogLevel.debug:
|
|
34
|
-
return '🔍 DEBUG'
|
|
35
|
-
default:
|
|
36
|
-
return '📝 LOG'
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function formatDeviceInfoForReport(items: AppInfoItem[]): string {
|
|
41
|
-
if (items.length === 0) return ''
|
|
42
|
-
const lines = items.map(i => `- **${i.label}**: ${i.value}`)
|
|
43
|
-
return `## 📱 Device Info\n\n${lines.join('\n')}\n`
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function formatLogsForReport(logs: LogEntry[]): string {
|
|
47
|
-
if (logs.length === 0) return ''
|
|
48
|
-
const lines = logs.map(
|
|
49
|
-
l => `\`${formatTimestamp(l.timestamp)}\` ${formatLogLevel(l.level)} ${l.message}`,
|
|
50
|
-
)
|
|
51
|
-
return `## 📋 Console Logs (${logs.length})\n\n\`\`\`\n${lines.join('\n')}\n\`\`\`\n`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function formatNetworkForReport(entries: NetworkEntry[]): string {
|
|
55
|
-
if (entries.length === 0) return ''
|
|
56
|
-
const lines = entries.map(e => {
|
|
57
|
-
const status = e.status ?? '⏳'
|
|
58
|
-
const duration = e.duration ? `${e.duration}ms` : '...'
|
|
59
|
-
return `- \`${e.method}\` ${e.url} → ${status} (${duration})`
|
|
60
|
-
})
|
|
61
|
-
return `## 🌐 Network Activity (${entries.length})\n\n${lines.join('\n')}\n`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function formatStateForReport(state: Record<string, unknown>): string {
|
|
65
|
-
try {
|
|
66
|
-
const json = JSON.stringify(state, null, 2)
|
|
67
|
-
// Truncate if too large
|
|
68
|
-
const truncated = json.length > 5000 ? json.substring(0, 5000) + '\n... (truncated)' : json
|
|
69
|
-
return `## 🌳 State Snapshot\n\n\`\`\`json\n${truncated}\n\`\`\`\n`
|
|
70
|
-
} catch {
|
|
71
|
-
return `## 🌳 State Snapshot\n\n\`[Could not serialize state]\`\n`
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function composeBugReport(report: BugReport): string {
|
|
76
|
-
const sections: string[] = []
|
|
77
|
-
|
|
78
|
-
sections.push(`# 🐛 Bug Report: ${report.title}\n`)
|
|
79
|
-
sections.push(`**Severity**: ${SEVERITY_LABELS[report.severity]}`)
|
|
80
|
-
sections.push(`**Reported**: ${formatTimestamp(report.timestamp)}`)
|
|
81
|
-
sections.push(`**Platform**: ${Platform.OS} ${Platform.Version}\n`)
|
|
82
|
-
|
|
83
|
-
if (report.description) {
|
|
84
|
-
sections.push(`## Description\n\n${report.description}\n`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (report.deviceInfo.length > 0) {
|
|
88
|
-
sections.push(formatDeviceInfoForReport(report.deviceInfo))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (report.logs.length > 0) {
|
|
92
|
-
sections.push(formatLogsForReport(report.logs))
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (report.networkEntries.length > 0) {
|
|
96
|
-
sections.push(formatNetworkForReport(report.networkEntries))
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (report.state && Object.keys(report.state).length > 0) {
|
|
100
|
-
sections.push(formatStateForReport(report.state))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (report.screenshotUri) {
|
|
104
|
-
sections.push(`## 📸 Screenshot\n\nAttached: ${report.screenshotUri}\n`)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
sections.push('---\n*Generated by rn-backstage*')
|
|
108
|
-
|
|
109
|
-
return sections.join('\n')
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─── Webhook Submission ──────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
export async function submitToWebhook(
|
|
115
|
-
webhookUrl: string,
|
|
116
|
-
report: BugReport,
|
|
117
|
-
): Promise<{ success: boolean; error?: string }> {
|
|
118
|
-
try {
|
|
119
|
-
const body: Record<string, unknown> = {
|
|
120
|
-
title: report.title,
|
|
121
|
-
description: report.description,
|
|
122
|
-
severity: report.severity,
|
|
123
|
-
platform: Platform.OS,
|
|
124
|
-
platformVersion: String(Platform.Version),
|
|
125
|
-
timestamp: report.timestamp,
|
|
126
|
-
deviceInfo: report.deviceInfo,
|
|
127
|
-
logs: report.logs.map(l => ({
|
|
128
|
-
level: l.level,
|
|
129
|
-
message: l.message,
|
|
130
|
-
timestamp: l.timestamp,
|
|
131
|
-
})),
|
|
132
|
-
networkEntries: report.networkEntries.map(e => ({
|
|
133
|
-
method: e.method,
|
|
134
|
-
url: e.url,
|
|
135
|
-
status: e.status,
|
|
136
|
-
duration: e.duration,
|
|
137
|
-
})),
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (report.state) {
|
|
141
|
-
body.state = report.state
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (report.screenshotUri) {
|
|
145
|
-
body.screenshotUri = report.screenshotUri
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const response = await fetch(webhookUrl, {
|
|
149
|
-
method: 'POST',
|
|
150
|
-
headers: { 'Content-Type': 'application/json' },
|
|
151
|
-
body: JSON.stringify(body),
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
if (!response.ok) {
|
|
155
|
-
return { success: false, error: `HTTP ${response.status}` }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return { success: true }
|
|
159
|
-
} catch (err) {
|
|
160
|
-
return {
|
|
161
|
-
success: false,
|
|
162
|
-
error: err instanceof Error ? err.message : 'Unknown error',
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ─── Device Info Builder ─────────────────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
export function buildDeviceInfo(
|
|
170
|
-
appVersion?: string,
|
|
171
|
-
buildNumber?: string,
|
|
172
|
-
bundleId?: string,
|
|
173
|
-
extraInfo: AppInfoItem[] = [],
|
|
174
|
-
): AppInfoItem[] {
|
|
175
|
-
const info: AppInfoItem[] = [
|
|
176
|
-
{ label: 'Platform', value: `${Platform.OS} ${Platform.Version}` },
|
|
177
|
-
]
|
|
178
|
-
|
|
179
|
-
if (appVersion) info.push({ label: 'App Version', value: appVersion })
|
|
180
|
-
if (buildNumber) info.push({ label: 'Build Number', value: buildNumber })
|
|
181
|
-
if (bundleId) info.push({ label: 'Bundle ID', value: bundleId })
|
|
182
|
-
|
|
183
|
-
return [...info, ...extraInfo]
|
|
184
|
-
}
|