Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
969d5d4c8a
commit
c9968531e4
@@ -5,12 +5,13 @@ import Link from "next/link"
|
||||
import { motion } from "framer-motion"
|
||||
import { Check, X, Zap, ShieldCheck } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { PLAN_AMOUNTS } from "@/lib/stripe/plans"
|
||||
|
||||
const PLANS = [
|
||||
{
|
||||
key: "starter",
|
||||
name: "Starter",
|
||||
monthlyPrice: 0,
|
||||
monthlyPrice: PLAN_AMOUNTS.starter,
|
||||
description: "For landlords just getting started.",
|
||||
highlight: false,
|
||||
cta: "Get started free",
|
||||
@@ -20,7 +21,7 @@ const PLANS = [
|
||||
{
|
||||
key: "pro",
|
||||
name: "Pro",
|
||||
monthlyPrice: 29,
|
||||
monthlyPrice: PLAN_AMOUNTS.pro,
|
||||
description: "For active landlords growing their portfolio.",
|
||||
highlight: true,
|
||||
badge: "Most popular",
|
||||
@@ -31,7 +32,7 @@ const PLANS = [
|
||||
{
|
||||
key: "landlord",
|
||||
name: "Landlord",
|
||||
monthlyPrice: 59,
|
||||
monthlyPrice: PLAN_AMOUNTS.landlord,
|
||||
description: "For serious portfolios at scale.",
|
||||
highlight: false,
|
||||
cta: "Start Landlord",
|
||||
@@ -42,7 +43,7 @@ const PLANS = [
|
||||
key: "lifetime",
|
||||
name: "Lifetime",
|
||||
monthlyPrice: null,
|
||||
fixedPrice: 199,
|
||||
fixedPrice: PLAN_AMOUNTS.lifetime,
|
||||
description: "Pay once, own it forever.",
|
||||
highlight: false,
|
||||
badge: "Best value",
|
||||
@@ -71,7 +72,7 @@ function Cell({ val }: { val: string | boolean }) {
|
||||
return <span className="text-sm font-medium text-white/80">{val}</span>
|
||||
}
|
||||
|
||||
export function PricingSection() {
|
||||
export function PricingSection({ annualEnabled = false }: { annualEnabled?: boolean }) {
|
||||
const [annual, setAnnual] = useState(false)
|
||||
const DISCOUNT = 0.8
|
||||
|
||||
@@ -95,30 +96,34 @@ export function PricingSection() {
|
||||
<h2 className="text-3xl font-bold text-white sm:text-4xl">Simple, honest pricing</h2>
|
||||
<p className="mt-3 text-white/50">Start free, upgrade when you grow. No per-unit fees, ever.</p>
|
||||
|
||||
{/* Toggle */}
|
||||
<div className="mt-8 inline-flex items-center rounded-full border border-white/10 bg-white/[0.04] p-1.5">
|
||||
<button
|
||||
onClick={() => setAnnual(false)}
|
||||
className={cn(
|
||||
"rounded-full px-4 sm:px-5 py-1.5 sm:py-2 text-xs sm:text-sm font-medium transition-all duration-200",
|
||||
!annual ? "bg-indigo-600 text-white shadow-lg shadow-indigo-500/30" : "text-white/50 hover:text-white"
|
||||
)}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setAnnual(true)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 sm:gap-2 rounded-full px-4 sm:px-5 py-1.5 sm:py-2 text-xs sm:text-sm font-medium transition-all duration-200",
|
||||
annual ? "bg-indigo-600 text-white shadow-lg shadow-indigo-500/30" : "text-white/50 hover:text-white"
|
||||
)}
|
||||
>
|
||||
Annual
|
||||
<span className="rounded-full bg-emerald-500/20 px-2 py-0.5 text-[10px] font-bold text-emerald-400 border border-emerald-500/30">
|
||||
–20%
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* Toggle — only shown when annual billing is actually purchasable.
|
||||
When annual isn't configured we stay monthly-only rather than
|
||||
advertising a plan that can't be bought. */}
|
||||
{annualEnabled && (
|
||||
<div className="mt-8 inline-flex items-center rounded-full border border-white/10 bg-white/[0.04] p-1.5">
|
||||
<button
|
||||
onClick={() => setAnnual(false)}
|
||||
className={cn(
|
||||
"rounded-full px-4 sm:px-5 py-1.5 sm:py-2 text-xs sm:text-sm font-medium transition-all duration-200",
|
||||
!annual ? "bg-indigo-600 text-white shadow-lg shadow-indigo-500/30" : "text-white/50 hover:text-white"
|
||||
)}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setAnnual(true)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 sm:gap-2 rounded-full px-4 sm:px-5 py-1.5 sm:py-2 text-xs sm:text-sm font-medium transition-all duration-200",
|
||||
annual ? "bg-indigo-600 text-white shadow-lg shadow-indigo-500/30" : "text-white/50 hover:text-white"
|
||||
)}
|
||||
>
|
||||
Annual
|
||||
<span className="rounded-full bg-emerald-500/20 px-2 py-0.5 text-[10px] font-bold text-emerald-400 border border-emerald-500/30">
|
||||
–20%
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Plan cards */}
|
||||
|
||||
Reference in New Issue
Block a user