diff --git a/app/Http/Controllers/Api/Tenant/OnboardingController.php b/app/Http/Controllers/Api/Tenant/OnboardingController.php index 41e0703..afa757d 100644 --- a/app/Http/Controllers/Api/Tenant/OnboardingController.php +++ b/app/Http/Controllers/Api/Tenant/OnboardingController.php @@ -34,6 +34,8 @@ class OnboardingController extends Controller 'admin_app_opened_at' => Arr::get($settings, 'onboarding.admin_app_opened_at'), 'primary_event_id' => Arr::get($settings, 'onboarding.primary_event_id'), 'selected_packages' => Arr::get($settings, 'onboarding.selected_packages'), + 'summary_seen_package_id' => Arr::get($settings, 'onboarding.summary_seen_package_id'), + 'summary_seen_at' => Arr::get($settings, 'onboarding.summary_seen_at'), 'dismissed_at' => Arr::get($settings, 'onboarding.dismissed_at'), 'completed_at' => Arr::get($settings, 'onboarding.completed_at'), 'branding_completed' => (bool) ($status['palette'] ?? false), @@ -86,6 +88,11 @@ class OnboardingController extends Controller Arr::set($settings, 'onboarding.invite_created_at', Carbon::now()->toIso8601String()); break; + case 'summary_seen': + Arr::set($settings, 'onboarding.summary_seen_package_id', Arr::get($meta, 'package_id')); + Arr::set($settings, 'onboarding.summary_seen_at', Carbon::now()->toIso8601String()); + break; + case 'dismissed': Arr::set($settings, 'onboarding.dismissed_at', Carbon::now()->toIso8601String()); break; diff --git a/resources/js/admin/api.ts b/resources/js/admin/api.ts index 7c49fc9..6f734dc 100644 --- a/resources/js/admin/api.ts +++ b/resources/js/admin/api.ts @@ -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; diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json index ccc2744..b4ff436 100644 --- a/resources/js/admin/i18n/locales/de/management.json +++ b/resources/js/admin/i18n/locales/de/management.json @@ -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", diff --git a/resources/js/admin/i18n/locales/de/onboarding.json b/resources/js/admin/i18n/locales/de/onboarding.json index e19ff89..a2cee34 100644 --- a/resources/js/admin/i18n/locales/de/onboarding.json +++ b/resources/js/admin/i18n/locales/de/onboarding.json @@ -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": { diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json index b43eac2..374e7f1 100644 --- a/resources/js/admin/i18n/locales/en/management.json +++ b/resources/js/admin/i18n/locales/en/management.json @@ -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", diff --git a/resources/js/admin/i18n/locales/en/onboarding.json b/resources/js/admin/i18n/locales/en/onboarding.json index 613e140..68cbeac 100644 --- a/resources/js/admin/i18n/locales/en/onboarding.json +++ b/resources/js/admin/i18n/locales/en/onboarding.json @@ -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": { diff --git a/resources/js/admin/mobile/DashboardPage.tsx b/resources/js/admin/mobile/DashboardPage.tsx index 92142e4..afee791 100644 --- a/resources/js/admin/mobile/DashboardPage.tsx +++ b/resources/js/admin/mobile/DashboardPage.tsx @@ -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(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() { ) : 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 ? ( + { + 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() { ))} {tourSheet} + {packageSummarySheet} ); } @@ -320,6 +394,7 @@ export default function MobileDashboardPage() { onOpenSettings={() => navigate(adminPath('/mobile/settings'))} /> {tourSheet} + {packageSummarySheet} ); } @@ -333,6 +408,7 @@ export default function MobileDashboardPage() { > {tourSheet} + {packageSummarySheet} ); } @@ -374,10 +450,125 @@ export default function MobileDashboardPage() { {tourSheet} + {packageSummarySheet} ); } +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 | 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 | null)?.max_photos ?? null; + const maxGuests = (limits as Record | null)?.max_guests ?? null; + const galleryDays = (limits as Record | 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 ( + + + + + + {packageName} + + + {t('mobileDashboard.packageSummary.subtitle', 'Here is a quick overview of your active package.')} + + + + + + + + + {expiresAt ? ( + + ) : null} + + + + {hasFeatures ? ( + + + {t('mobileDashboard.packageSummary.featuresTitle', 'Included features')} + + + {features?.map((feature) => ( + + + + + + {t(`mobileDashboard.packageSummary.feature.${feature}`, feature)} + + + ))} + + + ) : null} + + + + {t('mobileDashboard.packageSummary.hint', 'You can revisit billing any time to review or upgrade your package.')} + + + + + + + + + + ); +} + +function SummaryRow({ label, value }: { label: string; value: string }) { + const { textStrong, muted } = useAdminTheme(); + return ( + + + {label} + + + {value} + + + ); +} + function DeviceSetupCard({ installPrompt, pushState, devicePermissions, onOpenSettings }: DeviceSetupProps) { const { t } = useTranslation('management'); const { textStrong, muted, border, primary, accentSoft } = useAdminTheme(); diff --git a/resources/js/admin/mobile/lib/onboardingGuard.test.ts b/resources/js/admin/mobile/lib/onboardingGuard.test.ts index 2f73553..54e04b0 100644 --- a/resources/js/admin/mobile/lib/onboardingGuard.test.ts +++ b/resources/js/admin/mobile/lib/onboardingGuard.test.ts @@ -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(); diff --git a/resources/js/admin/mobile/lib/onboardingGuard.ts b/resources/js/admin/mobile/lib/onboardingGuard.ts index fea3a35..a4085ac 100644 --- a/resources/js/admin/mobile/lib/onboardingGuard.ts +++ b/resources/js/admin/mobile/lib/onboardingGuard.ts @@ -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; } diff --git a/resources/js/admin/mobile/welcome/WelcomeEventPage.tsx b/resources/js/admin/mobile/welcome/WelcomeEventPage.tsx index 706468a..f4595e8 100644 --- a/resources/js/admin/mobile/welcome/WelcomeEventPage.tsx +++ b/resources/js/admin/mobile/welcome/WelcomeEventPage.tsx @@ -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.", )} - navigate(adminPath('/mobile/events/new'))} /> + navigate(shouldGoBilling ? adminPath('/mobile/billing#packages') : adminPath('/mobile/events/new'))} + /> diff --git a/resources/js/admin/mobile/welcome/WelcomeLandingPage.tsx b/resources/js/admin/mobile/welcome/WelcomeLandingPage.tsx index d71934c..c7bc6cd 100644 --- a/resources/js/admin/mobile/welcome/WelcomeLandingPage.tsx +++ b/resources/js/admin/mobile/welcome/WelcomeLandingPage.tsx @@ -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() { navigate(hasActivePackage ? ADMIN_WELCOME_EVENT_PATH : ADMIN_WELCOME_PACKAGES_PATH)} + onPress={() => navigate(shouldGoBilling ? adminPath('/mobile/billing#packages') : ADMIN_WELCOME_EVENT_PATH)} fullWidth={false} /> {hasEvents ? ( diff --git a/resources/js/admin/router.tsx b/resources/js/admin/router.tsx index 28e1b2a..c039618 100644 --- a/resources/js/admin/router.tsx +++ b/resources/js/admin/router.tsx @@ -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, diff --git a/tests/Feature/Api/Tenant/OnboardingStatusTest.php b/tests/Feature/Api/Tenant/OnboardingStatusTest.php index 7956b9e..08cf4d3 100644 --- a/tests/Feature/Api/Tenant/OnboardingStatusTest.php +++ b/tests/Feature/Api/Tenant/OnboardingStatusTest.php @@ -47,4 +47,30 @@ class OnboardingStatusTest extends TenantTestCase $show->assertOk(); $show->assertJsonPath('steps.completed_at', $completedAt); } + + public function test_tenant_can_mark_summary_seen(): void + { + $response = $this->authenticatedRequest('POST', '/api/v1/tenant/onboarding', [ + 'step' => 'summary_seen', + 'meta' => [ + 'package_id' => 123, + ], + ]); + + $response->assertOk(); + + $this->tenant->refresh(); + + $seenPackageId = Arr::get($this->tenant->settings ?? [], 'onboarding.summary_seen_package_id'); + $seenAt = Arr::get($this->tenant->settings ?? [], 'onboarding.summary_seen_at'); + + $this->assertSame(123, $seenPackageId); + $this->assertNotNull($seenAt); + + $show = $this->authenticatedRequest('GET', '/api/v1/tenant/onboarding'); + + $show->assertOk(); + $show->assertJsonPath('steps.summary_seen_package_id', 123); + $show->assertJsonPath('steps.summary_seen_at', $seenAt); + } }