mirror of
https://github.com/hanzoai/dataroom.git
synced 2026-11-02 16:02:50 +00:00
Drop the dual-licensed `ee/` tree entirely. All features previously
locked behind the Hanzo Dataroom Commercial License are now part of the
AGPL-3.0 core and freely available — custom domains, branded viewing,
advanced analytics, watermarks, access controls, Q&A, AI vector stores,
workflows, SAML SSO, dataroom invitations, conversations, templates.
Layout changes:
* `ee/features/{ai,conversations,workflows,permissions,storage,templates,
dataroom-invitations,access-notifications,security,conversions}/` →
`features/<name>/` (AGPL).
* `ee/limits/` → `lib/billing/limits/` with every plan resolving to an
unlimited profile (no users/links/documents/domains/datarooms cap,
conversations and watermarks always on).
* `ee/stripe/` → `lib/billing/legacy/` (already a compat shim over
`@hanzoai/commerce`; renewal-reminder webhook is now a no-op).
* `ee/features/billing/cancellation/` deleted. Pause/unpause/cancel/
retention routes now compile against `lib/billing/cancellation.ts`
stubs that return 410 Gone; `isTeamPaused`/`isTeamPausedById` in
`lib/billing/paused.ts` always report active. `CancellationModal` is
a no-op shim in `components/billing/`.
* `app/(ee)/api/*` route group folded into `app/api/*` (SAML auth,
SCIM, AI chat, workflows, FAQ, link-upload).
Build: `npm run build` green.
200 lines
5.9 KiB
TypeScript
200 lines
5.9 KiB
TypeScript
import Link from "next/link";
|
||
import { useRouter } from "next/router";
|
||
|
||
import { useEffect, useState } from "react";
|
||
|
||
import { useTeam } from "@/context/team-context";
|
||
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
|
||
import { getQuantityFromPriceId } from "@/lib/billing/legacy/functions/get-quantity-from-plan";
|
||
import { toast } from "sonner";
|
||
|
||
import { useAnalytics } from "@/lib/analytics";
|
||
import { usePlan } from "@/lib/swr/use-billing";
|
||
import useLimits from "@/lib/swr/use-limits";
|
||
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogTrigger,
|
||
} from "@/components/ui/dialog";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
|
||
export function AddSeatModal({
|
||
open,
|
||
setOpen,
|
||
children,
|
||
}: {
|
||
open: boolean;
|
||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||
children?: React.ReactNode;
|
||
}) {
|
||
const router = useRouter();
|
||
const teamInfo = useTeam();
|
||
const teamId = teamInfo?.currentTeam?.id;
|
||
const analytics = useAnalytics();
|
||
const { plan: userPlan, planName, isAnnualPlan, isOldAccount } = usePlan();
|
||
const { limits } = useLimits();
|
||
|
||
const [quantity, setQuantity] = useState<number>(1);
|
||
const [loading, setLoading] = useState<boolean>(false);
|
||
|
||
// Get the minimum quantity for the current plan
|
||
const priceId = getPriceIdFromPlan({
|
||
planSlug: userPlan,
|
||
isOld: isOldAccount,
|
||
period: isAnnualPlan ? "yearly" : "monthly",
|
||
});
|
||
const minQuantity = getQuantityFromPriceId(priceId);
|
||
|
||
// Set initial quantity to 1 (adding one seat)
|
||
useEffect(() => {
|
||
if (open) {
|
||
setQuantity(1);
|
||
}
|
||
}, [open]);
|
||
|
||
// Calculate the total number of seats after the update
|
||
const totalSeatsAfterUpdate = limits ? limits.users! + quantity : quantity;
|
||
|
||
const handleDecrement = () => {
|
||
if (quantity > 1) {
|
||
setQuantity(quantity - 1);
|
||
}
|
||
};
|
||
|
||
const handleIncrement = () => {
|
||
setQuantity(quantity + 1);
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
setLoading(true);
|
||
|
||
try {
|
||
const response = await fetch(`/api/teams/${teamId}/billing/manage`, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
priceId,
|
||
quantity: totalSeatsAfterUpdate,
|
||
addSeat: true,
|
||
// return_url: `${process.env.NEXTAUTH_URL}/settings/people?success=true`,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
toast.error("Unable to add seats. Please contact support.");
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
const url = await response.json();
|
||
|
||
analytics.capture("Add Seat Clicked", {
|
||
quantity: quantity,
|
||
totalSeats: totalSeatsAfterUpdate,
|
||
teamId,
|
||
plan: userPlan,
|
||
});
|
||
|
||
router.push(url);
|
||
} catch (error) {
|
||
toast.error("An error occurred while processing your request.");
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={setOpen}>
|
||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||
<DialogContent className="sm:max-w-[425px]">
|
||
<DialogHeader className="text-start">
|
||
<DialogTitle>Add more seats</DialogTitle>
|
||
<DialogDescription>{planName}</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="py-6">
|
||
<div className="flex items-center justify-center space-x-4">
|
||
<Button
|
||
variant="outline"
|
||
size="icon"
|
||
onClick={handleDecrement}
|
||
disabled={quantity <= 1}
|
||
className="h-10 w-10 rounded-full"
|
||
>
|
||
<span className="text-xl">−</span>
|
||
</Button>
|
||
|
||
<div className="w-24">
|
||
<Input
|
||
type="number"
|
||
value={quantity}
|
||
onChange={(e) => {
|
||
const value = parseInt(e.target.value);
|
||
if (!isNaN(value) && value >= 1) {
|
||
setQuantity(value);
|
||
}
|
||
}}
|
||
className="text-center text-lg"
|
||
min={1}
|
||
/>
|
||
</div>
|
||
|
||
<Button
|
||
variant="outline"
|
||
size="icon"
|
||
onClick={handleIncrement}
|
||
className="h-10 w-10 rounded-full"
|
||
>
|
||
<span className="text-xl">+</span>
|
||
</Button>
|
||
</div>
|
||
|
||
{limits && (
|
||
<p className="mt-4 text-center text-sm text-muted-foreground">
|
||
Current limit: {limits.users}{" "}
|
||
{limits.users === 1 ? "user" : "users"}
|
||
</p>
|
||
)}
|
||
|
||
<p className="mt-2 text-center text-sm">
|
||
Adding <span className="font-semibold">{quantity}</span>{" "}
|
||
{quantity === 1 ? "seat" : "seats"}
|
||
</p>
|
||
|
||
<p className="mt-2 text-center text-sm font-medium">
|
||
Total after update: {totalSeatsAfterUpdate}{" "}
|
||
{totalSeatsAfterUpdate === 1 ? "user" : "users"}
|
||
</p>
|
||
|
||
{minQuantity > 1 && (
|
||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
||
Minimum quantity for {planName}: {minQuantity} users
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<DialogFooter className="flex flex-col gap-2 sm:flex-col sm:justify-center">
|
||
<Button onClick={handleSubmit} className="w-full" disabled={loading}>
|
||
{loading ? "Redirecting..." : "Proceed to checkout"}
|
||
</Button>
|
||
<Link
|
||
href="/settings/upgrade"
|
||
className="block w-full text-center text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||
onClick={() => setOpen(false)}
|
||
>
|
||
or upgrade to higher plan
|
||
</Link>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|