diff --git a/src/components/SettingsPanel.test.tsx b/src/components/SettingsPanel.test.tsx
index dcce7839..0e8ec6e1 100644
--- a/src/components/SettingsPanel.test.tsx
+++ b/src/components/SettingsPanel.test.tsx
@@ -403,4 +403,45 @@ describe('SettingsPanel', () => {
})
})
})
+
+ describe('Privacy & Telemetry section', () => {
+ it('renders crash reporting and analytics toggles', () => {
+ render(
+
+ )
+ expect(screen.getByTestId('settings-crash-reporting')).toBeInTheDocument()
+ expect(screen.getByTestId('settings-analytics')).toBeInTheDocument()
+ })
+
+ it('toggles reflect initial settings state', () => {
+ const withTelemetry: Settings = {
+ ...emptySettings,
+ telemetry_consent: true,
+ crash_reporting_enabled: true,
+ analytics_enabled: false,
+ anonymous_id: 'test-uuid',
+ }
+ render(
+
+ )
+ const crashCheckbox = screen.getByTestId('settings-crash-reporting').querySelector('input') as HTMLInputElement
+ const analyticsCheckbox = screen.getByTestId('settings-analytics').querySelector('input') as HTMLInputElement
+ expect(crashCheckbox.checked).toBe(true)
+ expect(analyticsCheckbox.checked).toBe(false)
+ })
+
+ it('saves telemetry settings when toggled and saved', () => {
+ render(
+
+ )
+ const crashCheckbox = screen.getByTestId('settings-crash-reporting').querySelector('input')!
+ fireEvent.click(crashCheckbox)
+ fireEvent.click(screen.getByTestId('settings-save'))
+
+ expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
+ crash_reporting_enabled: true,
+ analytics_enabled: false,
+ }))
+ })
+ })
})
diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx
index 552240e4..69a950ae 100644
--- a/src/components/SettingsPanel.tsx
+++ b/src/components/SettingsPanel.tsx
@@ -124,6 +124,8 @@ function SettingsPanelInner({ settings, onSave, onClose }: Omit(null)
// Auto-focus first input when settings panel opens
@@ -142,11 +144,11 @@ function SettingsPanelInner({ settings, onSave, onClose }: Omit {
onSave(buildSettings())
@@ -196,6 +198,8 @@ function SettingsPanelInner({ settings, onSave, onClose }: Omit
@@ -228,6 +232,8 @@ interface SettingsBodyProps {
onGitHubConnected: (token: string, username: string) => void
onGitHubDisconnect: () => void
pullInterval: number; setPullInterval: (v: number) => void
+ crashReporting: boolean; setCrashReporting: (v: boolean) => void
+ analytics: boolean; setAnalytics: (v: boolean) => void
}
function SettingsBody(props: SettingsBodyProps) {
@@ -285,10 +291,34 @@ function SettingsBody(props: SettingsBodyProps) {
+
+
+
Privacy & Telemetry
+
+ Anonymous data helps us fix bugs and improve Laputa. No vault content, note titles, or file paths are ever sent.
+
+
+
+ {
+ it('renders the consent dialog', () => {
+ render(
)
+ expect(screen.getByText('Help improve Laputa')).toBeDefined()
+ expect(screen.getByText(/anonymous crash reports/i)).toBeDefined()
+ })
+
+ it('calls onAccept when Allow button is clicked', () => {
+ const onAccept = vi.fn()
+ render(
)
+ fireEvent.click(screen.getByTestId('telemetry-accept'))
+ expect(onAccept).toHaveBeenCalledOnce()
+ })
+
+ it('calls onDecline when No thanks button is clicked', () => {
+ const onDecline = vi.fn()
+ render(
)
+ fireEvent.click(screen.getByTestId('telemetry-decline'))
+ expect(onDecline).toHaveBeenCalledOnce()
+ })
+
+ it('shows a details section explaining what data is shared', () => {
+ render(
)
+ expect(screen.getByText(/no vault content, note titles/i)).toBeDefined()
+ })
+})
diff --git a/src/components/TelemetryConsentDialog.tsx b/src/components/TelemetryConsentDialog.tsx
new file mode 100644
index 00000000..0a21edbb
--- /dev/null
+++ b/src/components/TelemetryConsentDialog.tsx
@@ -0,0 +1,68 @@
+import { ShieldCheck } from '@phosphor-icons/react'
+
+interface TelemetryConsentDialogProps {
+ onAccept: () => void
+ onDecline: () => void
+}
+
+export function TelemetryConsentDialog({ onAccept, onDecline }: TelemetryConsentDialogProps) {
+ return (
+
+
+
+
+
+
+ Help improve Laputa
+
+
+ Send anonymous crash reports to help us fix bugs faster.
+ No vault content, no personal data, no tracking.
+
+
+
+
+
What we collect:
+
+ - Stack traces from errors (JS & Rust)
+ - App version, OS, and architecture
+
+
What we never collect:
+
+ - No vault content, note titles, or file paths
+ - No personal data or IP addresses
+
+
+
+
+
+
+
+
+
+ You can change this anytime in Settings.
+
+
+
+ )
+}