Skip to content

[service-utils] Add platform, SMS, and pay capabilities to TeamCapabilities type #7210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
5 changes: 5 additions & 0 deletions .changeset/little-pillows-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/service-utils": patch
---

add new capabilities to the type
22 changes: 14 additions & 8 deletions apps/dashboard/src/@/components/blocks/pricing-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { CheckIcon, CircleDollarSignIcon } from "lucide-react";
import { CheckIcon, DollarSignIcon } from "lucide-react";
import Link from "next/link";
import type React from "react";
import { TEAM_PLANS } from "utils/pricing";
Expand Down Expand Up @@ -105,9 +105,12 @@ export const PricingCard: React.FC<PricingCardProps> = ({

{/* Price */}
<div className="flex flex-col gap-0.5">
{plan.isStartingPriceOnly && (
<span className="text-muted-foreground text-xs">Starting at</span>
)}
<div className="flex items-center gap-2">
<span className="font-semibold text-2xl text-foreground tracking-tight">
${plan.price}
${plan.price.toLocaleString()}
</span>

{!isCustomPrice && (
Expand Down Expand Up @@ -183,10 +186,10 @@ const billingPlanToSkuMap: Record<Team["billingPlan"], ProductSKU | undefined> =
{
starter: "plan:starter",
growth: "plan:growth",
accelerate: "plan:accelerate",
scale: "plan:scale",
pro: "plan:pro",
// we can't render checkout buttons for these plans:
pro: undefined,
accelerate: undefined,
free: undefined,
growth_legacy: undefined,
starter_legacy: undefined,
Expand All @@ -201,16 +204,19 @@ function FeatureItem({ text }: FeatureItemProps) {

return (
<div className="flex items-center gap-2 text-sm">
<CheckIcon className="size-4 shrink-0 text-green-500" />
{Array.isArray(text) ? (
<ToolTipLabel label={text[1]}>
<DollarSignIcon className="size-4 shrink-0 text-green-500" />
</ToolTipLabel>
) : (
<CheckIcon className="size-4 shrink-0 text-green-500" />
)}
{Array.isArray(text) ? (
<div className="flex items-center gap-2">
<p className="text-muted-foreground">
{titleStr}{" "}
<span className="text-muted-foreground md:hidden">{text[1]}</span>
</p>
<ToolTipLabel label={text[1]}>
<CircleDollarSignIcon className="hidden size-4 text-muted-foreground md:block" />
</ToolTipLabel>
</div>
) : (
<p className="text-muted-foreground">{titleStr}</p>
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/src/@/lib/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type ProductSKU =
| "plan:custom"
| "plan:accelerate"
| "plan:scale"
| "plan:pro"
| "product:ecosystem_wallets"
| "product:engine_standard"
| "product:engine_premium"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ export function InviteTeamMembersUI(props: {
recommendedMembers={[]}
customCTASection={
<div className="flex gap-3">
{props.team.billingPlan === "free" && (
{(props.team.billingPlan === "free" ||
props.team.billingPlan === "starter") && (
<Button
size="sm"
variant="default"
Expand Down Expand Up @@ -154,30 +155,8 @@ function InviteModalContent(props: {
getTeam: () => Promise<Team>;
teamId: string;
}) {
const [planToShow, setPlanToShow] = useState<
"starter" | "growth" | "accelerate" | "scale"
>("growth");

const starterPlan = (
<PricingCard
billingPlan="starter"
billingStatus={props.billingStatus}
teamSlug={props.teamSlug}
cta={{
label: "Get Started",
type: "checkout",
onClick() {
props.trackEvent({
category: "teamOnboarding",
action: "upgradePlan",
label: "attempt",
plan: "starter",
});
},
}}
getTeam={props.getTeam}
teamId={props.teamId}
/>
const [planToShow, setPlanToShow] = useState<"growth" | "scale" | "pro">(
"growth",
);

const growthPlan = (
Expand All @@ -203,9 +182,9 @@ function InviteModalContent(props: {
/>
);

const acceleratePlan = (
const scalePlan = (
<PricingCard
billingPlan="accelerate"
billingPlan="scale"
billingStatus={props.billingStatus}
teamSlug={props.teamSlug}
cta={{
Expand All @@ -216,7 +195,7 @@ function InviteModalContent(props: {
category: "teamOnboarding",
action: "upgradePlan",
label: "attempt",
plan: "accelerate",
plan: "scale",
});
},
}}
Expand All @@ -225,9 +204,9 @@ function InviteModalContent(props: {
/>
);

const scalePlan = (
const proPlan = (
<PricingCard
billingPlan="scale"
billingPlan="pro"
billingStatus={props.billingStatus}
teamSlug={props.teamSlug}
cta={{
Expand All @@ -238,7 +217,7 @@ function InviteModalContent(props: {
category: "teamOnboarding",
action: "upgradePlan",
label: "attempt",
plan: "scale",
plan: "pro",
});
},
}}
Expand Down Expand Up @@ -266,43 +245,36 @@ function InviteModalContent(props: {

{/* Desktop */}
<div className="hidden grid-cols-1 gap-6 md:grid-cols-4 md:gap-4 lg:grid">
{starterPlan}
{growthPlan}
{acceleratePlan}
{scalePlan}
{proPlan}
</div>

{/* Mobile */}
<div className="lg:hidden">
<TabButtons
tabs={[
{
name: "Starter",
onClick: () => setPlanToShow("starter"),
isActive: planToShow === "starter",
},
{
name: "Growth",
onClick: () => setPlanToShow("growth"),
isActive: planToShow === "growth",
},
{
name: "Accelerate",
onClick: () => setPlanToShow("accelerate"),
isActive: planToShow === "accelerate",
},
{
name: "Scale",
onClick: () => setPlanToShow("scale"),
isActive: planToShow === "scale",
},
{
name: "Pro",
onClick: () => setPlanToShow("pro"),
isActive: planToShow === "pro",
},
]}
/>
<div className="h-4" />
{planToShow === "starter" && starterPlan}
{planToShow === "growth" && growthPlan}
{planToShow === "accelerate" && acceleratePlan}
{planToShow === "scale" && scalePlan}
{planToShow === "pro" && proPlan}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ export function InviteSection(props: {
let bottomSection: React.ReactNode = null;
const maxAllowedInvitesAtOnce = 10;
// invites are enabled if user has edit permission and team plan is not "free"
const inviteEnabled = teamPlan !== "free" && props.userHasEditPermission;
const inviteEnabled =
teamPlan !== "free" &&
teamPlan !== "starter" &&
props.userHasEditPermission;

const form = useForm<InviteFormValues>({
resolver: zodResolver(inviteFormSchema),
Expand All @@ -108,7 +111,7 @@ export function InviteSection(props: {
},
});

if (teamPlan === "free") {
if (teamPlan === "free" || teamPlan === "starter") {
bottomSection = (
<div className="lg:px6 flex items-center justify-between gap-4 border-border border-t px-4 py-4">
<p className="text-muted-foreground text-sm">
Expand All @@ -127,7 +130,7 @@ export function InviteSection(props: {
) : (
<Button variant="outline" size="sm" asChild>
<Link
href={`/team/${props.team.slug}/~/settings/billing`}
href={`/team/${props.team.slug}/~/settings/billing?showPlans=true&highlight=growth`}
className="gap-2"
>
Upgrade
Expand All @@ -147,29 +150,16 @@ export function InviteSection(props: {
} else {
bottomSection = (
<div className="flex items-center border-border border-t px-4 py-4 lg:justify-between lg:px-6">
{teamPlan === "pro" ? (
<p className="text-muted-foreground text-sm">
Team members are billed according to your plan.{" "}
<Link
href="https://meetings.hubspot.com/sales-thirdweb/thirdweb-pro"
target="_blank"
className="text-link-foreground hover:text-foreground"
>
Reach out to sales <ExternalLinkIcon className="inline size-3" />.
</Link>
</p>
) : (
<p className="text-muted-foreground text-sm">
Team members are billed according to your plan.{" "}
<Link
href="https://thirdweb.com/pricing"
target="_blank"
className="text-link-foreground hover:text-foreground"
>
View pricing <ExternalLinkIcon className="inline size-3" />
</Link>
</p>
)}
<p className="text-muted-foreground text-sm">
Team members are billed according to your plan.{" "}
<Link
href="https://thirdweb.com/pricing"
target="_blank"
className="text-link-foreground hover:text-foreground"
>
View pricing <ExternalLinkIcon className="inline size-3" />
</Link>
</p>

<div className="flex gap-3">
{!props.shouldHideInviteButton && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { Team } from "@/api/team";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { PRO_CONTACT_US_URL } from "constants/pro";
import { ArrowRightIcon, DownloadIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";

Expand Down Expand Up @@ -77,91 +74,13 @@ function EngineInfoSection(props: { team_slug: string; project_slug: string }) {
);
}

function CloudHostedEngineSection(props: {
teamPlan: Exclude<Team["billingPlan"], "accelerate" | "scale">;
teamSlug: string;
}) {
return (
<div className="flex flex-col">
<h3 className="mb-0.5 font-semibold text-lg tracking-tight">
Get Managed Engine
</h3>

{props.teamPlan !== "pro" ? (
<div>
<p className="text-muted-foreground text-sm">
Upgrade your plan to{" "}
<UnderlineLink href="/pricing" target="_blank">
Accelerate
</UnderlineLink>{" "}
or{" "}
<UnderlineLink href="/pricing" target="_blank">
Scale
</UnderlineLink>{" "}
to get a managed Engine instance
</p>

<div className="h-5" />
<div className="flex justify-start gap-3">
<Button
variant="outline"
size="sm"
asChild
className="gap-2 bg-card"
>
<Link href={`/team/${props.teamSlug}/~/settings/billing`}>
Upgrade Plan
<ArrowRightIcon className="size-3 text-muted-foreground" />
</Link>
</Button>

<Button
variant="outline"
size="sm"
asChild
className="gap-2 bg-card"
>
<Link href="/pricing" target="_blank">
View Pricing
<ExternalLinkIcon className="size-3 text-muted-foreground" />
</Link>
</Button>
</div>
</div>
) : (
<div>
<p className="mb-4 text-muted-foreground text-sm">
Contact us to get a managed engine for your team
</p>
<Button variant="outline" size="sm" asChild className="gap-2">
<Link href={PRO_CONTACT_US_URL} target="_blank">
Contact Us
<ExternalLinkIcon className="size-3 text-muted-foreground" />
</Link>
</Button>
</div>
)}
</div>
);
}

export function EngineFooterCard(props: {
teamPlan: Team["billingPlan"];
team_slug: string;
project_slug: string;
}) {
return (
<div className="relative rounded-lg border p-6">
{props.teamPlan === "accelerate" || props.teamPlan === "scale" ? null : (
<>
<CloudHostedEngineSection
teamPlan={props.teamPlan}
teamSlug={props.team_slug}
/>
<Separator className="my-5" />
</>
)}

<EngineInfoSection
team_slug={props.team_slug}
project_slug={props.project_slug}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export const InAppWalletSettingsUI: React.FC<
const authRequiredPlan = "growth";
const brandingRequiredPlan = "starter";

// accelerate or higher plan required
// growth or higher plan required
const canEditSmsCountries =
planToTierRecordForGating[props.teamPlan] >=
planToTierRecordForGating[authRequiredPlan];
Expand Down
Loading
Loading