feat(billing): plan taxonomy reconciliation + Stripe sync + internal-tester allowlist #164
@@ -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