Onboarding guard/resume is now in place and respects “no package” deep links to billing.
This commit is contained in:
93
resources/js/admin/mobile/lib/onboardingGuard.test.ts
Normal file
93
resources/js/admin/mobile/lib/onboardingGuard.test.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { resolveOnboardingRedirect } from './onboardingGuard';
|
||||||
|
import {
|
||||||
|
ADMIN_WELCOME_EVENT_PATH,
|
||||||
|
ADMIN_WELCOME_PACKAGES_PATH,
|
||||||
|
ADMIN_WELCOME_SUMMARY_PATH,
|
||||||
|
} from '../../constants';
|
||||||
|
|
||||||
|
describe('resolveOnboardingRedirect', () => {
|
||||||
|
it('returns null when events exist', () => {
|
||||||
|
const result = resolveOnboardingRedirect({
|
||||||
|
hasEvents: true,
|
||||||
|
hasActivePackage: false,
|
||||||
|
selectedPackageId: null,
|
||||||
|
pathname: '/event-admin/mobile/dashboard',
|
||||||
|
isWelcomePath: false,
|
||||||
|
isBillingPath: false,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for welcome paths', () => {
|
||||||
|
const result = resolveOnboardingRedirect({
|
||||||
|
hasEvents: false,
|
||||||
|
hasActivePackage: false,
|
||||||
|
selectedPackageId: null,
|
||||||
|
pathname: ADMIN_WELCOME_PACKAGES_PATH,
|
||||||
|
isWelcomePath: true,
|
||||||
|
isBillingPath: false,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for billing paths', () => {
|
||||||
|
const result = resolveOnboardingRedirect({
|
||||||
|
hasEvents: false,
|
||||||
|
hasActivePackage: false,
|
||||||
|
selectedPackageId: null,
|
||||||
|
pathname: '/event-admin/mobile/billing',
|
||||||
|
isWelcomePath: false,
|
||||||
|
isBillingPath: true,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to event setup when package active', () => {
|
||||||
|
const result = resolveOnboardingRedirect({
|
||||||
|
hasEvents: false,
|
||||||
|
hasActivePackage: true,
|
||||||
|
selectedPackageId: null,
|
||||||
|
pathname: '/event-admin/mobile/dashboard',
|
||||||
|
isWelcomePath: false,
|
||||||
|
isBillingPath: false,
|
||||||
|
});
|
||||||
|
expect(result).toBe(ADMIN_WELCOME_EVENT_PATH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to summary when selection exists', () => {
|
||||||
|
const result = resolveOnboardingRedirect({
|
||||||
|
hasEvents: false,
|
||||||
|
hasActivePackage: false,
|
||||||
|
selectedPackageId: 5,
|
||||||
|
pathname: '/event-admin/mobile/dashboard',
|
||||||
|
isWelcomePath: false,
|
||||||
|
isBillingPath: false,
|
||||||
|
});
|
||||||
|
expect(result).toBe(ADMIN_WELCOME_SUMMARY_PATH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to packages when no selection exists', () => {
|
||||||
|
const result = resolveOnboardingRedirect({
|
||||||
|
hasEvents: false,
|
||||||
|
hasActivePackage: false,
|
||||||
|
selectedPackageId: null,
|
||||||
|
pathname: '/event-admin/mobile/dashboard',
|
||||||
|
isWelcomePath: false,
|
||||||
|
isBillingPath: false,
|
||||||
|
});
|
||||||
|
expect(result).toBe(ADMIN_WELCOME_PACKAGES_PATH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not redirect when already on target', () => {
|
||||||
|
const result = resolveOnboardingRedirect({
|
||||||
|
hasEvents: false,
|
||||||
|
hasActivePackage: false,
|
||||||
|
selectedPackageId: null,
|
||||||
|
pathname: ADMIN_WELCOME_PACKAGES_PATH,
|
||||||
|
isWelcomePath: false,
|
||||||
|
isBillingPath: false,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
44
resources/js/admin/mobile/lib/onboardingGuard.ts
Normal file
44
resources/js/admin/mobile/lib/onboardingGuard.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
ADMIN_WELCOME_EVENT_PATH,
|
||||||
|
ADMIN_WELCOME_PACKAGES_PATH,
|
||||||
|
ADMIN_WELCOME_SUMMARY_PATH,
|
||||||
|
} from '../../constants';
|
||||||
|
|
||||||
|
type OnboardingRedirectInput = {
|
||||||
|
hasEvents: boolean;
|
||||||
|
hasActivePackage: boolean;
|
||||||
|
selectedPackageId?: number | null;
|
||||||
|
pathname: string;
|
||||||
|
isWelcomePath: boolean;
|
||||||
|
isBillingPath: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveOnboardingRedirect({
|
||||||
|
hasEvents,
|
||||||
|
hasActivePackage,
|
||||||
|
selectedPackageId,
|
||||||
|
pathname,
|
||||||
|
isWelcomePath,
|
||||||
|
isBillingPath,
|
||||||
|
}: OnboardingRedirectInput): string | null {
|
||||||
|
if (hasEvents) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWelcomePath || isBillingPath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldContinueSummary = Boolean(selectedPackageId && selectedPackageId > 0);
|
||||||
|
const target = hasActivePackage
|
||||||
|
? ADMIN_WELCOME_EVENT_PATH
|
||||||
|
: shouldContinueSummary
|
||||||
|
? ADMIN_WELCOME_SUMMARY_PATH
|
||||||
|
: ADMIN_WELCOME_PACKAGES_PATH;
|
||||||
|
|
||||||
|
if (pathname === target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createBrowserRouter, Outlet, Navigate, useLocation, useParams } from 'react-router-dom';
|
import { createBrowserRouter, Outlet, Navigate, useLocation, useParams } from 'react-router-dom';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import RouteErrorElement from '@/components/RouteErrorElement';
|
import RouteErrorElement from '@/components/RouteErrorElement';
|
||||||
import { useAuth } from './auth/context';
|
import { useAuth } from './auth/context';
|
||||||
|
import { useEventContext } from './context/EventContext';
|
||||||
import {
|
import {
|
||||||
ADMIN_BASE_PATH,
|
ADMIN_BASE_PATH,
|
||||||
|
ADMIN_BILLING_PATH,
|
||||||
ADMIN_DEFAULT_AFTER_LOGIN_PATH,
|
ADMIN_DEFAULT_AFTER_LOGIN_PATH,
|
||||||
ADMIN_EVENTS_PATH,
|
ADMIN_EVENTS_PATH,
|
||||||
ADMIN_LOGIN_PATH,
|
ADMIN_LOGIN_PATH,
|
||||||
ADMIN_LOGIN_START_PATH,
|
ADMIN_LOGIN_START_PATH,
|
||||||
ADMIN_PUBLIC_LANDING_PATH,
|
ADMIN_PUBLIC_LANDING_PATH,
|
||||||
|
ADMIN_WELCOME_BASE_PATH,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import { getTenantPackagesOverview } from './api';
|
||||||
|
import { getSelectedPackageId } from './mobile/lib/onboardingSelection';
|
||||||
|
import { resolveOnboardingRedirect } from './mobile/lib/onboardingGuard';
|
||||||
const AuthCallbackPage = React.lazy(() => import('./mobile/AuthCallbackPage'));
|
const AuthCallbackPage = React.lazy(() => import('./mobile/AuthCallbackPage'));
|
||||||
const LoginStartPage = React.lazy(() => import('./mobile/LoginStartPage'));
|
const LoginStartPage = React.lazy(() => import('./mobile/LoginStartPage'));
|
||||||
const LogoutPage = React.lazy(() => import('./mobile/LogoutPage'));
|
const LogoutPage = React.lazy(() => import('./mobile/LogoutPage'));
|
||||||
@@ -42,6 +49,31 @@ const MobileWelcomeEventPage = React.lazy(() => import('./mobile/welcome/Welcome
|
|||||||
function RequireAuth() {
|
function RequireAuth() {
|
||||||
const { status } = useAuth();
|
const { status } = useAuth();
|
||||||
const location = useLocation();
|
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 shouldCheckPackages =
|
||||||
|
status === 'authenticated' && !eventsLoading && !hasEvents && !isWelcomePath && !isBillingPath;
|
||||||
|
|
||||||
|
const { data: packagesData, isLoading: packagesLoading } = useQuery({
|
||||||
|
queryKey: ['mobile', 'onboarding', 'packages-overview'],
|
||||||
|
queryFn: () => getTenantPackagesOverview({ force: true }),
|
||||||
|
enabled: shouldCheckPackages,
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasActivePackage =
|
||||||
|
Boolean(packagesData?.activePackage) || Boolean(packagesData?.packages?.some((pkg) => pkg.active));
|
||||||
|
|
||||||
|
const redirectTarget = resolveOnboardingRedirect({
|
||||||
|
hasEvents,
|
||||||
|
hasActivePackage,
|
||||||
|
selectedPackageId,
|
||||||
|
pathname: location.pathname,
|
||||||
|
isWelcomePath,
|
||||||
|
isBillingPath,
|
||||||
|
});
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
@@ -55,6 +87,18 @@ function RequireAuth() {
|
|||||||
return <Navigate to={ADMIN_LOGIN_START_PATH} state={{ from: location }} replace />;
|
return <Navigate to={ADMIN_LOGIN_START_PATH} state={{ from: location }} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isWelcomePath && !isBillingPath && (eventsLoading || packagesLoading)) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
||||||
|
Bitte warten ...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectTarget) {
|
||||||
|
return <Navigate to={redirectTarget} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Suspense fallback={<Outlet />}>
|
<React.Suspense fallback={<Outlet />}>
|
||||||
<MobileAnimatedOutlet />
|
<MobileAnimatedOutlet />
|
||||||
|
|||||||
Reference in New Issue
Block a user