feat(billing): plan taxonomy reconciliation + Stripe sync + internal-tester allowlist (#164)
Co-authored-by: Michael Chihlas <michael@resolutionflow.com> Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
This commit was merged in pull request #164.
This commit is contained in:
@@ -8,6 +8,7 @@ interface PageMetaProps {
|
||||
}
|
||||
|
||||
const SITE_NAME = 'ResolutionFlow'
|
||||
const DEFAULT_TAGLINE = 'AI-Powered Troubleshooting for MSPs'
|
||||
const DEFAULT_DESCRIPTION = 'Transform troubleshooting into guided workflows with automatic documentation'
|
||||
|
||||
/**
|
||||
@@ -20,7 +21,7 @@ export function PageMeta({
|
||||
ogImage,
|
||||
ogType = 'website',
|
||||
}: PageMetaProps) {
|
||||
const fullTitle = title ? `${title} | ${SITE_NAME}` : `${SITE_NAME} - Decision Tree Platform`
|
||||
const fullTitle = title ? `${title} | ${SITE_NAME}` : `${SITE_NAME} — ${DEFAULT_TAGLINE}`
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface CheckoutButtonProps {
|
||||
plan: 'pro' | 'team'
|
||||
plan: 'starter' | 'pro' | 'enterprise'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CheckoutButton({ plan, className }: CheckoutButtonProps) {
|
||||
const planLabels = { pro: 'Pro', team: 'Team' }
|
||||
const planLabels = { starter: 'Starter', pro: 'Pro', enterprise: 'Enterprise' }
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -8,7 +8,7 @@ export function useSubscription() {
|
||||
const usage = subscription?.usage ?? null
|
||||
const isActive = subscription?.subscription.status === 'active' || subscription?.subscription.status === 'trialing'
|
||||
|
||||
const isPaidPlan = plan === 'pro' || plan === 'team'
|
||||
const isPaidPlan = plan === 'pro' || plan === 'starter' || plan === 'enterprise'
|
||||
|
||||
const canUseFeature = (feature: 'custom_branding' | 'priority_support'): boolean => {
|
||||
if (!limits) return false
|
||||
|
||||
@@ -418,10 +418,10 @@ export function AccountSettingsPage() {
|
||||
<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">
|
||||
{plan === 'free' && <CheckoutButton plan="pro" />}
|
||||
<CheckoutButton plan="team" />
|
||||
<CheckoutButton plan="enterprise" />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -15,7 +15,7 @@ const FAQ_ITEMS = [
|
||||
},
|
||||
{
|
||||
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?',
|
||||
@@ -23,7 +23,7 @@ const FAQ_ITEMS = [
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<>
|
||||
<PageMeta
|
||||
title="ResolutionFlow \u2014 From Issue to Resolution, Documented"
|
||||
description="Your AI troubleshooting copilot. Describe the issue, get help fixing it, and get clean ticket notes \u2014 automatically."
|
||||
title="ResolutionFlow — From Issue to Resolution, Documented"
|
||||
description="Your AI troubleshooting copilot. Describe the issue, get help fixing it, and get clean ticket notes — automatically."
|
||||
/>
|
||||
|
||||
<div className="landing-page">
|
||||
|
||||
@@ -88,7 +88,7 @@ export function UsersPage() {
|
||||
})
|
||||
const [inviteLoading, setInviteLoading] = 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 fetchAccounts = useCallback(async () => {
|
||||
@@ -469,7 +469,8 @@ export function UsersPage() {
|
||||
<option value="all">All plans</option>
|
||||
<option value="free">Free</option>
|
||||
<option value="pro">Pro</option>
|
||||
<option value="team">Team</option>
|
||||
<option value="enterprise">Enterprise</option>
|
||||
<option value="starter">Starter</option>
|
||||
</select>
|
||||
<select
|
||||
value={statusFilter}
|
||||
@@ -629,7 +630,7 @@ export function UsersPage() {
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Initial Plan</label>
|
||||
<select
|
||||
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(
|
||||
'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'
|
||||
@@ -637,7 +638,8 @@ export function UsersPage() {
|
||||
>
|
||||
<option value="free">Free</option>
|
||||
<option value="pro">Pro</option>
|
||||
<option value="team">Team</option>
|
||||
<option value="enterprise">Enterprise</option>
|
||||
<option value="starter">Starter</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -12,8 +12,9 @@ import type { InviteCodeResponse, InviteCodeCreateRequest } from '@/types/admin'
|
||||
|
||||
const PLAN_OPTIONS = [
|
||||
{ value: 'free', label: 'Free' },
|
||||
{ value: 'starter', label: 'Starter' },
|
||||
{ value: 'pro', label: 'Pro' },
|
||||
{ value: 'team', label: 'Team' },
|
||||
{ value: 'enterprise', label: 'Enterprise' },
|
||||
] as const
|
||||
|
||||
const planBadgeVariant = (plan: string): 'success' | 'destructive' | 'warning' | 'default' => {
|
||||
@@ -33,7 +34,7 @@ export function InviteCodesPage() {
|
||||
// Form state
|
||||
const [email, setEmail] = 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 [note, setNote] = useState('')
|
||||
|
||||
@@ -269,7 +270,7 @@ export function InviteCodesPage() {
|
||||
aria-label="Plan"
|
||||
value={assignedPlan}
|
||||
onChange={(e) => {
|
||||
const plan = e.target.value as 'free' | 'pro' | 'team'
|
||||
const plan = e.target.value as 'free' | 'pro' | 'starter' | 'enterprise'
|
||||
setAssignedPlan(plan)
|
||||
if (plan === 'free') setTrialDays('')
|
||||
}}
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface Account {
|
||||
export interface Subscription {
|
||||
id: string
|
||||
account_id: string
|
||||
plan: 'free' | 'pro' | 'team'
|
||||
plan: 'free' | 'pro' | 'starter' | 'enterprise'
|
||||
status: 'active' | 'past_due' | 'canceled' | 'trialing' | 'orphaned'
|
||||
current_period_start: string | null
|
||||
current_period_end: string | null
|
||||
|
||||
@@ -113,7 +113,7 @@ export interface AdminAccountDetailResponse extends AdminAccountListItem {
|
||||
|
||||
export interface AdminAccountCreate {
|
||||
name: string
|
||||
plan: 'free' | 'pro' | 'team'
|
||||
plan: 'free' | 'pro' | 'starter' | 'enterprise'
|
||||
owner_email?: string
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ export interface InviteCodeCreateRequest {
|
||||
expires_at?: string | null
|
||||
note?: string | null
|
||||
email?: string | null
|
||||
assigned_plan?: 'free' | 'pro' | 'team'
|
||||
assigned_plan?: 'free' | 'pro' | 'starter' | 'enterprise'
|
||||
trial_duration_days?: number | null
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface BillingStateApiResponse {
|
||||
* Checkout / Customer-Portal session types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export type CheckoutPlan = 'starter' | 'pro' | 'team' | 'enterprise'
|
||||
export type CheckoutPlan = 'starter' | 'pro' | 'enterprise'
|
||||
export type BillingInterval = 'monthly' | 'annual'
|
||||
|
||||
export interface CheckoutSessionRequest {
|
||||
|
||||
Reference in New Issue
Block a user