feat: auto-redirect to Pro shell when proInterface is on
This commit is contained in:
@@ -6,6 +6,7 @@ import { EmbeddedBridgeProvider } from "@/components/providers/embedded-bridge-p
|
|||||||
import { RateLimitToastProvider } from "@/components/providers/rate-limit-toast-provider";
|
import { RateLimitToastProvider } from "@/components/providers/rate-limit-toast-provider";
|
||||||
import { TourProvider } from "@/components/tour/tour-provider";
|
import { TourProvider } from "@/components/tour/tour-provider";
|
||||||
import { ProtocolLaunchHandlerProvider } from "@/components/protocol/protocol-launch-handler-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 { PluginDialogHost } from "@/components/plugins/plugin-dialog-host";
|
||||||
import { PluginConsentDialog } from "@/components/plugins/plugin-consent-dialog";
|
import { PluginConsentDialog } from "@/components/plugins/plugin-consent-dialog";
|
||||||
import { locales } from "@/i18n/routing";
|
import { locales } from "@/i18n/routing";
|
||||||
@@ -36,6 +37,7 @@ export default async function LocaleLayout({
|
|||||||
<EmbeddedBridgeProvider>
|
<EmbeddedBridgeProvider>
|
||||||
<TourProvider>
|
<TourProvider>
|
||||||
<ProtocolLaunchHandlerProvider>
|
<ProtocolLaunchHandlerProvider>
|
||||||
|
<ProInterfaceRedirect />
|
||||||
{children}
|
{children}
|
||||||
<PluginDialogHost />
|
<PluginDialogHost />
|
||||||
<PluginConsentDialog />
|
<PluginConsentDialog />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { InlineAppView } from "@/components/layout/inline-app-view";
|
|||||||
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
|
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
|
||||||
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
|
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
|
||||||
import { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore } from "@/stores/email-store";
|
||||||
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
import { useDeviceDetection } from "@/hooks/use-media-query";
|
import { useDeviceDetection } from "@/hooks/use-media-query";
|
||||||
import { EmbeddedContext } from "@/hooks/use-is-embedded";
|
import { EmbeddedContext } from "@/hooks/use-is-embedded";
|
||||||
import { PaneSizeContext } from "@/hooks/use-pane-size";
|
import { PaneSizeContext } from "@/hooks/use-pane-size";
|
||||||
@@ -128,6 +129,7 @@ export default function ProHome() {
|
|||||||
const authLoading = useAuthStore((s) => s.isLoading);
|
const authLoading = useAuthStore((s) => s.isLoading);
|
||||||
const quota = useEmailStore((s) => s.quota);
|
const quota = useEmailStore((s) => s.quota);
|
||||||
const isPushConnected = useEmailStore((s) => s.isPushConnected);
|
const isPushConnected = useEmailStore((s) => s.isPushConnected);
|
||||||
|
const proInterface = useSettingsStore((s) => s.proInterface);
|
||||||
|
|
||||||
const tabs = useProTabStore((s) => s.tabs);
|
const tabs = useProTabStore((s) => s.tabs);
|
||||||
const activeMainTabId = useProTabStore((s) => s.activeTabId);
|
const activeMainTabId = useProTabStore((s) => s.activeTabId);
|
||||||
@@ -165,10 +167,14 @@ export default function ProHome() {
|
|||||||
}, [initialCheckDone, isAuthenticated, authLoading]);
|
}, [initialCheckDone, isAuthenticated, authLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
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("/");
|
window.location.replace("/");
|
||||||
}
|
}
|
||||||
}, [initialCheckDone, isMobile, isTablet]);
|
}, [initialCheckDone, isMobile, isTablet, proInterface]);
|
||||||
|
|
||||||
const mainTabs = useMemo(() => tabs.filter((t) => t.paneId === 'main'), [tabs]);
|
const mainTabs = useMemo(() => tabs.filter((t) => t.paneId === 'main'), [tabs]);
|
||||||
const splitTabs = useMemo(() => tabs.filter((t) => t.paneId === 'split'), [tabs]);
|
const splitTabs = useMemo(() => tabs.filter((t) => t.paneId === 'split'), [tabs]);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Link } from '@/i18n/navigation';
|
|
||||||
import { useSettingsStore, type ToolbarPosition, type MailLayout } from '@/stores/settings-store';
|
import { useSettingsStore, type ToolbarPosition, type MailLayout } from '@/stores/settings-store';
|
||||||
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
|
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { usePolicyStore } from '@/stores/policy-store';
|
import { usePolicyStore } from '@/stores/policy-store';
|
||||||
import { useAccountStore } from '@/stores/account-store';
|
import { useAccountStore } from '@/stores/account-store';
|
||||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
|
||||||
|
|
||||||
const MAIL_LAYOUT_PREVIEW_ROWS = [
|
const MAIL_LAYOUT_PREVIEW_ROWS = [
|
||||||
{ sender: 'Alice', subject: 'Quarterly roadmap', preview: 'The draft is ready for review.', selected: false },
|
{ 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 { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore();
|
||||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||||
const accounts = useAccountStore(s => s.accounts);
|
const accounts = useAccountStore(s => s.accounts);
|
||||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection title={t('title')} description={t('description')}>
|
<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')}>
|
<SettingItem label={t('pro_interface.label')} description={t('pro_interface.description')}>
|
||||||
<div className="flex items-center gap-3">
|
<ToggleSwitch
|
||||||
{proInterface && isDesktop && (
|
checked={proInterface}
|
||||||
<Link
|
onChange={(v) => updateSetting('proInterface', v)}
|
||||||
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>
|
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user