diff --git a/app/api/stripe/checkout/route.ts b/app/api/stripe/checkout/route.ts
index 457a09a..8cf3084 100644
--- a/app/api/stripe/checkout/route.ts
+++ b/app/api/stripe/checkout/route.ts
@@ -5,6 +5,7 @@ import { logAuditEvent } from '@/lib/audit-log';
import { db } from '@/lib/db';
import { getConfiguredAppBaseUrl } from '@/lib/env.server';
import { getCorrelationIdFromRequest, reportApplicationError, withCorrelationIdHeader } from '@/lib/observability';
+import { isPortfolioDemoMode, PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE } from '@/lib/portfolio-demo';
import { isAllowedRequestOrigin } from '@/lib/request-origin';
import { getStripe } from '@/lib/stripe';
import { absoluteUrl } from '@/lib/url';
@@ -24,6 +25,10 @@ export async function POST(request: Request) {
if (process.env.NODE_ENV === 'production' && !isAllowedRequestOrigin(request, getConfiguredAppBaseUrl())) {
return withCorrelation(NextResponse.json({ error: 'Invalid request origin' }, { status: 403 }));
}
+ if (isPortfolioDemoMode()) {
+ return withCorrelation(errorRedirect(PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE));
+ }
+
const { userId } = await auth();
if (!userId) {
return withCorrelation(NextResponse.redirect(absoluteUrl('/sign-in'), { status: 303 }));
diff --git a/app/api/stripe/portal/route.ts b/app/api/stripe/portal/route.ts
index 044f14f..3142ef7 100644
--- a/app/api/stripe/portal/route.ts
+++ b/app/api/stripe/portal/route.ts
@@ -5,6 +5,7 @@ import { logAuditEvent } from '@/lib/audit-log';
import { db } from '@/lib/db';
import { getConfiguredAppBaseUrl } from '@/lib/env.server';
import { getCorrelationIdFromRequest, reportApplicationError, withCorrelationIdHeader } from '@/lib/observability';
+import { isPortfolioDemoMode, PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE } from '@/lib/portfolio-demo';
import { isAllowedRequestOrigin } from '@/lib/request-origin';
import { getStripe } from '@/lib/stripe';
import { absoluteUrl } from '@/lib/url';
@@ -19,6 +20,12 @@ export async function POST(request: Request) {
if (process.env.NODE_ENV === 'production' && !isAllowedRequestOrigin(request, getConfiguredAppBaseUrl())) {
return withCorrelation(NextResponse.json({ error: 'Invalid request origin' }, { status: 403 }));
}
+ if (isPortfolioDemoMode()) {
+ return withCorrelation(
+ NextResponse.redirect(absoluteUrl(`/app/billing?error=${encodeURIComponent(PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE)}`), { status: 303 })
+ );
+ }
+
const { userId } = await auth();
if (!userId) {
return withCorrelation(NextResponse.redirect(absoluteUrl('/sign-in'), { status: 303 }));
diff --git a/app/app/billing/page.tsx b/app/app/billing/page.tsx
index 5eea9ea..47f382f 100644
--- a/app/app/billing/page.tsx
+++ b/app/app/billing/page.tsx
@@ -7,7 +7,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { requireBusiness } from '@/lib/auth';
import { getBillingUsageSnapshotForBusiness } from '@/lib/business-access';
import { db } from '@/lib/db';
-import { getPortfolioDemoBlockedCount, isPortfolioDemoMode } from '@/lib/portfolio-demo';
+import { getPortfolioDemoBlockedCount, isPortfolioDemoMode, PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE } from '@/lib/portfolio-demo';
import { getBusinessBillingAccessState } from '@/lib/subscription';
import { getBillingDisplayLabel } from '@/lib/system-status';
import { getStripe } from '@/lib/stripe';
@@ -212,6 +212,7 @@ export default async function BillingPage({ searchParams }: { searchParams?: Rec
{error ?
{error}
: null}
+ {demoMode ? {PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE}
: null}
{requestedPlan ? (
Selected plan:
{requestedPlan === 'starter' ? 'Starter' : 'Growth'}. Continue with the checkout card below.
@@ -250,7 +251,9 @@ export default async function BillingPage({ searchParams }: { searchParams?: Rec
{business.stripeCustomerId ? (
) : (
@@ -332,7 +335,12 @@ export default async function BillingPage({ searchParams }: { searchParams?: Rec
@@ -351,7 +359,12 @@ export default async function BillingPage({ searchParams }: { searchParams?: Rec
@@ -372,7 +385,13 @@ export default async function BillingPage({ searchParams }: { searchParams?: Rec
{business.stripeCustomerId ? (
diff --git a/app/app/leads/[leadId]/page.tsx b/app/app/leads/[leadId]/page.tsx
index 8dd94d8..8337848 100644
--- a/app/app/leads/[leadId]/page.tsx
+++ b/app/app/leads/[leadId]/page.tsx
@@ -21,7 +21,7 @@ import {
smsStateLabels,
} from '@/lib/lead-presenters';
import { formatPhoneForDisplay } from '@/lib/phone';
-import { getPortfolioDemoLeadDetail, isPortfolioDemoMode } from '@/lib/portfolio-demo';
+import { getPortfolioDemoLeadDetail, isPortfolioDemoMode, PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE } from '@/lib/portfolio-demo';
function resolveSafeReturnPath(value: string | null | undefined) {
if (!value) return '/app/leads';
@@ -55,6 +55,7 @@ function LeadStatusActionForm({
successRedirectTo,
label,
variant,
+ disabled = false,
}: {
leadId: string;
status: 'CONTACTED' | 'BOOKED' | 'LOST';
@@ -62,7 +63,16 @@ function LeadStatusActionForm({
successRedirectTo: string;
label: string;
variant?: 'default' | 'outline' | 'destructive';
+ disabled?: boolean;
}) {
+ if (disabled) {
+ return (
+
+ );
+ }
+
return (
diff --git a/app/app/leads/actions.ts b/app/app/leads/actions.ts
index 4622286..75d0463 100644
--- a/app/app/leads/actions.ts
+++ b/app/app/leads/actions.ts
@@ -6,6 +6,7 @@ import { redirect } from 'next/navigation';
import { requireBusiness } from '@/lib/auth';
import { updateLeadStatusForBusiness } from '@/lib/business-access';
+import { isPortfolioDemoMode, PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE } from '@/lib/portfolio-demo';
import { leadStatusSchema } from '@/lib/validators';
function resolveSafeAppRedirect(value: FormDataEntryValue | null, fallback: string) {
@@ -20,6 +21,10 @@ export async function updateLeadStatusAction(formData: FormData) {
const redirectTo = resolveSafeAppRedirect(formData.get('redirectTo'), fallbackPath);
const successRedirectTo = resolveSafeAppRedirect(formData.get('successRedirectTo'), redirectTo);
+ if (isPortfolioDemoMode()) {
+ redirect(`${redirectTo}${redirectTo.includes('?') ? '&' : '?'}error=${encodeURIComponent(PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE)}`);
+ }
+
const business = await requireBusiness();
const parsed = leadStatusSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
diff --git a/app/app/settings/actions.ts b/app/app/settings/actions.ts
index 0be9874..f901c7f 100644
--- a/app/app/settings/actions.ts
+++ b/app/app/settings/actions.ts
@@ -25,6 +25,7 @@ import { averageJobValueDollarsToCents } from '@/lib/business-settings';
import { db } from '@/lib/db';
import { formatPhoneDetail, maskSid, recordBusinessOperatorEvent } from '@/lib/operator-events';
import { maskPhoneForAudit, normalizePhoneNumber, normalizePhoneNumberToE164 } from '@/lib/phone';
+import { isPortfolioDemoMode, PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE } from '@/lib/portfolio-demo';
import {
getBusinessTwilioSaveErrorMessage,
getMessagingComplianceSidValidationError,
@@ -87,6 +88,10 @@ function redirectToSettingsError(message: string): never {
}
export async function saveBusinessSettingsAction(formData: FormData) {
+ if (isPortfolioDemoMode()) {
+ redirect(`/app/settings?error=${encodeURIComponent(PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE)}`);
+ }
+
const business = await getBusinessForOwner();
const parsed = businessSettingsSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
diff --git a/app/app/settings/page.tsx b/app/app/settings/page.tsx
index 5e11494..17a78e1 100644
--- a/app/app/settings/page.tsx
+++ b/app/app/settings/page.tsx
@@ -11,7 +11,7 @@ import { getBusinessNotificationSettingsForBusiness } from '@/lib/business-acces
import { getBusinessRoutingNumber, getPublicBusinessPhone } from '@/lib/business-phone-setup';
import { db } from '@/lib/db';
import { formatPhoneForDisplay } from '@/lib/phone';
-import { isPortfolioDemoMode } from '@/lib/portfolio-demo';
+import { isPortfolioDemoMode, PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE } from '@/lib/portfolio-demo';
import { getCustomerSystemStatus } from '@/lib/system-status';
import { saveBusinessSettingsAction } from './actions';
@@ -133,6 +133,7 @@ export default async function SettingsPage({ searchParams }: { searchParams?: Re
{error ?
{error}
: null}
{saved ?
Business settings saved.
: null}
+ {demoMode ?
{PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE}
: null}
{statusCards.map((item) => (
@@ -157,6 +158,7 @@ export default async function SettingsPage({ searchParams }: { searchParams?: Re
-
+
+
diff --git a/components/home-dashboard.tsx b/components/home-dashboard.tsx
index 238b00f..c7ec30b 100644
--- a/components/home-dashboard.tsx
+++ b/components/home-dashboard.tsx
@@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge';
import { Button, buttonVariants } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { getLeadStatusBadgeVariant, leadStatusLabels } from '@/lib/lead-presenters';
+import { PORTFOLIO_DEMO_ACTIONS_DISABLED_MESSAGE } from '@/lib/portfolio-demo';
import { cn } from '@/lib/utils';
type DashboardFeedback = {
@@ -48,13 +49,23 @@ function LeadStatusActionForm({
redirectTo,
label,
variant,
+ disabled = false,
}: {
leadId: string;
status: 'CONTACTED' | 'BOOKED' | 'LOST';
redirectTo: string;
label: string;
variant?: 'default' | 'outline' | 'destructive';
+ disabled?: boolean;
}) {
+ if (disabled) {
+ return (
+
+ );
+ }
+
return (