Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions app/api/entitlements/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
extractErrorMessage,
isRateLimitError,
} from "@/lib/api/response";
import type { SubscriptionTier } from "@/types";

const workos = new WorkOS(process.env.WORKOS_API_KEY!, {
clientId: process.env.WORKOS_CLIENT_ID!,
Expand Down Expand Up @@ -79,18 +80,24 @@ export async function GET(req: NextRequest) {
allEntitlements.includes("ultra-monthly-plan") ||
allEntitlements.includes("ultra-yearly-plan");
const hasTeam = allEntitlements.includes("team-plan");
const hasProPlus =
allEntitlements.includes("pro-plus-plan") ||
allEntitlements.includes("pro-plus-monthly-plan") ||
allEntitlements.includes("pro-plus-yearly-plan");
const hasPro =
allEntitlements.includes("pro-plan") ||
allEntitlements.includes("pro-monthly-plan") ||
allEntitlements.includes("pro-yearly-plan");

const subscription: "free" | "pro" | "ultra" | "team" = hasUltra
const subscription: SubscriptionTier = hasUltra
? "ultra"
: hasTeam
? "team"
: hasPro
? "pro"
: "free";
: hasProPlus
? "pro-plus"
: hasPro
? "pro"
: "free";

// Create response with entitlements and normalized subscription tier
const response = json({
Expand Down
4 changes: 4 additions & 0 deletions app/api/subscribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ export const POST = async (req: NextRequest) => {
const orgName = user.email;
const allowedPlans = new Set([
"pro-monthly-plan",
"pro-plus-monthly-plan",
"ultra-monthly-plan",
"pro-yearly-plan",
"pro-plus-yearly-plan",
"ultra-yearly-plan",
"team-monthly-plan",
"team-yearly-plan",
Expand All @@ -26,8 +28,10 @@ export const POST = async (req: NextRequest) => {
typeof requestedPlan === "string" && allowedPlans.has(requestedPlan)
? (requestedPlan as
| "pro-monthly-plan"
| "pro-plus-monthly-plan"
| "ultra-monthly-plan"
| "pro-yearly-plan"
| "pro-plus-yearly-plan"
| "ultra-yearly-plan"
| "team-monthly-plan"
| "team-yearly-plan")
Expand Down
8 changes: 7 additions & 1 deletion app/api/subscription-details/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { stripe } from "../stripe";
import { workos } from "../workos";
import { getUserID } from "@/lib/auth/get-user-id";
import { NextRequest, NextResponse } from "next/server";
import { SubscriptionTier } from "@/types/chat";

export const POST = async (req: NextRequest) => {
try {
Expand Down Expand Up @@ -94,7 +95,7 @@ export const POST = async (req: NextRequest) => {
let totalDue = targetAmount * quantity;
let additionalCredit = 0; // credit left over to be added to customer balance
let paymentMethodInfo = "";
let planType: "free" | "pro" | "ultra" | "team" = "free";
let planType: SubscriptionTier = "free";
let interval: "monthly" | "yearly" = "monthly";
let currentPeriodStart: number | null = null; // unix seconds
let currentPeriodEnd: number | null = null; // unix seconds
Expand Down Expand Up @@ -127,6 +128,11 @@ export const POST = async (req: NextRequest) => {
productMetadata.plan === "team"
)
planType = "team";
else if (
productName.includes("pro-plus") ||
productMetadata.plan === "pro-plus"
)
planType = "pro-plus";
else if (
productName.includes("pro") ||
productMetadata.plan === "pro"
Expand Down
28 changes: 19 additions & 9 deletions app/components/AccountTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { usePentestgptMigration } from "@/app/hooks/usePentestgptMigration";
import { X, ChevronDown, Sparkle } from "lucide-react";
import {
proFeatures,
proPlusFeatures,
ultraFeatures,
teamFeatures,
} from "@/lib/pricing/features";
Expand All @@ -38,15 +39,20 @@ const AccountTab = () => {
}
}, [subscription]);

// For individual plans (pro/ultra), user always has billing access
// For individual plans (pro/pro-plus/ultra), user always has billing access
// For team plans, only admins can manage billing
const canManageBilling =
subscription === "pro" ||
subscription === "pro-plus" ||
subscription === "ultra" ||
(subscription === "team" && isTeamAdmin === true);

const currentPlanFeatures =
subscription === "team" ? teamFeatures : proFeatures;
subscription === "team"
? teamFeatures
: subscription === "pro-plus"
? proPlusFeatures
: proFeatures;

const redirectToBillingPortal = async () => {
try {
Expand Down Expand Up @@ -82,9 +88,11 @@ const AccountTab = () => {
? "HackerAI Ultra"
: subscription === "team"
? "HackerAI Team"
: subscription === "pro"
? "HackerAI Pro"
: "Get HackerAI Pro"}
: subscription === "pro-plus"
? "HackerAI Pro+"
: subscription === "pro"
? "HackerAI Pro"
: "Get HackerAI Pro"}
</div>
</div>
{subscription !== "free" ? (
Expand All @@ -97,7 +105,7 @@ const AccountTab = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{subscription === "pro" && (
{(subscription === "pro" || subscription === "pro-plus") && (
<>
<DropdownMenuItem onClick={redirectToPricing}>
<Sparkle className="h-4 w-4" />
Expand Down Expand Up @@ -134,9 +142,11 @@ const AccountTab = () => {
? "Thanks for subscribing to Ultra! Your plan includes everything in Pro, plus:"
: subscription === "team"
? "Thanks for subscribing to Team! Your plan includes:"
: subscription !== "free"
? "Thanks for subscribing to Pro! Your plan includes:"
: "Get everything in Free, and more."}
: subscription === "pro-plus"
? "Thanks for subscribing to Pro+! Your plan includes everything in Pro, plus:"
: subscription === "pro"
? "Thanks for subscribing to Pro! Your plan includes:"
: "Get everything in Free, and more."}
</span>
<ul className="mb-2 flex flex-col gap-5">
{(subscription === "ultra"
Expand Down
76 changes: 74 additions & 2 deletions app/components/PricingDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { navigateToAuth } from "../hooks/useTauri";
import {
freeFeatures,
proFeatures,
proPlusFeatures,
ultraFeatures,
teamFeatures,
PRICING,
Expand Down Expand Up @@ -156,7 +157,7 @@ const PricingDialog: React.FC<PricingDialogProps> = ({ isOpen, onClose }) => {
price: number;
} | null>(null);

// Auto-close pricing dialog for ultra/team users
// Auto-close pricing dialog for ultra/team users (pro-plus can still upgrade to ultra)
React.useEffect(() => {
if (isOpen && (subscription === "ultra" || subscription === "team")) {
onClose();
Expand All @@ -170,8 +171,10 @@ const PricingDialog: React.FC<PricingDialogProps> = ({ isOpen, onClose }) => {
const handleUpgradeClick = async (
plan:
| "pro-monthly-plan"
| "pro-plus-monthly-plan"
| "ultra-monthly-plan"
| "pro-yearly-plan"
| "pro-plus-yearly-plan"
| "ultra-yearly-plan" = "pro-monthly-plan",
planName: string,
price: number,
Expand Down Expand Up @@ -248,6 +251,14 @@ const PricingDialog: React.FC<PricingDialogProps> = ({ isOpen, onClose }) => {
className: "opacity-50 cursor-not-allowed",
variant: "secondary" as const,
};
} else if (user && subscription === "pro-plus") {
// Pro+ users can't downgrade to Pro
return {
text: "Pro",
disabled: true,
className: "opacity-50 cursor-not-allowed",
variant: "secondary" as const,
};
} else if (user) {
return {
text: "Get Pro",
Expand All @@ -273,6 +284,42 @@ const PricingDialog: React.FC<PricingDialogProps> = ({ isOpen, onClose }) => {
}
};

// Button configurations for Pro+ plan
const getProPlusButtonConfig = () => {
if (user && !isCheckingProPlan && subscription === "pro-plus") {
return {
text: "Current Plan",
disabled: true,
className: "opacity-50 cursor-not-allowed",
variant: "secondary" as const,
};
} else if (user) {
const buttonText =
subscription === "pro" ? "Upgrade to Pro+" : "Get Pro+";
return {
text: buttonText,
disabled: upgradeLoading,
className: "font-semibold bg-[#615eeb] hover:bg-[#504bb8] text-white",
variant: "default" as const,
onClick: () =>
handleUpgradeClick(
isYearly ? "pro-plus-yearly-plan" : "pro-plus-monthly-plan",
"Pro+",
isYearly ? PRICING["pro-plus"].yearly : PRICING["pro-plus"].monthly,
),
loading: upgradeLoading,
};
} else {
return {
text: "Get Pro+",
disabled: false,
className: "font-semibold bg-[#615eeb] hover:bg-[#504bb8] text-white",
variant: "default" as const,
onClick: () => navigateToAuth("/login"),
};
}
};

// Button configurations for Ultra plan
const getUltraButtonConfig = () => {
if (user && !isCheckingProPlan && subscription === "ultra") {
Expand All @@ -284,7 +331,10 @@ const PricingDialog: React.FC<PricingDialogProps> = ({ isOpen, onClose }) => {
};
} else if (user) {
return {
text: subscription === "pro" ? "Upgrade to Ultra" : "Get Ultra",
text:
subscription === "pro" || subscription === "pro-plus"
? "Upgrade to Ultra"
: "Get Ultra",
disabled: upgradeLoading,
className: "",
variant: "default" as const,
Expand All @@ -309,6 +359,7 @@ const PricingDialog: React.FC<PricingDialogProps> = ({ isOpen, onClose }) => {

const freeButtonConfig = getFreeButtonConfig();
const proButtonConfig = getProButtonConfig();
const proPlusButtonConfig = getProPlusButtonConfig();
const ultraButtonConfig = getUltraButtonConfig();

const hasSubscription = subscription !== "free";
Expand Down Expand Up @@ -433,6 +484,27 @@ const PricingDialog: React.FC<PricingDialogProps> = ({ isOpen, onClose }) => {
featureHeader={PLAN_HEADERS.pro}
/>

{/* Pro+ Plan */}
<PlanCard
planName="Pro+"
price={
isYearly
? PRICING["pro-plus"].yearly
: PRICING["pro-plus"].monthly
}
description="For power users who need more"
features={proPlusFeatures}
buttonText={proPlusButtonConfig.text}
buttonVariant={proPlusButtonConfig.variant}
buttonClassName={proPlusButtonConfig.className}
onButtonClick={proPlusButtonConfig.onClick}
isButtonDisabled={proPlusButtonConfig.disabled}
isButtonLoading={proPlusButtonConfig.loading}
customClassName="border-[#CFCEFC] bg-[#F5F5FF] dark:bg-[#282841] dark:border-[#484777]"
badgeText="RECOMMENDED"
featureHeader={PLAN_HEADERS["pro-plus"]}
/>

{/* Ultra Plan */}
<PlanCard
planName="Ultra"
Expand Down
10 changes: 6 additions & 4 deletions app/components/SidebarUserNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,11 @@ const SidebarUserNav = ({ isCollapsed = false }: { isCollapsed?: boolean }) => {
? "Ultra"
: subscription === "team"
? "Team"
: subscription === "pro"
? "Pro"
: "Free"}
: subscription === "pro-plus"
? "Pro+"
: subscription === "pro"
? "Pro"
: "Free"}
</div>
</div>
</button>
Expand All @@ -272,7 +274,7 @@ const SidebarUserNav = ({ isCollapsed = false }: { isCollapsed?: boolean }) => {

<DropdownMenuSeparator />

{subscription === "pro" && (
{(subscription === "pro" || subscription === "pro-plus") && (
<DropdownMenuItem
data-testid="upgrade-menu-item"
onClick={redirectToPricing}
Expand Down
19 changes: 17 additions & 2 deletions app/contexts/GlobalState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,24 @@ export const GlobalStateProvider: React.FC<GlobalStateProviderProps> = ({
entitlements.includes("ultra-monthly-plan") ||
entitlements.includes("ultra-yearly-plan");
const hasTeam = entitlements.includes("team-plan");
const hasProPlus =
entitlements.includes("pro-plus-plan") ||
entitlements.includes("pro-plus-monthly-plan") ||
entitlements.includes("pro-plus-yearly-plan");
const hasPro =
entitlements.includes("pro-plan") ||
entitlements.includes("pro-monthly-plan") ||
entitlements.includes("pro-yearly-plan");
setSubscription(
hasUltra ? "ultra" : hasTeam ? "team" : hasPro ? "pro" : "free",
hasUltra
? "ultra"
: hasTeam
? "team"
: hasProPlus
? "pro-plus"
: hasPro
? "pro"
: "free",
);
}
}, [user, entitlements]);
Expand Down Expand Up @@ -372,7 +384,10 @@ export const GlobalStateProvider: React.FC<GlobalStateProviderProps> = ({
const data = await response.json();
const tier = data.subscription as SubscriptionTier | undefined;
setSubscription(
tier === "ultra" || tier === "team" || tier === "pro"
tier === "ultra" ||
tier === "team" ||
tier === "pro-plus" ||
tier === "pro"
? tier
: "free",
);
Expand Down
4 changes: 3 additions & 1 deletion app/hooks/useUpgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ export const useUpgrade = () => {
const handleUpgrade = async (
planKey?:
| "pro-monthly-plan"
| "pro-plus-monthly-plan"
| "ultra-monthly-plan"
| "pro-yearly-plan"
| "pro-plus-yearly-plan"
| "ultra-yearly-plan"
| "team-monthly-plan"
| "team-yearly-plan",
e?: React.MouseEvent<HTMLButtonElement | HTMLDivElement>,
quantity?: number,
currentSubscription?: "free" | "pro" | "ultra" | "team",
currentSubscription?: "free" | "pro" | "pro-plus" | "ultra" | "team",
) => {
e?.preventDefault();

Expand Down
10 changes: 0 additions & 10 deletions convex/__tests__/s3Actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ jest.mock("../chats", () => ({
validateServiceKey: jest.fn(),
}));

// Mock fileStorage module to avoid aggregate import issues
jest.mock("../fileStorage", () => ({
getFileLimit: jest.fn((entitlements: string[]) => {
if (entitlements.includes("ultra-plan")) return 1000;
if (entitlements.includes("team-plan")) return 500;
if (entitlements.includes("pro-plan")) return 300;
return 0;
}),
}));

// Mock fileActions module for rate limiting
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockCheckFileUploadRateLimit = jest.fn<any>();
Expand Down
Loading