diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index cff4c6c8..551fc184 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -5043,6 +5043,21 @@ "saveFailed": "Einige Änderungen konnten nicht gespeichert werden — Sie können sie unter Einstellungen → Veranstaltungsarten abschließen.", "deleteFailed": "Eine Löschung ist fehlgeschlagen — die Liste wurde neu geladen. Mitgelieferte Arten können nur hier gelöscht werden; versuchen Sie es erneut oder fahren Sie mit ihnen fort." }, + "usageReporting": { + "subtitle": "Noch eine Sache", + "intro": "Möchten Sie helfen, die Weiterentwicklung von PicPeak mitzugestalten? Die anonyme Nutzungsstatistik ist freiwillig — und funktioniert anders als das Tracking, das Sie sonst gewohnt sind abzulehnen.", + "oneWayTitle": "Anders als Google Analytics & Co.", + "oneWayDesc": "Ein reiner Einwegkanal: PicPeak sendet signierte Berichte, kann aber niemals Befehle zurückempfangen. Keine Fotos, Namen, E-Mails oder Klickverfolgung — niemals.", + "mutualTitle": "Sehen, was alle anderen nutzen", + "mutualDesc": "Als Teilnehmer sehen Sie denselben geteilten Datensatz — welche Funktionen andere Installationen wirklich verwenden, nicht nur das, was Sie selbst senden.", + "feedbackTitle": "Feedback direkt teilen", + "feedbackDesc": "Senden Sie kurzes Feedback oder Funktionswünsche direkt an den Entwickler — ganz einfach aus den Einstellungen.", + "consentCheck": "Ich bin einverstanden — jederzeit wieder abschaltbar unter Einstellungen → Produktnutzung.", + "enable": "Ja, ich möchte teilnehmen", + "skip": "Vielleicht später", + "enabled": "Danke für Ihre Teilnahme!", + "enableFailed": "Konnte gerade nicht aktiviert werden — Sie können es jederzeit unter Einstellungen → Produktnutzung erneut versuchen." + }, "community": { "subtitle": "Alles bereit", "mission": "PicPeak gibt es, damit Fotografinnen und Fotografen ihre Galerien und Kundendaten selbst besitzen — auf dem eigenen Server, ohne monatliche SaaS-Gebühren. Danke, dass Sie es ausprobieren.", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index aaa4ddc4..8f4b087d 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -4925,6 +4925,21 @@ "saveFailed": "Some event type changes could not be saved — you can finish them in Settings → Event Types.", "deleteFailed": "A deletion failed — the list has been reloaded. Built-in types can only be deleted here, so retry or continue with them kept." }, + "usageReporting": { + "subtitle": "One more thing", + "intro": "Want to help shape what PicPeak builds next? Anonymous usage reporting is optional, and it works differently from the analytics you're used to declining.", + "oneWayTitle": "Not like Google Analytics", + "oneWayDesc": "A one-way channel only: PicPeak sends signed reports but can never receive commands back. No photos, names, emails or click tracking — ever.", + "mutualTitle": "See what everyone else uses", + "mutualDesc": "As a participant, you can browse the same shared dataset — which features other installations actually use, not just what you send.", + "feedbackTitle": "Share feedback directly", + "feedbackDesc": "Send quick feedback or feature requests straight to the maintainer, right from Settings.", + "consentCheck": "I agree to participate — I can turn this off anytime in Settings → Product usage.", + "enable": "Yes, I'll participate", + "skip": "Maybe later", + "enabled": "Thanks for participating!", + "enableFailed": "Couldn't enable it just now — you can try again anytime in Settings → Product usage." + }, "community": { "subtitle": "You're all set", "mission": "PicPeak exists so photographers can own their galleries and client data — on their own server, without monthly SaaS fees. Thanks for giving it a try.", diff --git a/frontend/src/pages/SetupPage.tsx b/frontend/src/pages/SetupPage.tsx index c5c35eea..8d186d00 100644 --- a/frontend/src/pages/SetupPage.tsx +++ b/frontend/src/pages/SetupPage.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Navigate, useNavigate } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; -import { Key, Mail, Lock, Eye, EyeOff, AlertCircle, ArrowLeft, ArrowRight, Copy, Check, ExternalLink, Bug, Lightbulb, Star, Coffee } from 'lucide-react'; +import { Key, Mail, Lock, Eye, EyeOff, AlertCircle, ArrowLeft, ArrowRight, Copy, Check, ExternalLink, Bug, Lightbulb, Star, Coffee, ShieldOff, Users, MessageSquare } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; @@ -11,6 +11,7 @@ import { useAdminAuth } from '../contexts'; import { setupService } from '../services/setup.service'; import { settingsService } from '../services/settings.service'; import { featureFlagsService, type FeatureFlags, type FeatureKey } from '../services/featureFlags.service'; +import { productUsageService } from '../services/productUsage.service'; import { PicpeakRestoreCard } from '../components/admin/PicpeakBackupCard'; import { SetupConfigStep } from '../components/admin/SetupConfigStep'; import { SetupEventTypesStep } from '../components/admin/SetupEventTypesStep'; @@ -36,6 +37,17 @@ const COMMUNITY_LINKS: { { key: 'support', href: 'https://www.buymeacoffee.com/theluap', icon: Coffee }, ]; +// Anonymous usage-reporting opt-in, one step before the final thank-you +// screen. Kept deliberately short — most people reflexively decline "send us +// data" prompts, so this leads with what makes PicPeak's reporting different +// from typical analytics rather than repeating the full disclosure the +// Settings → Product usage tab already shows in detail. +const USAGE_REPORTING_POINTS: { key: string; icon: LucideIcon }[] = [ + { key: 'oneWay', icon: ShieldOff }, + { key: 'mutual', icon: Users }, + { key: 'feedback', icon: MessageSquare }, +]; + // "How will you use PicPeak?" — the opt-in feature groups shown after the admin // account is created. galleries/analytics/userManagement are always on and not // listed. Labels/descriptions reuse the existing Settings→Features i18n keys @@ -70,7 +82,7 @@ export const SetupPage: React.FC = () => { staleTime: Infinity, }); - const [step, setStep] = useState<'token' | 'account' | 'usage' | 'eventTypes' | 'restore' | 'config' | 'community'>('token'); + const [step, setStep] = useState<'token' | 'account' | 'usage' | 'eventTypes' | 'restore' | 'config' | 'usageReporting' | 'community'>('token'); const [form, setForm] = useState({ token: '', email: '', password: '', confirm: '' }); const [showPassword, setShowPassword] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); @@ -79,6 +91,8 @@ export const SetupPage: React.FC = () => { const [errors, setErrors] = useState>({}); const [selectedFeatures, setSelectedFeatures] = useState>(new Set()); const [isSavingFeatures, setIsSavingFeatures] = useState(false); + const [usageReportingConsent, setUsageReportingConsent] = useState(false); + const [isEnablingUsageReporting, setIsEnablingUsageReporting] = useState(false); if (statusLoading) { return ; @@ -275,6 +289,22 @@ export const SetupPage: React.FC = () => { setStep('config'); }; + // Best-effort, same as the feature-flag save above: a collector hiccup on a + // fresh install must not trap the admin here. They can always opt in later + // from Settings → Product usage, where the full disclosure lives. + const enableUsageReporting = async () => { + setIsEnablingUsageReporting(true); + try { + await productUsageService.enable(); + toast.success(t('setup.usageReporting.enabled')); + } catch (_) { + toast.warn(t('setup.usageReporting.enableFailed')); + } finally { + setIsEnablingUsageReporting(false); + setStep('community'); + } + }; + const stepNumber = step === 'token' ? 1 : step === 'account' ? 2 : 3; return ( @@ -305,9 +335,11 @@ export const SetupPage: React.FC = () => { ? t('setup.restoreStepSubtitle') : step === 'config' ? t('setup.config.subtitle') - : step === 'community' - ? t('setup.community.subtitle') - : t('setup.usageSubtitle')} + : step === 'usageReporting' + ? t('setup.usageReporting.subtitle') + : step === 'community' + ? t('setup.community.subtitle') + : t('setup.usageSubtitle')}

{(step === 'token' || step === 'account' || step === 'usage') && (

@@ -537,8 +569,62 @@ export const SetupPage: React.FC = () => { ) : step === 'config' ? ( setStep('community')} + onDone={() => setStep('usageReporting')} /> + ) : step === 'usageReporting' ? ( +

+

{t('setup.usageReporting.intro')}

+ +
+ {USAGE_REPORTING_POINTS.map(({ key, icon: Icon }) => ( +
+ + + + {t(`setup.usageReporting.${key}Title`)} + + + {t(`setup.usageReporting.${key}Desc`)} + + +
+ ))} +
+ + + +
+ + +
+
) : (

{t('setup.community.mission')}