Skip to content

Commit 56f0c74

Browse files
committed
Implements new seats purchase logic and UI
1 parent 2e28389 commit 56f0c74

File tree

5 files changed

+594
-43
lines changed

5 files changed

+594
-43
lines changed

apps/webapp/app/presenters/TeamPresenter.server.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getTeamMembersAndInvites } from "~/models/member.server";
2-
import { getLimit } from "~/services/platform.v3.server";
2+
import { getCurrentPlan, getLimit, getPlans } from "~/services/platform.v3.server";
33
import { BasePresenter } from "./v3/basePresenter.server";
44

55
export class TeamPresenter extends BasePresenter {
@@ -13,14 +13,31 @@ export class TeamPresenter extends BasePresenter {
1313
return;
1414
}
1515

16-
const limit = await getLimit(organizationId, "teamMembers", 100_000_000);
16+
const [baseLimit, currentPlan, plans] = await Promise.all([
17+
getLimit(organizationId, "teamMembers", 100_000_000),
18+
getCurrentPlan(organizationId),
19+
getPlans(),
20+
]);
21+
22+
const canPurchaseSeats =
23+
currentPlan?.v3Subscription?.plan?.limits.teamMembers.canExceed === true;
24+
const extraSeats = currentPlan?.v3Subscription?.addOns?.seats?.purchased ?? 0;
25+
const maxSeatQuota = currentPlan?.v3Subscription?.addOns?.seats?.quota ?? 0;
26+
const planSeatLimit = currentPlan?.v3Subscription?.plan?.limits.teamMembers.number ?? 0;
27+
const seatPricing = plans?.addOnPricing.seats ?? null;
28+
const limit = baseLimit + extraSeats;
1729

1830
return {
1931
...result,
2032
limits: {
2133
used: result.members.length + result.invites.length,
2234
limit,
2335
},
36+
canPurchaseSeats,
37+
extraSeats,
38+
seatPricing,
39+
maxSeatQuota,
40+
planSeatLimit,
2441
};
2542
}
2643
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { conform, list, requestIntent, useFieldList, useForm } from "@conform-to/react";
22
import { parse } from "@conform-to/zod";
3-
import { EnvelopeIcon, LockOpenIcon, UserPlusIcon } from "@heroicons/react/20/solid";
3+
import {
4+
ArrowUpCircleIcon,
5+
EnvelopeIcon,
6+
LockOpenIcon,
7+
UserPlusIcon,
8+
} from "@heroicons/react/20/solid";
49
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node";
510
import { json } from "@remix-run/node";
611
import { Form, useActionData } from "@remix-run/react";
@@ -28,7 +33,13 @@ import { redirectWithSuccessMessage } from "~/models/message.server";
2833
import { TeamPresenter } from "~/presenters/TeamPresenter.server";
2934
import { scheduleEmail } from "~/services/email.server";
3035
import { requireUserId } from "~/services/session.server";
31-
import { acceptInvitePath, organizationTeamPath, v3BillingPath } from "~/utils/pathBuilder";
36+
import {
37+
acceptInvitePath,
38+
inviteTeamMemberPath,
39+
organizationTeamPath,
40+
v3BillingPath,
41+
} from "~/utils/pathBuilder";
42+
import { PurchaseSeatsModal } from "../_app.orgs.$organizationSlug.settings.team/route";
3243

3344
const Params = z.object({
3445
organizationSlug: z.string(),
@@ -122,7 +133,8 @@ export const action: ActionFunction = async ({ request, params }) => {
122133
};
123134

124135
export default function Page() {
125-
const { limits } = useTypedLoaderData<typeof loader>();
136+
const { limits, canPurchaseSeats, seatPricing, extraSeats, maxSeatQuota, planSeatLimit } =
137+
useTypedLoaderData<typeof loader>();
126138
const [total, setTotal] = useState(limits.used);
127139
const organization = useOrganization();
128140
const lastSubmission = useActionData();
@@ -150,25 +162,55 @@ export default function Page() {
150162
title="Invite team members"
151163
description={`Invite new team members to ${organization.title}.`}
152164
/>
153-
{total > limits.limit && (
154-
<InfoPanel
155-
variant="upgrade"
156-
icon={LockOpenIcon}
157-
iconClassName="text-indigo-500"
158-
title="Unlock more team members"
159-
accessory={
160-
<LinkButton to={v3BillingPath(organization)} variant="secondary/small">
161-
Upgrade
162-
</LinkButton>
163-
}
164-
panelClassName="mb-4"
165-
>
166-
<Paragraph variant="small">
167-
You've used all {limits.limit} of your available team members. Upgrade your plan to
168-
add more.
169-
</Paragraph>
170-
</InfoPanel>
171-
)}
165+
{total > limits.limit &&
166+
(canPurchaseSeats && seatPricing ? (
167+
<InfoPanel
168+
variant="upgrade"
169+
icon={LockOpenIcon}
170+
iconClassName="text-indigo-500"
171+
title="Need more seats?"
172+
accessory={
173+
<PurchaseSeatsModal
174+
seatPricing={seatPricing}
175+
extraSeats={extraSeats}
176+
usedSeats={limits.used}
177+
maxQuota={maxSeatQuota}
178+
planSeatLimit={planSeatLimit}
179+
triggerButton={<Button variant="primary/small">Purchase more seats…</Button>}
180+
redirectTo={inviteTeamMemberPath(organization)}
181+
/>
182+
}
183+
panelClassName="mb-4"
184+
>
185+
<Paragraph variant="small">
186+
You've used all {limits.limit} of your available team members. Purchase extra seats
187+
to add more.
188+
</Paragraph>
189+
</InfoPanel>
190+
) : (
191+
<InfoPanel
192+
variant="upgrade"
193+
icon={LockOpenIcon}
194+
iconClassName="text-indigo-500"
195+
title="Unlock more team members"
196+
accessory={
197+
<LinkButton
198+
to={v3BillingPath(organization)}
199+
variant="secondary/small"
200+
LeadingIcon={ArrowUpCircleIcon}
201+
leadingIconClassName="text-indigo-500"
202+
>
203+
Upgrade
204+
</LinkButton>
205+
}
206+
panelClassName="mb-4"
207+
>
208+
<Paragraph variant="small">
209+
You've used all {limits.limit} of your available team members. Upgrade your plan to
210+
add more.
211+
</Paragraph>
212+
</InfoPanel>
213+
))}
172214
<Form method="post" {...form.props}>
173215
<Fieldset>
174216
<InputGroup>

0 commit comments

Comments
 (0)