feat(setup): add anonymous usage-reporting opt-in to the first-run wizard
Adds a step between the SMTP/site-URL config step and the final thank-you screen, asking whether to participate in the existing Product usage & feedback reporting. Kept short — three points on how it differs from typical telemetry (one-way only, no personal data, and participants can browse the same shared feature-adoption dataset other installations report) plus an explicit consent checkbox — and calls the same enable() endpoint the post-install Settings page uses. Skipping does nothing; participation can be toggled anytime from Settings → Product usage.
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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<Record<string, string>>({});
|
||||
const [selectedFeatures, setSelectedFeatures] = useState<Set<FeatureKey>>(new Set());
|
||||
const [isSavingFeatures, setIsSavingFeatures] = useState(false);
|
||||
const [usageReportingConsent, setUsageReportingConsent] = useState(false);
|
||||
const [isEnablingUsageReporting, setIsEnablingUsageReporting] = useState(false);
|
||||
|
||||
if (statusLoading) {
|
||||
return <Loading fullScreen />;
|
||||
@@ -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,6 +335,8 @@ export const SetupPage: React.FC = () => {
|
||||
? t('setup.restoreStepSubtitle')
|
||||
: step === 'config'
|
||||
? t('setup.config.subtitle')
|
||||
: step === 'usageReporting'
|
||||
? t('setup.usageReporting.subtitle')
|
||||
: step === 'community'
|
||||
? t('setup.community.subtitle')
|
||||
: t('setup.usageSubtitle')}
|
||||
@@ -537,8 +569,62 @@ export const SetupPage: React.FC = () => {
|
||||
) : step === 'config' ? (
|
||||
<SetupConfigStep
|
||||
selectedFeatures={selectedFeatures}
|
||||
onDone={() => setStep('community')}
|
||||
onDone={() => setStep('usageReporting')}
|
||||
/>
|
||||
) : step === 'usageReporting' ? (
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-neutral-700">{t('setup.usageReporting.intro')}</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{USAGE_REPORTING_POINTS.map(({ key, icon: Icon }) => (
|
||||
<div key={key} className="flex items-start gap-3 rounded-lg border border-neutral-200 p-3">
|
||||
<Icon className="w-5 h-5 flex-shrink-0 mt-0.5" style={{ color: 'var(--color-primary, #5C8762)' }} />
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-medium text-neutral-800">
|
||||
{t(`setup.usageReporting.${key}Title`)}
|
||||
</span>
|
||||
<span className="block text-xs text-neutral-500">
|
||||
{t(`setup.usageReporting.${key}Desc`)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label className="flex items-start gap-3 rounded-lg border border-neutral-200 p-3 cursor-pointer hover:bg-neutral-50 transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-0.5 h-4 w-4 rounded border-neutral-300"
|
||||
checked={usageReportingConsent}
|
||||
onChange={(e) => setUsageReportingConsent(e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-neutral-600">{t('setup.usageReporting.consentCheck')}</span>
|
||||
</label>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
isLoading={isEnablingUsageReporting}
|
||||
disabled={!usageReportingConsent}
|
||||
onClick={enableUsageReporting}
|
||||
>
|
||||
{t('setup.usageReporting.enable')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
disabled={isEnablingUsageReporting}
|
||||
onClick={() => setStep('community')}
|
||||
>
|
||||
{t('setup.usageReporting.skip')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-neutral-700">{t('setup.community.mission')}</p>
|
||||
|
||||
Reference in New Issue
Block a user