feat: auto-redirect to Pro shell when proInterface is on

This commit is contained in:
Linus Rath
2026-05-21 15:47:05 +02:00
parent 9763ffa2a3
commit b75bbaa517
4 changed files with 52 additions and 19 deletions
+2
View File
@@ -6,6 +6,7 @@ import { EmbeddedBridgeProvider } from "@/components/providers/embedded-bridge-p
import { RateLimitToastProvider } from "@/components/providers/rate-limit-toast-provider";
import { TourProvider } from "@/components/tour/tour-provider";
import { ProtocolLaunchHandlerProvider } from "@/components/protocol/protocol-launch-handler-provider";
import { ProInterfaceRedirect } from "@/components/pro/pro-interface-redirect";
import { PluginDialogHost } from "@/components/plugins/plugin-dialog-host";
import { PluginConsentDialog } from "@/components/plugins/plugin-consent-dialog";
import { locales } from "@/i18n/routing";
@@ -36,6 +37,7 @@ export default async function LocaleLayout({
<EmbeddedBridgeProvider>
<TourProvider>
<ProtocolLaunchHandlerProvider>
<ProInterfaceRedirect />
{children}
<PluginDialogHost />
<PluginConsentDialog />
+8 -2
View File
@@ -9,6 +9,7 @@ import { InlineAppView } from "@/components/layout/inline-app-view";
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
import { useSettingsStore } from "@/stores/settings-store";
import { useDeviceDetection } from "@/hooks/use-media-query";
import { EmbeddedContext } from "@/hooks/use-is-embedded";
import { PaneSizeContext } from "@/hooks/use-pane-size";
@@ -128,6 +129,7 @@ export default function ProHome() {
const authLoading = useAuthStore((s) => s.isLoading);
const quota = useEmailStore((s) => s.quota);
const isPushConnected = useEmailStore((s) => s.isPushConnected);
const proInterface = useSettingsStore((s) => s.proInterface);
const tabs = useProTabStore((s) => s.tabs);
const activeMainTabId = useProTabStore((s) => s.activeTabId);
@@ -165,10 +167,14 @@ export default function ProHome() {
}, [initialCheckDone, isAuthenticated, authLoading]);
useEffect(() => {
if (initialCheckDone && (isMobile || isTablet) && typeof window !== "undefined") {
if (!initialCheckDone || typeof window === "undefined") return;
// Pro is desktop-only, and only used when the user has explicitly
// enabled it. If either precondition stops holding, hand the user back
// to the standard shell.
if (isMobile || isTablet || !proInterface) {
window.location.replace("/");
}
}, [initialCheckDone, isMobile, isTablet]);
}, [initialCheckDone, isMobile, isTablet, proInterface]);
const mainTabs = useMemo(() => tabs.filter((t) => t.paneId === 'main'), [tabs]);
const splitTabs = useMemo(() => tabs.filter((t) => t.paneId === 'split'), [tabs]);
+38
View File
@@ -0,0 +1,38 @@
"use client";
import { useEffect } from "react";
import { usePathname, useRouter } from "@/i18n/navigation";
import { useSettingsStore } from "@/stores/settings-store";
import { useIsDesktop } from "@/hooks/use-media-query";
import { useProTabStore, type ProTabKind } from "@/stores/pro-tab-store";
const STANDARD_PATH_TO_TAB: Record<string, Exclude<ProTabKind, 'compose' | 'email'>> = {
'/': 'mail',
'/calendar': 'calendar',
'/contacts': 'contacts',
'/files': 'files',
'/settings': 'settings',
};
/**
* When the Pro interface is enabled, the standard mail/calendar/contacts/
* files/settings routes are taken over by the Pro shell — the user shouldn't
* have to click "Open" in settings to land there. Mobile/tablet keeps the
* standard layout because Pro is desktop-only (see pro/page.tsx).
*/
export function ProInterfaceRedirect() {
const router = useRouter();
const pathname = usePathname();
const proInterface = useSettingsStore((s) => s.proInterface);
const isDesktop = useIsDesktop();
useEffect(() => {
if (!proInterface || !isDesktop) return;
const tabKind = STANDARD_PATH_TO_TAB[pathname];
if (!tabKind) return;
useProTabStore.getState().openTab(tabKind);
router.replace('/pro');
}, [proInterface, isDesktop, pathname, router]);
return null;
}
+4 -17
View File
@@ -1,13 +1,11 @@
"use client";
import { useTranslations } from 'next-intl';
import { Link } from '@/i18n/navigation';
import { useSettingsStore, type ToolbarPosition, type MailLayout } from '@/stores/settings-store';
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
import { cn } from '@/lib/utils';
import { usePolicyStore } from '@/stores/policy-store';
import { useAccountStore } from '@/stores/account-store';
import { useMediaQuery } from '@/hooks/use-media-query';
const MAIL_LAYOUT_PREVIEW_ROWS = [
{ sender: 'Alice', subject: 'Quarterly roadmap', preview: 'The draft is ready for review.', selected: false },
@@ -120,7 +118,6 @@ export function LayoutSettings() {
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore();
const { isSettingLocked, isSettingHidden } = usePolicyStore();
const accounts = useAccountStore(s => s.accounts);
const isDesktop = useMediaQuery('(min-width: 1024px)');
return (
<SettingsSection title={t('title')} description={t('description')}>
@@ -193,20 +190,10 @@ export function LayoutSettings() {
)}
<SettingItem label={t('pro_interface.label')} description={t('pro_interface.description')}>
<div className="flex items-center gap-3">
{proInterface && isDesktop && (
<Link
href="/pro"
className="text-sm font-medium text-primary hover:underline"
>
{t('pro_interface.open_label')}
</Link>
)}
<ToggleSwitch
checked={proInterface}
onChange={(v) => updateSetting('proInterface', v)}
/>
</div>
<ToggleSwitch
checked={proInterface}
onChange={(v) => updateSetting('proInterface', v)}
/>
</SettingItem>
</SettingsSection>
);