feat(routing): serve public landing at / and move authed index to /home
Stripe's compliance crawler fetches the apex URL without executing JS and declined live-mode review when `https://resolutionflow.com/` returned the empty SPA shell that redirected to /landing client-side. Restructure the router so / serves LandingPage directly: - `/` → new `PublicLanding` wrapper (LandingPage for anon; Navigate to /home for authed users so there's no marketing-frame flicker). - Authed tree converted to a path-less layout route with absolute child paths. QuickStartPage moves to `/home`; all other children (`/trees`, `/pilot`, `/admin/*`, `/account/*`, etc.) keep their URLs. - `/landing` kept as a one-release stale-bookmark redirect to /. - `ProtectedRoute` unauth redirect flipped /landing → /; `state.from` preserved for post-login return. Reference updates: - Post-login / post-onboarding destinations → /home: OAuthCallbackPage (incl. `?welcome=teammate` query), WelcomeStep1/2/3 dismiss-rest, AssistantChatPage post-escalate, WelcomeRouter completion/dismiss redirects, VerifyEmailPage's three "Go to dashboard" links. - Authed chrome → /home: TopBar logo, AppLayout mobile nav + drawer logo, CommandPalette Dashboard entry. - Dashboard onboarding → /home: NextStepCard `ran_session.ctaPath`, SetupChecklist `ran_session.path`, SessionHistoryPage empty-state CTA. - Public back-links → /: TermsPage, PrivacyPage, PoliciesPage, ContactPage, PromotionsPage, PublicTemplatesPage (header + footer). SharedSessionPage's `to="/"` left as-is — now correctly lands anon visitors on the public landing. Crawlability: - New `frontend/public/robots.txt` allowlisting public pages and disallowing the authed app. - New `frontend/public/sitemap.xml` for /, /pricing, /contact-sales, /contact, /templates, /terms, /privacy, /policies, /promotions. - `PageMeta` gains an `og:url` (defaults to `window.location.href`) and flips `twitter:card` to `summary_large_image` when an `ogImage` is passed. Tests: - `AppLayout.test.tsx` updated to mount at `/home`. - New `ProtectedRoute.test.tsx` asserts unauthenticated `/home` redirects to `/` (not `/landing`) and preserves origin in `state.from`. If Stripe's crawler still cannot see the site after this (zero-JS crawler), the documented next escalation is server-side prerendering of public routes via `vite-plugin-ssg`. Out of scope here. Plan: docs/plans/2026-05-13-public-landing-routing-refactor.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
36
frontend/public/robots.txt
Normal file
36
frontend/public/robots.txt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Allow: /terms
|
||||||
|
Allow: /policies
|
||||||
|
Allow: /privacy
|
||||||
|
Allow: /contact
|
||||||
|
Allow: /contact-sales
|
||||||
|
Allow: /pricing
|
||||||
|
Allow: /promotions
|
||||||
|
Allow: /templates
|
||||||
|
Disallow: /home
|
||||||
|
Disallow: /trees/
|
||||||
|
Disallow: /my-trees
|
||||||
|
Disallow: /pilot/
|
||||||
|
Disallow: /admin/
|
||||||
|
Disallow: /account/
|
||||||
|
Disallow: /script-builder
|
||||||
|
Disallow: /scripts
|
||||||
|
Disallow: /sessions
|
||||||
|
Disallow: /analytics
|
||||||
|
Disallow: /escalations
|
||||||
|
Disallow: /queue
|
||||||
|
Disallow: /review-queue
|
||||||
|
Disallow: /network-diagrams
|
||||||
|
Disallow: /kb-accelerator
|
||||||
|
Disallow: /step-library
|
||||||
|
Disallow: /tickets
|
||||||
|
Disallow: /shares
|
||||||
|
Disallow: /feedback
|
||||||
|
Disallow: /welcome
|
||||||
|
Disallow: /flow-assist
|
||||||
|
Disallow: /dev/
|
||||||
|
Disallow: /flows/
|
||||||
|
Disallow: /guides
|
||||||
|
|
||||||
|
Sitemap: https://resolutionflow.com/sitemap.xml
|
||||||
57
frontend/public/sitemap.xml
Normal file
57
frontend/public/sitemap.xml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://resolutionflow.com/</loc>
|
||||||
|
<lastmod>2026-05-13</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://resolutionflow.com/pricing</loc>
|
||||||
|
<lastmod>2026-05-13</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://resolutionflow.com/contact-sales</loc>
|
||||||
|
<lastmod>2026-05-13</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://resolutionflow.com/contact</loc>
|
||||||
|
<lastmod>2026-05-13</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://resolutionflow.com/templates</loc>
|
||||||
|
<lastmod>2026-05-13</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://resolutionflow.com/terms</loc>
|
||||||
|
<lastmod>2026-05-13</lastmod>
|
||||||
|
<changefreq>yearly</changefreq>
|
||||||
|
<priority>0.4</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://resolutionflow.com/privacy</loc>
|
||||||
|
<lastmod>2026-05-13</lastmod>
|
||||||
|
<changefreq>yearly</changefreq>
|
||||||
|
<priority>0.4</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://resolutionflow.com/policies</loc>
|
||||||
|
<lastmod>2026-05-13</lastmod>
|
||||||
|
<changefreq>yearly</changefreq>
|
||||||
|
<priority>0.4</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://resolutionflow.com/promotions</loc>
|
||||||
|
<lastmod>2026-05-13</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.4</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
@@ -5,6 +5,8 @@ interface PageMetaProps {
|
|||||||
description?: string
|
description?: string
|
||||||
ogImage?: string
|
ogImage?: string
|
||||||
ogType?: string
|
ogType?: string
|
||||||
|
/** Canonical/Open Graph URL. Defaults to `window.location.href` in the browser. */
|
||||||
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const SITE_NAME = 'ResolutionFlow'
|
const SITE_NAME = 'ResolutionFlow'
|
||||||
@@ -20,8 +22,12 @@ export function PageMeta({
|
|||||||
description = DEFAULT_DESCRIPTION,
|
description = DEFAULT_DESCRIPTION,
|
||||||
ogImage,
|
ogImage,
|
||||||
ogType = 'website',
|
ogType = 'website',
|
||||||
|
url,
|
||||||
}: PageMetaProps) {
|
}: PageMetaProps) {
|
||||||
const fullTitle = title ? `${title} | ${SITE_NAME}` : `${SITE_NAME} — ${DEFAULT_TAGLINE}`
|
const fullTitle = title ? `${title} | ${SITE_NAME}` : `${SITE_NAME} — ${DEFAULT_TAGLINE}`
|
||||||
|
const resolvedUrl =
|
||||||
|
url ?? (typeof window !== 'undefined' ? window.location.href : undefined)
|
||||||
|
const twitterCard = ogImage ? 'summary_large_image' : 'summary'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -33,10 +39,11 @@ export function PageMeta({
|
|||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:type" content={ogType} />
|
<meta property="og:type" content={ogType} />
|
||||||
<meta property="og:site_name" content={SITE_NAME} />
|
<meta property="og:site_name" content={SITE_NAME} />
|
||||||
|
{resolvedUrl && <meta property="og:url" content={resolvedUrl} />}
|
||||||
{ogImage && <meta property="og:image" content={ogImage} />}
|
{ogImage && <meta property="og:image" content={ogImage} />}
|
||||||
|
|
||||||
{/* Twitter */}
|
{/* Twitter */}
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content={twitterCard} />
|
||||||
<meta name="twitter:title" content={fullTitle} />
|
<meta name="twitter:title" content={fullTitle} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
{ogImage && <meta name="twitter:image" content={ogImage} />}
|
{ogImage && <meta name="twitter:image" content={ogImage} />}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function pickNextStep(
|
|||||||
title: 'Run your first FlowPilot session',
|
title: 'Run your first FlowPilot session',
|
||||||
description: 'Paste a ticket or pick a flow to see ResolutionFlow in action.',
|
description: 'Paste a ticket or pick a flow to see ResolutionFlow in action.',
|
||||||
ctaLabel: 'Start a session',
|
ctaLabel: 'Start a session',
|
||||||
ctaPath: '/',
|
ctaPath: '/home',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!status.connected_psa) {
|
if (!status.connected_psa) {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function buildChecklistItems(
|
|||||||
{
|
{
|
||||||
key: 'ran_session',
|
key: 'ran_session',
|
||||||
label: 'Run your first FlowPilot session',
|
label: 'Run your first FlowPilot session',
|
||||||
path: '/',
|
path: '/home',
|
||||||
done: status.ran_session,
|
done: status.ran_session,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function AppLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mobileNavItems = [
|
const mobileNavItems = [
|
||||||
{ path: '/', label: 'Dashboard', icon: LayoutGrid },
|
{ path: '/home', label: 'Dashboard', icon: LayoutGrid },
|
||||||
{ path: '/sessions', label: 'Session History', icon: Clock },
|
{ path: '/sessions', label: 'Session History', icon: Clock },
|
||||||
{ path: '/escalations', label: 'Escalations', icon: AlertTriangle },
|
{ path: '/escalations', label: 'Escalations', icon: AlertTriangle },
|
||||||
{ path: '/trees', label: 'Guided Flows', icon: GitBranch },
|
{ path: '/trees', label: 'Guided Flows', icon: GitBranch },
|
||||||
@@ -106,7 +106,7 @@ export function AppLayout() {
|
|||||||
style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)' }}
|
style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)' }}
|
||||||
>
|
>
|
||||||
<div className="flex h-14 items-center justify-between px-4" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
|
<div className="flex h-14 items-center justify-between px-4" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
|
||||||
<Link to="/" className="flex items-center gap-2.5">
|
<Link to="/home" className="flex items-center gap-2.5">
|
||||||
<BrandLogo size="sm" />
|
<BrandLogo size="sm" />
|
||||||
<span className="text-sm font-heading font-bold text-text-heading">ResolutionFlow</span>
|
<span className="text-sm font-heading font-bold text-text-heading">ResolutionFlow</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ interface Group {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PAGES: PaletteItem[] = [
|
const PAGES: PaletteItem[] = [
|
||||||
{ id: 'page-dashboard', group: 'pages', title: 'Dashboard', path: '/', icon: 'page' },
|
{ id: 'page-dashboard', group: 'pages', title: 'Dashboard', path: '/home', icon: 'page' },
|
||||||
{ id: 'page-flows', group: 'pages', title: 'All Flows', subtitle: 'Browse your flow library', path: '/trees', icon: 'page' },
|
{ id: 'page-flows', group: 'pages', title: 'All Flows', subtitle: 'Browse your flow library', path: '/trees', icon: 'page' },
|
||||||
{ id: 'page-sessions', group: 'pages', title: 'Sessions', subtitle: 'View session history', path: '/sessions', icon: 'page' },
|
{ id: 'page-sessions', group: 'pages', title: 'Sessions', subtitle: 'View session history', path: '/sessions', icon: 'page' },
|
||||||
{ id: 'page-flowpilot', group: 'pages', title: 'FlowPilot', subtitle: 'AI troubleshooting', path: '/pilot', icon: 'page' },
|
{ id: 'page-flowpilot', group: 'pages', title: 'FlowPilot', subtitle: 'AI troubleshooting', path: '/pilot', icon: 'page' },
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <Navigate to="/landing" state={{ from: location }} replace />
|
return <Navigate to="/" state={{ from: location }} replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce must_change_password — redirect unless already on /change-password
|
// Enforce must_change_password — redirect unless already on /change-password
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function TopBar() {
|
|||||||
>
|
>
|
||||||
{/* Logo area */}
|
{/* Logo area */}
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/home"
|
||||||
className="flex items-center gap-2.5 pr-4 transition-all duration-200"
|
className="flex items-center gap-2.5 pr-4 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<BrandLogo size="sm" />
|
<BrandLogo size="sm" />
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ const FROZEN_NOW = new Date('2026-05-06T00:00:00Z')
|
|||||||
|
|
||||||
function renderAppLayout() {
|
function renderAppLayout() {
|
||||||
return render(
|
return render(
|
||||||
<MemoryRouter initialEntries={['/']}>
|
<MemoryRouter initialEntries={['/home']}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<AppLayout />}>
|
<Route element={<AppLayout />}>
|
||||||
<Route
|
<Route
|
||||||
index
|
path="/home"
|
||||||
element={<div data-testid="child-route-content">child route</div>}
|
element={<div data-testid="child-route-content">child route</div>}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { ProtectedRoute } from '../ProtectedRoute'
|
||||||
|
import { useAuthStore } from '@/store/authStore'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Probe component: surfaces the current pathname and `location.state.from` so
|
||||||
|
* the test can assert both the redirect target and that the original
|
||||||
|
* destination is preserved for post-login return.
|
||||||
|
*/
|
||||||
|
function LocationProbe() {
|
||||||
|
const loc = useLocation()
|
||||||
|
const from =
|
||||||
|
(loc.state as { from?: { pathname?: string } } | null)?.from?.pathname ?? ''
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-testid="probe-pathname">{loc.pathname}</div>
|
||||||
|
<div data-testid="probe-from">{from}</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ProtectedRoute — unauthenticated redirect', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useAuthStore.setState({
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isLoading: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects unauthenticated visits to /home → / and preserves origin in state.from', () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/home']}>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/home"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<div data-testid="home-content">home</div>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="/" element={<LocationProbe />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// The protected page should not render.
|
||||||
|
expect(screen.queryByTestId('home-content')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
// We landed on / (the public landing route), not /landing.
|
||||||
|
expect(screen.getByTestId('probe-pathname')).toHaveTextContent('/')
|
||||||
|
expect(screen.getByTestId('probe-from')).toHaveTextContent('/home')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2416,7 +2416,7 @@ export default function AssistantChatPage() {
|
|||||||
setShowConclude(false)
|
setShowConclude(false)
|
||||||
if (activeSessionStatus === 'escalated') {
|
if (activeSessionStatus === 'escalated') {
|
||||||
toast.info('Session escalated. Heading back to your dashboard.')
|
toast.info('Session escalated. Heading back to your dashboard.')
|
||||||
navigate('/')
|
navigate('/home')
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onConclude={handleConclude}
|
onConclude={handleConclude}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default function ContactPage() {
|
|||||||
<PageMeta title="Contact" description="Contact ResolutionFlow customer service, sales, billing, or security." />
|
<PageMeta title="Contact" description="Contact ResolutionFlow customer service, sales, billing, or security." />
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||||
<h1 className="text-3xl font-bold font-heading mb-4">Contact ResolutionFlow</h1>
|
<h1 className="text-3xl font-bold font-heading mb-4">Contact ResolutionFlow</h1>
|
||||||
<p className="text-muted-foreground mb-10">
|
<p className="text-muted-foreground mb-10">
|
||||||
We respond to customer inquiries Monday through Friday during U.S. business hours, excluding federal holidays. Email is the fastest path to a response.
|
We respond to customer inquiries Monday through Friday during U.S. business hours, excluding federal holidays. Email is the fastest path to a response.
|
||||||
|
|||||||
@@ -112,10 +112,10 @@ export function OAuthCallbackPage() {
|
|||||||
|
|
||||||
// Invitee path lands on the dashboard with the teammate-welcome
|
// Invitee path lands on the dashboard with the teammate-welcome
|
||||||
// marker; new self-serve owners go to the welcome wizard; returning
|
// marker; new self-serve owners go to the welcome wizard; returning
|
||||||
// users to /.
|
// users to /home.
|
||||||
let dest = '/'
|
let dest = '/home'
|
||||||
if (decoded?.accountInviteCode) {
|
if (decoded?.accountInviteCode) {
|
||||||
dest = '/?welcome=teammate'
|
dest = '/home?welcome=teammate'
|
||||||
} else if (result.is_new_user) {
|
} else if (result.is_new_user) {
|
||||||
dest = '/welcome'
|
dest = '/welcome'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default function PoliciesPage() {
|
|||||||
<PageMeta title="Customer Policies" description="ResolutionFlow customer service, billing, refunds, cancellation, legal restrictions, and promotional terms." />
|
<PageMeta title="Customer Policies" description="ResolutionFlow customer service, billing, refunds, cancellation, legal restrictions, and promotional terms." />
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||||
<h1 className="text-3xl font-bold font-heading mb-4">Customer Policies</h1>
|
<h1 className="text-3xl font-bold font-heading mb-4">Customer Policies</h1>
|
||||||
<p className="text-muted-foreground mb-2">Last updated: May 7, 2026</p>
|
<p className="text-muted-foreground mb-2">Last updated: May 7, 2026</p>
|
||||||
<p className="text-muted-foreground mb-2"><strong className="text-foreground">Operator:</strong> ResolutionFlow, LLC (the “Company”), operator of ResolutionFlow (“Service”).</p>
|
<p className="text-muted-foreground mb-2"><strong className="text-foreground">Operator:</strong> ResolutionFlow, LLC (the “Company”), operator of ResolutionFlow (“Service”).</p>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default function PrivacyPage() {
|
|||||||
<PageMeta title="Privacy Policy" description="ResolutionFlow Privacy Policy" />
|
<PageMeta title="Privacy Policy" description="ResolutionFlow Privacy Policy" />
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||||
<h1 className="text-3xl font-bold font-heading mb-8">Privacy Policy</h1>
|
<h1 className="text-3xl font-bold font-heading mb-8">Privacy Policy</h1>
|
||||||
<p className="text-muted-foreground mb-6">Last updated: March 21, 2026</p>
|
<p className="text-muted-foreground mb-6">Last updated: March 21, 2026</p>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default function PromotionsPage() {
|
|||||||
<PageMeta title="Promotions" description="Active ResolutionFlow promotional offers and their terms." />
|
<PageMeta title="Promotions" description="Active ResolutionFlow promotional offers and their terms." />
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||||
<h1 className="text-3xl font-bold font-heading mb-4">Promotions</h1>
|
<h1 className="text-3xl font-bold font-heading mb-4">Promotions</h1>
|
||||||
<p className="text-muted-foreground mb-10">Last updated: May 7, 2026</p>
|
<p className="text-muted-foreground mb-10">Last updated: May 7, 2026</p>
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export default function PublicTemplatesPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="sticky top-0 z-40 border-b border-border bg-background/80">
|
<header className="sticky top-0 z-40 border-b border-border bg-background/80">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||||
<Link to="/landing" className="flex items-center gap-2.5">
|
<Link to="/" className="flex items-center gap-2.5">
|
||||||
<BrandLogo size="sm" />
|
<BrandLogo size="sm" />
|
||||||
<span className="font-heading text-lg font-semibold">
|
<span className="font-heading text-lg font-semibold">
|
||||||
<span className="text-foreground">Resolution</span>
|
<span className="text-foreground">Resolution</span>
|
||||||
@@ -406,7 +406,7 @@ export default function PublicTemplatesPage() {
|
|||||||
<footer className="border-t border-border py-8 px-4 sm:px-6 lg:px-8">
|
<footer className="border-t border-border py-8 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||||
<Link
|
<Link
|
||||||
to="/landing"
|
to="/"
|
||||||
className="text-muted-foreground text-sm hover:text-foreground transition-colors"
|
className="text-muted-foreground text-sm hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
Powered by <span className="font-semibold">ResolutionFlow</span>
|
Powered by <span className="font-semibold">ResolutionFlow</span>
|
||||||
|
|||||||
@@ -423,7 +423,7 @@ export default function SessionHistoryPage() {
|
|||||||
description="Start a FlowPilot or chat session to begin. All your sessions will appear here."
|
description="Start a FlowPilot or chat session to begin. All your sessions will appear here."
|
||||||
action={
|
action={
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/home"
|
||||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-semibold text-white hover:brightness-110 active:scale-[0.98] transition-all"
|
className="inline-flex items-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-semibold text-white hover:brightness-110 active:scale-[0.98] transition-all"
|
||||||
>
|
>
|
||||||
Start a Session
|
Start a Session
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default function TermsPage() {
|
|||||||
<PageMeta title="Terms of Service" description="ResolutionFlow Terms of Service" />
|
<PageMeta title="Terms of Service" description="ResolutionFlow Terms of Service" />
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||||
<Link to="/landing" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
<Link to="/" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">← Back to home</Link>
|
||||||
<h1 className="text-3xl font-bold font-heading mb-8">Terms of Service</h1>
|
<h1 className="text-3xl font-bold font-heading mb-8">Terms of Service</h1>
|
||||||
<p className="text-muted-foreground mb-6">Last updated: March 21, 2026</p>
|
<p className="text-muted-foreground mb-6">Last updated: March 21, 2026</p>
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export function VerifyEmailPage() {
|
|||||||
action needed.
|
action needed.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/home"
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-6 inline-flex items-center rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground',
|
'mt-6 inline-flex items-center rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground',
|
||||||
'hover:brightness-110',
|
'hover:brightness-110',
|
||||||
@@ -181,7 +181,7 @@ export function VerifyEmailPage() {
|
|||||||
Resend verification email
|
Resend verification email
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/home"
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center justify-center rounded-lg border border-default bg-input px-6 py-2 text-sm font-medium text-foreground',
|
'inline-flex items-center justify-center rounded-lg border border-default bg-input px-6 py-2 text-sm font-medium text-foreground',
|
||||||
'hover:border-border-hover',
|
'hover:border-border-hover',
|
||||||
@@ -204,7 +204,7 @@ export function VerifyEmailPage() {
|
|||||||
Try the link in your verification email again.
|
Try the link in your verification email again.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/home"
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-6 inline-flex items-center rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground',
|
'mt-6 inline-flex items-center rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground',
|
||||||
'hover:brightness-110',
|
'hover:brightness-110',
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { PageLoader } from '@/components/common/PageLoader'
|
|||||||
* `/welcome` index — redirect to the next incomplete step (or `/` if done /
|
* `/welcome` index — redirect to the next incomplete step (or `/` if done /
|
||||||
* dismissed). Decision table:
|
* dismissed). Decision table:
|
||||||
*
|
*
|
||||||
* onboarding_dismissed === true → /
|
* onboarding_dismissed === true → /home
|
||||||
* onboarding_step_completed >= 3 → /
|
* onboarding_step_completed >= 3 → /home
|
||||||
* onboarding_step_completed === null/0 → /welcome/step-1
|
* onboarding_step_completed === null/0 → /welcome/step-1
|
||||||
* onboarding_step_completed === 1 → /welcome/step-2
|
* onboarding_step_completed === 1 → /welcome/step-2
|
||||||
* onboarding_step_completed === 2 → /welcome/step-3
|
* onboarding_step_completed === 2 → /welcome/step-3
|
||||||
@@ -19,10 +19,10 @@ export function WelcomeRouter() {
|
|||||||
// the page loader rather than racing past the redirect.
|
// the page loader rather than racing past the redirect.
|
||||||
if (!user) return <PageLoader />
|
if (!user) return <PageLoader />
|
||||||
|
|
||||||
if (user.onboarding_dismissed) return <Navigate to="/" replace />
|
if (user.onboarding_dismissed) return <Navigate to="/home" replace />
|
||||||
|
|
||||||
const completed = user.onboarding_step_completed ?? 0
|
const completed = user.onboarding_step_completed ?? 0
|
||||||
if (completed >= 3) return <Navigate to="/" replace />
|
if (completed >= 3) return <Navigate to="/home" replace />
|
||||||
if (completed === 2) return <Navigate to="/welcome/step-3" replace />
|
if (completed === 2) return <Navigate to="/welcome/step-3" replace />
|
||||||
if (completed === 1) return <Navigate to="/welcome/step-2" replace />
|
if (completed === 1) return <Navigate to="/welcome/step-2" replace />
|
||||||
return <Navigate to="/welcome/step-1" replace />
|
return <Navigate to="/welcome/step-1" replace />
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function WelcomeStep1() {
|
|||||||
try {
|
try {
|
||||||
await onboardingApi.dismissRest()
|
await onboardingApi.dismissRest()
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
navigate('/')
|
navigate('/home')
|
||||||
} catch {
|
} catch {
|
||||||
setError('Could not save. Please try again.')
|
setError('Could not save. Please try again.')
|
||||||
setSubmitting(null)
|
setSubmitting(null)
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function WelcomeStep2() {
|
|||||||
try {
|
try {
|
||||||
await onboardingApi.dismissRest()
|
await onboardingApi.dismissRest()
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
navigate('/')
|
navigate('/home')
|
||||||
} catch {
|
} catch {
|
||||||
setError('Could not save. Please try again.')
|
setError('Could not save. Please try again.')
|
||||||
setSubmitting(null)
|
setSubmitting(null)
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export function WelcomeStep3() {
|
|||||||
try {
|
try {
|
||||||
await onboardingApi.dismissRest()
|
await onboardingApi.dismissRest()
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
navigate('/')
|
navigate('/home')
|
||||||
} catch {
|
} catch {
|
||||||
setError('Could not save. Please try again.')
|
setError('Could not save. Please try again.')
|
||||||
setSubmitting(null)
|
setSubmitting(null)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { RouteError } from '@/components/common/RouteError'
|
|||||||
import { ErrorBoundary } from '@/components/common/ErrorBoundary'
|
import { ErrorBoundary } from '@/components/common/ErrorBoundary'
|
||||||
import { PageLoader } from '@/components/common/PageLoader'
|
import { PageLoader } from '@/components/common/PageLoader'
|
||||||
import { lazyWithRetry } from '@/lib/lazyWithRetry'
|
import { lazyWithRetry } from '@/lib/lazyWithRetry'
|
||||||
|
import { useAuthStore } from '@/store/authStore'
|
||||||
|
|
||||||
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter)
|
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter)
|
||||||
import {
|
import {
|
||||||
@@ -118,10 +119,27 @@ function page(Component: React.LazyExoticComponent<React.ComponentType>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public `/` wrapper — sends authenticated users to /home before LandingPage
|
||||||
|
* mounts, so they never see marketing-frame flicker.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components -- router.tsx exports a router instance, not a component
|
||||||
|
function PublicLanding() {
|
||||||
|
const isAuthed = useAuthStore((s) => s.isAuthenticated)
|
||||||
|
if (isAuthed) return <Navigate to="/home" replace />
|
||||||
|
return page(LandingPage)
|
||||||
|
}
|
||||||
|
|
||||||
export const router = sentryCreateBrowserRouter([
|
export const router = sentryCreateBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <PublicLanding />,
|
||||||
|
errorElement: <RouteError />,
|
||||||
|
},
|
||||||
|
// Stale-bookmark redirect — keep one release, delete in a follow-up.
|
||||||
{
|
{
|
||||||
path: '/landing',
|
path: '/landing',
|
||||||
element: page(LandingPage),
|
element: <Navigate to="/" replace />,
|
||||||
errorElement: <RouteError />,
|
errorElement: <RouteError />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -229,7 +247,6 @@ export const router = sentryCreateBrowserRouter([
|
|||||||
errorElement: <RouteError />,
|
errorElement: <RouteError />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
|
||||||
element: (
|
element: (
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
@@ -237,56 +254,56 @@ export const router = sentryCreateBrowserRouter([
|
|||||||
),
|
),
|
||||||
errorElement: <RouteError />,
|
errorElement: <RouteError />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: page(QuickStartPage) },
|
{ path: '/home', element: page(QuickStartPage) },
|
||||||
{ path: 'trees', element: page(TreeLibraryPage) },
|
{ path: '/trees', element: page(TreeLibraryPage) },
|
||||||
{ path: 'my-trees', element: page(MyTreesPage) },
|
{ path: '/my-trees', element: page(MyTreesPage) },
|
||||||
{ path: 'trees/new', element: page(TreeEditorPage) },
|
{ path: '/trees/new', element: page(TreeEditorPage) },
|
||||||
{ path: 'trees/:id/edit', element: page(TreeEditorPage) },
|
{ path: '/trees/:id/edit', element: page(TreeEditorPage) },
|
||||||
{ path: 'flows/new', element: page(ProceduralEditorPage) },
|
{ path: '/flows/new', element: page(ProceduralEditorPage) },
|
||||||
{ path: 'flows/:id/edit', element: page(ProceduralEditorPage) },
|
{ path: '/flows/:id/edit', element: page(ProceduralEditorPage) },
|
||||||
{ path: 'flows/:id/navigate', element: page(ProceduralNavigationPage) },
|
{ path: '/flows/:id/navigate', element: page(ProceduralNavigationPage) },
|
||||||
{ path: 'flows/:id/maintenance', element: page(MaintenanceFlowDetailPage) },
|
{ path: '/flows/:id/maintenance', element: page(MaintenanceFlowDetailPage) },
|
||||||
{ path: 'flows/:id/batches/:batchId', element: page(BatchStatusPage) },
|
{ path: '/flows/:id/batches/:batchId', element: page(BatchStatusPage) },
|
||||||
{ path: 'trees/:id/navigate', element: page(TreeNavigationPage) },
|
{ path: '/trees/:id/navigate', element: page(TreeNavigationPage) },
|
||||||
{ path: 'sessions', element: page(SessionHistoryPage) },
|
{ path: '/sessions', element: page(SessionHistoryPage) },
|
||||||
{ path: 'sessions/:id', element: page(SessionDetailPage) },
|
{ path: '/sessions/:id', element: page(SessionDetailPage) },
|
||||||
{ path: 'tickets', element: page(TicketsPage) },
|
{ path: '/tickets', element: page(TicketsPage) },
|
||||||
{ path: 'shares', element: page(MySharesPage) },
|
{ path: '/shares', element: page(MySharesPage) },
|
||||||
{ path: 'analytics', element: page(TeamAnalyticsPage) },
|
{ path: '/analytics', element: page(TeamAnalyticsPage) },
|
||||||
{ path: 'analytics/me', element: page(MyAnalyticsPage) },
|
{ path: '/analytics/me', element: page(MyAnalyticsPage) },
|
||||||
{ path: 'feedback', element: page(FeedbackPage) },
|
{ path: '/feedback', element: page(FeedbackPage) },
|
||||||
{ path: 'step-library', element: page(StepLibraryPage) },
|
{ path: '/step-library', element: page(StepLibraryPage) },
|
||||||
{ path: 'scripts', element: page(ScriptLibraryPage) },
|
{ path: '/scripts', element: page(ScriptLibraryPage) },
|
||||||
{ path: 'scripts/manage', element: page(ScriptManagePage) },
|
{ path: '/scripts/manage', element: page(ScriptManagePage) },
|
||||||
{ path: 'script-builder', element: page(ScriptBuilderPage) },
|
{ path: '/script-builder', element: page(ScriptBuilderPage) },
|
||||||
{ path: 'network-diagrams', element: page(NetworkDiagramsPage) },
|
{ path: '/network-diagrams', element: page(NetworkDiagramsPage) },
|
||||||
{ path: 'network-diagrams/new', element: page(DiagramEditorPage) },
|
{ path: '/network-diagrams/new', element: page(DiagramEditorPage) },
|
||||||
{ path: 'network-diagrams/:id', element: page(DiagramEditorPage) },
|
{ path: '/network-diagrams/:id', element: page(DiagramEditorPage) },
|
||||||
{ path: 'kb-accelerator', element: page(KBAcceleratorPage) },
|
{ path: '/kb-accelerator', element: page(KBAcceleratorPage) },
|
||||||
// Phase 1 — FlowPilot migration. The unified chat-primary surface lives at
|
// Phase 1 — FlowPilot migration. The unified chat-primary surface lives at
|
||||||
// /pilot; /assistant permanently redirects. FlowPilotSessionPage (old
|
// /pilot; /assistant permanently redirects. FlowPilotSessionPage (old
|
||||||
// guided surface) is no longer mounted.
|
// guided surface) is no longer mounted.
|
||||||
{ path: 'pilot', element: page(AssistantChatPage) },
|
{ path: '/pilot', element: page(AssistantChatPage) },
|
||||||
{ path: 'pilot/:sessionId', element: page(AssistantChatPage) },
|
{ path: '/pilot/:sessionId', element: page(AssistantChatPage) },
|
||||||
{ path: 'assistant', element: <Navigate to="/pilot" replace /> },
|
{ path: '/assistant', element: <Navigate to="/pilot" replace /> },
|
||||||
{ path: 'assistant/:sessionId', element: <AssistantSessionRedirect /> },
|
{ path: '/assistant/:sessionId', element: <AssistantSessionRedirect /> },
|
||||||
{ path: 'flow-assist', element: page(FlowAssistPage) },
|
{ path: '/flow-assist', element: page(FlowAssistPage) },
|
||||||
{ path: 'escalations', element: page(EscalationQueuePage) },
|
{ path: '/escalations', element: page(EscalationQueuePage) },
|
||||||
{ path: 'queue', element: page(SessionQueuePage) },
|
{ path: '/queue', element: page(SessionQueuePage) },
|
||||||
{ path: 'review-queue', element: page(ReviewQueuePage) },
|
{ path: '/review-queue', element: page(ReviewQueuePage) },
|
||||||
{ path: 'analytics/flowpilot', element: page(FlowPilotAnalyticsPage) },
|
{ path: '/analytics/flowpilot', element: page(FlowPilotAnalyticsPage) },
|
||||||
{ path: 'dev/branching', element: page(DevBranchingPage) },
|
{ path: '/dev/branching', element: page(DevBranchingPage) },
|
||||||
{ path: 'guides', element: page(GuidesHubPage) },
|
{ path: '/guides', element: page(GuidesHubPage) },
|
||||||
{ path: 'guides/:slug', element: page(GuideDetailPage) },
|
{ path: '/guides/:slug', element: page(GuideDetailPage) },
|
||||||
// Welcome wizard (Phase 2). Mounted inside AppLayout so the email-
|
// Welcome wizard (Phase 2). Mounted inside AppLayout so the email-
|
||||||
// verification banner persists above each step.
|
// verification banner persists above each step.
|
||||||
{ path: 'welcome', element: page(WelcomeRouter) },
|
{ path: '/welcome', element: page(WelcomeRouter) },
|
||||||
{ path: 'welcome/step-1', element: page(WelcomeStep1) },
|
{ path: '/welcome/step-1', element: page(WelcomeStep1) },
|
||||||
{ path: 'welcome/step-2', element: page(WelcomeStep2) },
|
{ path: '/welcome/step-2', element: page(WelcomeStep2) },
|
||||||
{ path: 'welcome/step-3', element: page(WelcomeStep3) },
|
{ path: '/welcome/step-3', element: page(WelcomeStep3) },
|
||||||
// Admin routes
|
// Admin routes
|
||||||
{
|
{
|
||||||
path: 'admin',
|
path: '/admin',
|
||||||
element: (
|
element: (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<PageLoader />}>
|
<Suspense fallback={<PageLoader />}>
|
||||||
@@ -315,7 +332,7 @@ export const router = sentryCreateBrowserRouter([
|
|||||||
},
|
},
|
||||||
// Account routes
|
// Account routes
|
||||||
{
|
{
|
||||||
path: 'account',
|
path: '/account',
|
||||||
element: (
|
element: (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<PageLoader />}>
|
<Suspense fallback={<PageLoader />}>
|
||||||
|
|||||||
Reference in New Issue
Block a user