Admin package summary sheet

This commit is contained in:
Codex Agent
2026-01-06 11:57:30 +01:00
parent eba212a056
commit a796973861
13 changed files with 320 additions and 92 deletions

View File

@@ -330,6 +330,8 @@ export type TenantOnboardingStatus = {
admin_app_opened_at?: string | null;
primary_event_id?: number | string | null;
selected_packages?: unknown;
summary_seen_package_id?: number | null;
summary_seen_at?: string | null;
dismissed_at?: string | null;
completed_at?: string | null;
branding_completed?: boolean;

View File

@@ -2061,6 +2061,23 @@
"storageUnprotected": "Nicht geschützt",
"storageAction": "Jetzt schützen"
},
"packageSummary": {
"title": "Deine Paketübersicht",
"fallbackTitle": "Paketübersicht",
"subtitle": "Hier ist dein aktuelles Paket auf einen Blick.",
"limitPhotos": "Fotos",
"limitGuests": "Gäste",
"limitDays": "Galerietage",
"remaining": "Verbleibende Events",
"purchased": "Gekauft",
"expires": "Läuft ab",
"unlimited": "Unbegrenzt",
"unknown": "Unbekannt",
"featuresTitle": "Enthaltene Features",
"hint": "Im Billing kannst du dein Paket jederzeit prüfen oder upgraden.",
"continue": "Weiter zum Event-Setup",
"dismiss": "Schließen"
},
"pickEvent": "Event auswählen",
"status": {
"published": "Live",

View File

@@ -15,7 +15,8 @@
"description": "Mit nur wenigen Schritten führst du deine Gäste durch ein magisches Fotoabenteuer inklusive Storytelling, Aufgaben und moderierter Galerie.",
"primary": {
"label": "Pakete entdecken",
"button": "Pakete entdecken"
"button": "Pakete entdecken",
"billing": "Zum Billing"
},
"secondary": {
"label": "Events anzeigen",
@@ -246,7 +247,8 @@
"cta": {
"heading": "Bereit für dein erstes Event?",
"description": "Du wechselst jetzt in den Event-Manager. Dort kannst du Tasks zuweisen, Mitglieder einladen und die Gästegalerie testen. Keine Sorge: Du kannst jederzeit zur Welcome Journey zurückkehren.",
"button": "Event erstellen"
"button": "Event erstellen",
"billing": "Zum Billing"
},
"actions": {
"back": {

View File

@@ -2065,6 +2065,23 @@
"storageUnprotected": "Not protected",
"storageAction": "Protect now"
},
"packageSummary": {
"title": "Your package summary",
"fallbackTitle": "Package summary",
"subtitle": "Here is a quick overview of your active package.",
"limitPhotos": "Photos",
"limitGuests": "Guests",
"limitDays": "Gallery days",
"remaining": "Remaining events",
"purchased": "Purchased",
"expires": "Expires",
"unlimited": "Unlimited",
"unknown": "Unknown",
"featuresTitle": "Included features",
"hint": "You can revisit billing any time to review or upgrade your package.",
"continue": "Continue to event setup",
"dismiss": "Close"
},
"pickEvent": "Select an event",
"status": {
"published": "Live",

View File

@@ -15,7 +15,8 @@
"description": "In just a few steps you guide guests through a magical photo journey complete with storytelling, tasks, and a moderated gallery.",
"primary": {
"label": "Explore packages",
"button": "Explore packages"
"button": "Explore packages",
"billing": "Open billing"
},
"secondary": {
"label": "View events",
@@ -246,7 +247,8 @@
"cta": {
"heading": "Ready for your first event?",
"description": "You're switching to the event manager. Assign tasks, invite members, and test the gallery. You can always return to the welcome journey.",
"button": "Create event"
"button": "Create event",
"billing": "Open billing"
},
"actions": {
"back": {

View File

@@ -11,7 +11,7 @@ import { MobileCard, CTAButton, KpiTile, ActionTile, PillBadge, SkeletonCard } f
import { MobileSheet } from './components/Sheet';
import { adminPath, ADMIN_WELCOME_BASE_PATH } from '../constants';
import { useEventContext } from '../context/EventContext';
import { getEventStats, EventStats, TenantEvent, getEvents } from '../api';
import { fetchOnboardingStatus, getEventStats, EventStats, TenantEvent, getEvents, getTenantPackagesOverview } from '../api';
import { formatEventDate, isBrandingAllowed, resolveEngagementMode, resolveEventDisplayName } from '../lib/events';
import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
import { useDevicePermissions } from './hooks/useDevicePermissions';
@@ -39,6 +39,8 @@ export default function MobileDashboardPage() {
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
const [tourOpen, setTourOpen] = React.useState(false);
const [tourStep, setTourStep] = React.useState(0);
const [summaryOpen, setSummaryOpen] = React.useState(false);
const [summarySeenOverride, setSummarySeenOverride] = React.useState<number | null>(null);
const onboardingTrackedRef = React.useRef(false);
const installPrompt = useInstallPrompt();
const pushState = useAdminPushSubscription();
@@ -64,6 +66,16 @@ export default function MobileDashboardPage() {
queryFn: () => getEvents({ force: true }),
staleTime: 60_000,
});
const { data: onboardingStatus, isLoading: onboardingLoading } = useQuery({
queryKey: ['mobile', 'onboarding', 'status'],
queryFn: fetchOnboardingStatus,
staleTime: 60_000,
});
const { data: packagesOverview, isLoading: packagesLoading, isError: packagesError } = useQuery({
queryKey: ['mobile', 'onboarding', 'packages-overview'],
queryFn: () => getTenantPackagesOverview({ force: true }),
staleTime: 60_000,
});
const effectiveEvents = events.length ? events : dashboardEvents?.length ? dashboardEvents : fallbackEvents;
const effectiveHasEvents = hasEvents || Boolean(dashboardEvents?.length) || fallbackEvents.length > 0;
const effectiveMultiple =
@@ -115,6 +127,41 @@ export default function MobileDashboardPage() {
setTourOpen(true);
}, [forceTour, location.pathname, navigate]);
const activePackage =
packagesOverview?.activePackage ?? packagesOverview?.packages?.find((pkg) => pkg.active) ?? null;
const remainingEvents = activePackage?.remaining_events ?? null;
const summarySeenPackageId =
summarySeenOverride ?? onboardingStatus?.steps?.summary_seen_package_id ?? null;
const hasSummaryPackage =
Boolean(activePackage?.id && activePackage.id !== summarySeenPackageId);
const shouldRedirectToBilling =
!packagesLoading &&
!packagesError &&
!effectiveHasEvents &&
(activePackage === null || (remainingEvents !== null && remainingEvents <= 0));
React.useEffect(() => {
if (packagesLoading || !shouldRedirectToBilling) {
return;
}
navigate(adminPath('/mobile/billing#packages'), { replace: true });
}, [navigate, packagesLoading, shouldRedirectToBilling]);
React.useEffect(() => {
if (packagesLoading || packagesError || onboardingLoading) {
return;
}
if (shouldRedirectToBilling) {
return;
}
if (hasSummaryPackage) {
setSummaryOpen(true);
}
}, [hasSummaryPackage, onboardingLoading, packagesLoading, shouldRedirectToBilling]);
const markTourSeen = React.useCallback(() => {
if (typeof window === 'undefined') {
return;
@@ -278,6 +325,32 @@ export default function MobileDashboardPage() {
</MobileSheet>
) : null;
const handleSummaryClose = React.useCallback(() => {
setSummaryOpen(false);
if (activePackage?.id) {
setSummarySeenOverride(activePackage.id);
void trackOnboarding('summary_seen', { package_id: activePackage.id });
}
}, [activePackage?.id]);
const packageSummarySheet = activePackage ? (
<PackageSummarySheet
open={summaryOpen}
onClose={handleSummaryClose}
onContinue={() => {
handleSummaryClose();
navigate(adminPath('/mobile/events/new'));
}}
packageName={activePackage.package_name ?? t('mobileDashboard.packageSummary.fallbackTitle', 'Package summary')}
remainingEvents={remainingEvents}
purchasedAt={activePackage.purchased_at}
expiresAt={activePackage.expires_at}
limits={activePackage.package_limits ?? null}
features={activePackage.features ?? null}
locale={locale}
/>
) : null;
React.useEffect(() => {
if (events.length || isLoading || fallbackLoading || fallbackAttempted) {
return;
@@ -306,6 +379,7 @@ export default function MobileDashboardPage() {
))}
</YStack>
{tourSheet}
{packageSummarySheet}
</MobileShell>
);
}
@@ -320,6 +394,7 @@ export default function MobileDashboardPage() {
onOpenSettings={() => navigate(adminPath('/mobile/settings'))}
/>
{tourSheet}
{packageSummarySheet}
</MobileShell>
);
}
@@ -333,6 +408,7 @@ export default function MobileDashboardPage() {
>
<EventPickerList events={effectiveEvents} locale={locale} text={text} muted={muted} border={border} />
{tourSheet}
{packageSummarySheet}
</MobileShell>
);
}
@@ -374,10 +450,125 @@ export default function MobileDashboardPage() {
<AlertsAndHints event={activeEvent} stats={stats} tasksEnabled={tasksEnabled} />
{tourSheet}
{packageSummarySheet}
</MobileShell>
);
}
function PackageSummarySheet({
open,
onClose,
onContinue,
packageName,
remainingEvents,
purchasedAt,
expiresAt,
limits,
features,
locale,
}: {
open: boolean;
onClose: () => void;
onContinue: () => void;
packageName: string;
remainingEvents: number | null | undefined;
purchasedAt: string | null | undefined;
expiresAt: string | null | undefined;
limits: Record<string, unknown> | null;
features: string[] | null;
locale: string;
}) {
const { t } = useTranslation('management');
const { textStrong, muted, border, surface, accentSoft, primary } = useAdminTheme();
const text = textStrong;
const maxPhotos = (limits as Record<string, number | null> | null)?.max_photos ?? null;
const maxGuests = (limits as Record<string, number | null> | null)?.max_guests ?? null;
const galleryDays = (limits as Record<string, number | null> | null)?.gallery_days ?? null;
const hasFeatures = Array.isArray(features) && features.length > 0;
const formatLimit = (value: number | null) =>
value === null || value === undefined
? t('mobileDashboard.packageSummary.unlimited', 'Unlimited')
: String(value);
const formatDate = (value?: string | null) => formatEventDate(value, locale) ?? t('mobileDashboard.packageSummary.unknown', 'Unknown');
return (
<MobileSheet open={open} title={t('mobileDashboard.packageSummary.title', 'Your package summary')} onClose={onClose}>
<YStack space="$3">
<MobileCard space="$2.5" borderColor={border} backgroundColor={surface}>
<YStack space="$1.5">
<Text fontSize="$sm" fontWeight="800" color={text}>
{packageName}
</Text>
<Text fontSize="$xs" color={muted}>
{t('mobileDashboard.packageSummary.subtitle', 'Here is a quick overview of your active package.')}
</Text>
</YStack>
<YStack space="$2" marginTop="$2">
<SummaryRow label={t('mobileDashboard.packageSummary.limitPhotos', 'Photos')} value={formatLimit(maxPhotos)} />
<SummaryRow label={t('mobileDashboard.packageSummary.limitGuests', 'Guests')} value={formatLimit(maxGuests)} />
<SummaryRow label={t('mobileDashboard.packageSummary.limitDays', 'Gallery days')} value={formatLimit(galleryDays)} />
<SummaryRow
label={t('mobileDashboard.packageSummary.remaining', 'Remaining events')}
value={remainingEvents === null || remainingEvents === undefined ? t('mobileDashboard.packageSummary.unlimited', 'Unlimited') : String(remainingEvents)}
/>
<SummaryRow label={t('mobileDashboard.packageSummary.purchased', 'Purchased')} value={formatDate(purchasedAt)} />
{expiresAt ? (
<SummaryRow label={t('mobileDashboard.packageSummary.expires', 'Expires')} value={formatDate(expiresAt)} />
) : null}
</YStack>
</MobileCard>
{hasFeatures ? (
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('mobileDashboard.packageSummary.featuresTitle', 'Included features')}
</Text>
<YStack space="$1.5" marginTop="$2">
{features?.map((feature) => (
<XStack key={feature} alignItems="center" space="$2">
<XStack width={24} height={24} borderRadius={8} backgroundColor={accentSoft} alignItems="center" justifyContent="center">
<Sparkles size={14} color={primary} />
</XStack>
<Text fontSize="$xs" color={text}>
{t(`mobileDashboard.packageSummary.feature.${feature}`, feature)}
</Text>
</XStack>
))}
</YStack>
</MobileCard>
) : null}
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
<Text fontSize="$xs" color={muted}>
{t('mobileDashboard.packageSummary.hint', 'You can revisit billing any time to review or upgrade your package.')}
</Text>
</MobileCard>
<XStack space="$2">
<CTAButton label={t('mobileDashboard.packageSummary.continue', 'Continue to event setup')} onPress={onContinue} fullWidth={false} />
<CTAButton label={t('mobileDashboard.packageSummary.dismiss', 'Close')} tone="ghost" onPress={onClose} fullWidth={false} />
</XStack>
</YStack>
</MobileSheet>
);
}
function SummaryRow({ label, value }: { label: string; value: string }) {
const { textStrong, muted } = useAdminTheme();
return (
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$xs" color={textStrong}>
{label}
</Text>
<Text fontSize="$xs" color={muted}>
{value}
</Text>
</XStack>
);
}
function DeviceSetupCard({ installPrompt, pushState, devicePermissions, onOpenSettings }: DeviceSetupProps) {
const { t } = useTranslation('management');
const { textStrong, muted, border, primary, accentSoft } = useAdminTheme();

View File

@@ -1,9 +1,7 @@
import { describe, expect, it } from 'vitest';
import { resolveOnboardingRedirect } from './onboardingGuard';
import {
ADMIN_WELCOME_BASE_PATH,
ADMIN_WELCOME_EVENT_PATH,
ADMIN_WELCOME_SUMMARY_PATH,
ADMIN_BILLING_PATH,
} from '../../constants';
describe('resolveOnboardingRedirect', () => {
@@ -11,23 +9,8 @@ describe('resolveOnboardingRedirect', () => {
const result = resolveOnboardingRedirect({
hasEvents: true,
hasActivePackage: false,
selectedPackageId: null,
remainingEvents: null,
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBeNull();
});
it('returns null for welcome paths', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
pathname: ADMIN_WELCOME_BASE_PATH,
isWelcomePath: true,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
@@ -39,9 +22,8 @@ describe('resolveOnboardingRedirect', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
pathname: '/event-admin/mobile/billing',
isWelcomePath: false,
remainingEvents: null,
pathname: ADMIN_BILLING_PATH,
isBillingPath: true,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
@@ -53,9 +35,8 @@ describe('resolveOnboardingRedirect', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: true,
selectedPackageId: null,
remainingEvents: 1,
pathname: '/event-admin/mobile/events/new',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
@@ -63,55 +44,51 @@ describe('resolveOnboardingRedirect', () => {
expect(result).toBeNull();
});
it('redirects to event setup when package active', () => {
it('redirects to billing when no active package', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
remainingEvents: null,
pathname: '/event-admin/mobile/dashboard',
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBe(ADMIN_BILLING_PATH);
});
it('redirects to billing when no remaining events', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: true,
selectedPackageId: null,
remainingEvents: 0,
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBe(ADMIN_WELCOME_EVENT_PATH);
expect(result).toBe(ADMIN_BILLING_PATH);
});
it('redirects to summary when selection exists', () => {
it('returns null when remaining events are available', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
selectedPackageId: 5,
hasActivePackage: true,
remainingEvents: 2,
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBe(ADMIN_WELCOME_SUMMARY_PATH);
expect(result).toBeNull();
});
it('redirects to landing when no selection exists', () => {
it('returns null when remaining events are unlimited', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
hasActivePackage: true,
remainingEvents: null,
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBe(ADMIN_WELCOME_BASE_PATH);
});
it('does not redirect when already on target', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
pathname: ADMIN_WELCOME_BASE_PATH,
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
@@ -123,9 +100,8 @@ describe('resolveOnboardingRedirect', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
remainingEvents: null,
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: true,
isOnboardingCompleted: false,
@@ -137,11 +113,9 @@ describe('resolveOnboardingRedirect', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
remainingEvents: null,
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: true,
});
expect(result).toBeNull();

View File

@@ -1,16 +1,13 @@
import {
ADMIN_EVENT_CREATE_PATH,
ADMIN_WELCOME_BASE_PATH,
ADMIN_WELCOME_EVENT_PATH,
ADMIN_WELCOME_SUMMARY_PATH,
ADMIN_BILLING_PATH,
} from '../../constants';
type OnboardingRedirectInput = {
hasEvents: boolean;
hasActivePackage: boolean;
selectedPackageId?: number | null;
remainingEvents?: number | null;
pathname: string;
isWelcomePath: boolean;
isBillingPath: boolean;
isOnboardingDismissed?: boolean;
isOnboardingCompleted?: boolean;
@@ -19,22 +16,17 @@ type OnboardingRedirectInput = {
export function resolveOnboardingRedirect({
hasEvents,
hasActivePackage,
selectedPackageId,
remainingEvents,
pathname,
isWelcomePath,
isBillingPath,
isOnboardingDismissed,
isOnboardingCompleted,
}: OnboardingRedirectInput): string | null {
if (hasEvents) {
return null;
}
if (isOnboardingDismissed || isOnboardingCompleted) {
return null;
}
if (isWelcomePath || isBillingPath) {
if (isBillingPath) {
return null;
}
@@ -42,16 +34,9 @@ export function resolveOnboardingRedirect({
return null;
}
const shouldContinueSummary = Boolean(selectedPackageId && selectedPackageId > 0);
const target = hasActivePackage
? ADMIN_WELCOME_EVENT_PATH
: shouldContinueSummary
? ADMIN_WELCOME_SUMMARY_PATH
: ADMIN_WELCOME_BASE_PATH;
if (pathname === target) {
return null;
if (!hasEvents && (!hasActivePackage || (remainingEvents !== null && remainingEvents <= 0))) {
return ADMIN_BILLING_PATH;
}
return target;
return null;
}

View File

@@ -25,6 +25,8 @@ export default function WelcomeEventPage() {
const hasActivePackage =
Boolean(overview?.activePackage) || Boolean(overview?.packages?.some((pkg) => pkg.active));
const remainingEvents = overview?.activePackage?.remaining_events ?? null;
const shouldGoBilling = !hasActivePackage || (remainingEvents !== null && remainingEvents <= 0);
const handleSkip = React.useCallback(() => {
void trackOnboarding('dismissed');
navigate(ADMIN_HOME_PATH);
@@ -87,7 +89,10 @@ export default function WelcomeEventPage() {
"You're switching to the event manager. Assign tasks, invite members, and test the gallery. You can always return to the welcome journey.",
)}
</Text>
<CTAButton label={t('eventSetup.cta.button', 'Create event')} onPress={() => navigate(adminPath('/mobile/events/new'))} />
<CTAButton
label={shouldGoBilling ? t('eventSetup.cta.billing', 'Open billing') : t('eventSetup.cta.button', 'Create event')}
onPress={() => navigate(shouldGoBilling ? adminPath('/mobile/billing#packages') : adminPath('/mobile/events/new'))}
/>
</MobileCard>
<YStack space="$2">

View File

@@ -30,6 +30,8 @@ export default function WelcomeLandingPage() {
const hasActivePackage =
Boolean(packagesData?.activePackage) || Boolean(packagesData?.packages?.some((pkg) => pkg.active));
const remainingEvents = packagesData?.activePackage?.remaining_events ?? null;
const shouldGoBilling = !hasActivePackage || (remainingEvents !== null && remainingEvents <= 0);
const handleSkip = React.useCallback(() => {
void trackOnboarding('dismissed');
navigate(ADMIN_HOME_PATH);
@@ -60,11 +62,11 @@ export default function WelcomeLandingPage() {
<XStack space="$2" flexWrap="wrap">
<CTAButton
label={
hasActivePackage
? t('ctaList.createEvent.button', 'Go to event manager')
: t('hero.primary.button', 'Explore packages')
shouldGoBilling
? t('hero.primary.billing', 'Open billing')
: t('ctaList.createEvent.button', 'Go to event manager')
}
onPress={() => navigate(hasActivePackage ? ADMIN_WELCOME_EVENT_PATH : ADMIN_WELCOME_PACKAGES_PATH)}
onPress={() => navigate(shouldGoBilling ? adminPath('/mobile/billing#packages') : ADMIN_WELCOME_EVENT_PATH)}
fullWidth={false}
/>
{hasEvents ? (

View File

@@ -15,7 +15,6 @@ import {
ADMIN_WELCOME_BASE_PATH,
} from './constants';
import { fetchOnboardingStatus, getTenantPackagesOverview } from './api';
import { getSelectedPackageId } from './mobile/lib/onboardingSelection';
import { resolveOnboardingRedirect } from './mobile/lib/onboardingGuard';
const AuthCallbackPage = React.lazy(() => import('./mobile/AuthCallbackPage'));
const LoginStartPage = React.lazy(() => import('./mobile/LoginStartPage'));
@@ -56,7 +55,6 @@ function RequireAuth() {
const { status, user } = useAuth();
const location = useLocation();
const { hasEvents, isLoading: eventsLoading } = useEventContext();
const selectedPackageId = getSelectedPackageId();
const isWelcomePath = location.pathname.startsWith(ADMIN_WELCOME_BASE_PATH);
const isBillingPath = location.pathname.startsWith(ADMIN_BILLING_PATH);
const isTenantAdmin = Boolean(user && user.role !== 'member');
@@ -79,6 +77,7 @@ function RequireAuth() {
const hasActivePackage =
Boolean(packagesData?.activePackage) || Boolean(packagesData?.packages?.some((pkg) => pkg.active));
const remainingEvents = packagesData?.activePackage?.remaining_events ?? null;
const isOnboardingDismissed = Boolean(onboardingStatus?.steps?.dismissed_at);
const isOnboardingCompleted = Boolean(onboardingStatus?.steps?.completed_at);
const shouldBlockOnboarding = shouldCheckPackages && onboardingLoading;
@@ -86,9 +85,8 @@ function RequireAuth() {
const redirectTarget = resolveOnboardingRedirect({
hasEvents,
hasActivePackage,
selectedPackageId,
remainingEvents,
pathname: location.pathname,
isWelcomePath,
isBillingPath,
isOnboardingDismissed,
isOnboardingCompleted,