fix(frontend): page-title — escapes + propagate plan taxonomy through frontend types
Two fixes that surfaced together:
1. LandingPage.tsx had `—` in a JSX attribute string — JSX attribute
strings don't process JS escape sequences, so the literal six-character
"—" was rendering in the browser tab title and OG description
instead of the intended em dash. Replaced with the literal em dash
character. Same pattern was previously valid because every other use
of `\u...` in the codebase is inside a JS string (regular `'...'`
string literal in TS, or `{...}` expression in JSX), where escapes
resolve at compile time. Verified by grep — LandingPage was the only
site with the bug.
2. PageMeta default fallback tagline was "Decision Tree Platform" — a
stale tagline from before the FlowPilot pivot. Updated to the current
"AI-Powered Troubleshooting for MSPs" (matches index.html and brand
positioning). Default branch is rarely hit since every page passes
a title, but cleaner.
While building, hit TS errors that revealed the prior taxonomy commit
(team -> enterprise + add starter) didn't propagate through the frontend.
Cleared all of them:
- types/account.ts, types/admin.ts: Subscription.plan, AdminAccountCreate.plan,
InviteCodeCreateRequest.assigned_plan literals updated to the new tax.
- types/billing.ts: dropped 'team' from CheckoutPlan (was hybrid old+new).
- admin/AccountsPage.tsx, admin/InviteCodesPage.tsx: state-type literals,
select onChange casts, and the visible <option> rows updated. PLAN_OPTIONS
in InviteCodesPage now has all four tiers with correct labels.
- AccountSettingsPage.tsx: `plan !== 'team'` -> `'enterprise'`, CheckoutButton
prop value too.
- subscription/CheckoutButton.tsx: prop type was 'pro' | 'team', updated
to 'starter' | 'pro' | 'enterprise' with matching planLabels.
Verified: tsc -b clean, lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ interface PageMetaProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SITE_NAME = 'ResolutionFlow'
|
const SITE_NAME = 'ResolutionFlow'
|
||||||
|
const DEFAULT_TAGLINE = 'AI-Powered Troubleshooting for MSPs'
|
||||||
const DEFAULT_DESCRIPTION = 'Transform troubleshooting into guided workflows with automatic documentation'
|
const DEFAULT_DESCRIPTION = 'Transform troubleshooting into guided workflows with automatic documentation'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,7 +21,7 @@ export function PageMeta({
|
|||||||
ogImage,
|
ogImage,
|
||||||
ogType = 'website',
|
ogType = 'website',
|
||||||
}: PageMetaProps) {
|
}: PageMetaProps) {
|
||||||
const fullTitle = title ? `${title} | ${SITE_NAME}` : `${SITE_NAME} - Decision Tree Platform`
|
const fullTitle = title ? `${title} | ${SITE_NAME}` : `${SITE_NAME} — ${DEFAULT_TAGLINE}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface CheckoutButtonProps {
|
interface CheckoutButtonProps {
|
||||||
plan: 'pro' | 'team'
|
plan: 'starter' | 'pro' | 'enterprise'
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CheckoutButton({ plan, className }: CheckoutButtonProps) {
|
export function CheckoutButton({ plan, className }: CheckoutButtonProps) {
|
||||||
const planLabels = { pro: 'Pro', team: 'Team' }
|
const planLabels = { starter: 'Starter', pro: 'Pro', enterprise: 'Enterprise' }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -418,10 +418,10 @@ export function AccountSettingsPage() {
|
|||||||
<p className="text-sm text-muted-foreground">Plan limits unavailable.</p>
|
<p className="text-sm text-muted-foreground">Plan limits unavailable.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{plan !== 'team' && (
|
{plan !== 'enterprise' && (
|
||||||
<div className="flex flex-wrap justify-end gap-2 pt-2">
|
<div className="flex flex-wrap justify-end gap-2 pt-2">
|
||||||
{plan === 'free' && <CheckoutButton plan="pro" />}
|
{plan === 'free' && <CheckoutButton plan="pro" />}
|
||||||
<CheckoutButton plan="team" />
|
<CheckoutButton plan="enterprise" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const FAQ_ITEMS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: 'What PSA tools do you integrate with?',
|
q: 'What PSA tools do you integrate with?',
|
||||||
a: 'Launching with ConnectWise PSA \u2014 session documentation exports directly as internal ticket notes. Atera and Syncro integrations are next. During beta, you can copy formatted notes into any PSA.',
|
a: 'Launching with ConnectWise PSA — session documentation exports directly as internal ticket notes. Atera and Syncro integrations are next. During beta, you can copy formatted notes into any PSA.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: 'What counts as a \u201csession\u201d?',
|
q: 'What counts as a \u201csession\u201d?',
|
||||||
@@ -23,7 +23,7 @@ const FAQ_ITEMS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: 'What if FlowPilot gets it wrong?',
|
q: 'What if FlowPilot gets it wrong?',
|
||||||
a: 'FlowPilot is a copilot, not autopilot. Every suggestion is a recommendation \u2014 you decide what to act on. And because every step is documented, you always have a full audit trail of what was tried and why.',
|
a: 'FlowPilot is a copilot, not autopilot. Every suggestion is a recommendation — you decide what to act on. And because every step is documented, you always have a full audit trail of what was tried and why.',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -75,8 +75,8 @@ export default function LandingPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageMeta
|
<PageMeta
|
||||||
title="ResolutionFlow \u2014 From Issue to Resolution, Documented"
|
title="ResolutionFlow — From Issue to Resolution, Documented"
|
||||||
description="Your AI troubleshooting copilot. Describe the issue, get help fixing it, and get clean ticket notes \u2014 automatically."
|
description="Your AI troubleshooting copilot. Describe the issue, get help fixing it, and get clean ticket notes — automatically."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="landing-page">
|
<div className="landing-page">
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function UsersPage() {
|
|||||||
})
|
})
|
||||||
const [inviteLoading, setInviteLoading] = useState(false)
|
const [inviteLoading, setInviteLoading] = useState(false)
|
||||||
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
|
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
|
||||||
const [createAccountForm, setCreateAccountForm] = useState({ name: '', plan: 'free' as 'free' | 'pro' | 'team', owner_email: '' })
|
const [createAccountForm, setCreateAccountForm] = useState({ name: '', plan: 'free' as 'free' | 'pro' | 'starter' | 'enterprise', owner_email: '' })
|
||||||
const [createAccountLoading, setCreateAccountLoading] = useState(false)
|
const [createAccountLoading, setCreateAccountLoading] = useState(false)
|
||||||
|
|
||||||
const fetchAccounts = useCallback(async () => {
|
const fetchAccounts = useCallback(async () => {
|
||||||
@@ -469,7 +469,8 @@ export function UsersPage() {
|
|||||||
<option value="all">All plans</option>
|
<option value="all">All plans</option>
|
||||||
<option value="free">Free</option>
|
<option value="free">Free</option>
|
||||||
<option value="pro">Pro</option>
|
<option value="pro">Pro</option>
|
||||||
<option value="team">Team</option>
|
<option value="enterprise">Enterprise</option>
|
||||||
|
<option value="starter">Starter</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
@@ -629,7 +630,7 @@ export function UsersPage() {
|
|||||||
<label className="mb-1 block text-sm font-medium text-foreground">Initial Plan</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Initial Plan</label>
|
||||||
<select
|
<select
|
||||||
value={createAccountForm.plan}
|
value={createAccountForm.plan}
|
||||||
onChange={(e) => setCreateAccountForm((form) => ({ ...form, plan: e.target.value as 'free' | 'pro' | 'team' }))}
|
onChange={(e) => setCreateAccountForm((form) => ({ ...form, plan: e.target.value as 'free' | 'pro' | 'starter' | 'enterprise' }))}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||||
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||||
@@ -637,7 +638,8 @@ export function UsersPage() {
|
|||||||
>
|
>
|
||||||
<option value="free">Free</option>
|
<option value="free">Free</option>
|
||||||
<option value="pro">Pro</option>
|
<option value="pro">Pro</option>
|
||||||
<option value="team">Team</option>
|
<option value="enterprise">Enterprise</option>
|
||||||
|
<option value="starter">Starter</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import type { InviteCodeResponse, InviteCodeCreateRequest } from '@/types/admin'
|
|||||||
|
|
||||||
const PLAN_OPTIONS = [
|
const PLAN_OPTIONS = [
|
||||||
{ value: 'free', label: 'Free' },
|
{ value: 'free', label: 'Free' },
|
||||||
|
{ value: 'starter', label: 'Starter' },
|
||||||
{ value: 'pro', label: 'Pro' },
|
{ value: 'pro', label: 'Pro' },
|
||||||
{ value: 'team', label: 'Team' },
|
{ value: 'enterprise', label: 'Enterprise' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const planBadgeVariant = (plan: string): 'success' | 'destructive' | 'warning' | 'default' => {
|
const planBadgeVariant = (plan: string): 'success' | 'destructive' | 'warning' | 'default' => {
|
||||||
@@ -33,7 +34,7 @@ export function InviteCodesPage() {
|
|||||||
// Form state
|
// Form state
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [expiresInDays, setExpiresInDays] = useState('')
|
const [expiresInDays, setExpiresInDays] = useState('')
|
||||||
const [assignedPlan, setAssignedPlan] = useState<'free' | 'pro' | 'team'>('free')
|
const [assignedPlan, setAssignedPlan] = useState<'free' | 'pro' | 'starter' | 'enterprise'>('free')
|
||||||
const [trialDays, setTrialDays] = useState('')
|
const [trialDays, setTrialDays] = useState('')
|
||||||
const [note, setNote] = useState('')
|
const [note, setNote] = useState('')
|
||||||
|
|
||||||
@@ -269,7 +270,7 @@ export function InviteCodesPage() {
|
|||||||
aria-label="Plan"
|
aria-label="Plan"
|
||||||
value={assignedPlan}
|
value={assignedPlan}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const plan = e.target.value as 'free' | 'pro' | 'team'
|
const plan = e.target.value as 'free' | 'pro' | 'starter' | 'enterprise'
|
||||||
setAssignedPlan(plan)
|
setAssignedPlan(plan)
|
||||||
if (plan === 'free') setTrialDays('')
|
if (plan === 'free') setTrialDays('')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface Account {
|
|||||||
export interface Subscription {
|
export interface Subscription {
|
||||||
id: string
|
id: string
|
||||||
account_id: string
|
account_id: string
|
||||||
plan: 'free' | 'pro' | 'team'
|
plan: 'free' | 'pro' | 'starter' | 'enterprise'
|
||||||
status: 'active' | 'past_due' | 'canceled' | 'trialing' | 'orphaned'
|
status: 'active' | 'past_due' | 'canceled' | 'trialing' | 'orphaned'
|
||||||
current_period_start: string | null
|
current_period_start: string | null
|
||||||
current_period_end: string | null
|
current_period_end: string | null
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export interface AdminAccountDetailResponse extends AdminAccountListItem {
|
|||||||
|
|
||||||
export interface AdminAccountCreate {
|
export interface AdminAccountCreate {
|
||||||
name: string
|
name: string
|
||||||
plan: 'free' | 'pro' | 'team'
|
plan: 'free' | 'pro' | 'starter' | 'enterprise'
|
||||||
owner_email?: string
|
owner_email?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ export interface InviteCodeCreateRequest {
|
|||||||
expires_at?: string | null
|
expires_at?: string | null
|
||||||
note?: string | null
|
note?: string | null
|
||||||
email?: string | null
|
email?: string | null
|
||||||
assigned_plan?: 'free' | 'pro' | 'team'
|
assigned_plan?: 'free' | 'pro' | 'starter' | 'enterprise'
|
||||||
trial_duration_days?: number | null
|
trial_duration_days?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export interface BillingStateApiResponse {
|
|||||||
* Checkout / Customer-Portal session types
|
* Checkout / Customer-Portal session types
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
export type CheckoutPlan = 'starter' | 'pro' | 'team' | 'enterprise'
|
export type CheckoutPlan = 'starter' | 'pro' | 'enterprise'
|
||||||
export type BillingInterval = 'monthly' | 'annual'
|
export type BillingInterval = 'monthly' | 'annual'
|
||||||
|
|
||||||
export interface CheckoutSessionRequest {
|
export interface CheckoutSessionRequest {
|
||||||
|
|||||||
Reference in New Issue
Block a user