From f157e3484a5b64f4534040e019a346918d9bd91e Mon Sep 17 00:00:00 2001 From: MananTank Date: Thu, 24 Apr 2025 14:50:39 +0000 Subject: [PATCH] [TOOL-4277] Dashboard: Update gated features (#6838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on enhancing the handling of team billing plans across various components in the application. It introduces new properties for better plan management and refines the UI elements related to billing features. ### Detailed summary - Added `teamSlug` prop to several components for better context. - Changed `validTeamPlan` to `teamPlan` in multiple files for consistency. - Introduced `planToTierRecordForGating` to manage billing plans. - Updated UI components to reflect new billing logic and props. - Refactored `GatedSwitch` to utilize `currentPlan` and `requiredPlan`. - Enhanced form handling in `InAppWalletSettingsUI` and other components to reflect billing plan changes. - Renamed story exports for clarity in `GatedSwitch` stories. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../account-abstraction/settings/page.tsx | 1 + .../connect/in-app-wallets/settings/page.tsx | 2 +- .../InAppWalletSettingsUI.stories.tsx | 32 +-- .../embedded-wallets/Configure/index.tsx | 245 ++++++++++-------- .../Account/Billing/GatedSwitch.stories.tsx | 84 +++--- .../settings/Account/Billing/GatedSwitch.tsx | 47 ++-- .../Account/Billing/planToTierRecord.ts | 13 + .../settings/ApiKeys/Create/index.tsx | 4 +- .../SponsorshipPolicies/index.tsx | 34 ++- 9 files changed, 268 insertions(+), 194 deletions(-) create mode 100644 apps/dashboard/src/components/settings/Account/Billing/planToTierRecord.ts diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/connect/account-abstraction/settings/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/connect/account-abstraction/settings/page.tsx index 9ed0b0d7c37..776e0d1ad47 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/connect/account-abstraction/settings/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/connect/account-abstraction/settings/page.tsx @@ -60,6 +60,7 @@ export default async function Page(props: { trackingCategory="account-abstraction-project-settings" project={project} teamId={team.id} + teamSlug={team.slug} validTeamPlan={getValidTeamPlan(team)} /> diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/connect/in-app-wallets/settings/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/connect/in-app-wallets/settings/page.tsx index 9d495455d6a..6d92ca7c3c3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/connect/in-app-wallets/settings/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/connect/in-app-wallets/settings/page.tsx @@ -30,7 +30,7 @@ export default async function Page(props: { teamId={team.id} trackingCategory="in-app-wallet-project-settings" teamSlug={team_slug} - validTeamPlan={getValidTeamPlan(team)} + teamPlan={getValidTeamPlan(team)} smsCountryTiers={smsCountryTiers} /> ); diff --git a/apps/dashboard/src/components/embedded-wallets/Configure/InAppWalletSettingsUI.stories.tsx b/apps/dashboard/src/components/embedded-wallets/Configure/InAppWalletSettingsUI.stories.tsx index f8f2c934f38..770b98c89e5 100644 --- a/apps/dashboard/src/components/embedded-wallets/Configure/InAppWalletSettingsUI.stories.tsx +++ b/apps/dashboard/src/components/embedded-wallets/Configure/InAppWalletSettingsUI.stories.tsx @@ -1,6 +1,6 @@ +import type { Team } from "@/api/team"; import type { Meta, StoryObj } from "@storybook/react"; import { projectStub } from "../../../stories/stubs"; -import { mobileViewport } from "../../../stories/utils"; import { InAppWalletSettingsUI } from "./index"; const meta = { @@ -16,44 +16,44 @@ const meta = { export default meta; type Story = StoryObj; -export const GrowthPlan: Story = { +export const FreePlan: Story = { args: { - canEditAdvancedFeatures: true, + currentPlan: "free", }, }; -export const FreePlan: Story = { +export const GrowthPlan: Story = { args: { - canEditAdvancedFeatures: false, + currentPlan: "growth", }, }; -export const GrowthPlanMobile: Story = { +export const AcceleratePlan: Story = { args: { - canEditAdvancedFeatures: true, - }, - parameters: { - viewport: mobileViewport("iphone14"), + currentPlan: "accelerate", }, }; -export const FreePlanMobile: Story = { +export const GrowthLegacyPlan: Story = { args: { - canEditAdvancedFeatures: false, + currentPlan: "growth_legacy", }, - parameters: { - viewport: mobileViewport("iphone14"), +}; + +export const ProPlan: Story = { + args: { + currentPlan: "pro", }, }; function Variants(props: { - canEditAdvancedFeatures: boolean; + currentPlan: Team["billingPlan"]; }) { return (
, trackingData: UpdateAPIKeyTrackingData, @@ -163,8 +162,7 @@ const InAppWalletSettingsPageUI: React.FC< }; export const InAppWalletSettingsUI: React.FC< - Omit & { - canEditAdvancedFeatures: boolean; + InAppWalletSettingsPageProps & { updateApiKey: ( projectValues: Partial, trackingData: UpdateAPIKeyTrackingData, @@ -173,7 +171,6 @@ export const InAppWalletSettingsUI: React.FC< embeddedWalletService: ProjectEmbeddedWalletsService; } > = (props) => { - const { canEditAdvancedFeatures } = props; const services = props.project.services; const config = props.embeddedWalletService; @@ -181,6 +178,13 @@ export const InAppWalletSettingsUI: React.FC< const hasCustomBranding = !!config.applicationImageUrl?.length || !!config.applicationName?.length; + const authRequiredPlan = "accelerate"; + + // accelerate or higher plan required + const canEditSmsCountries = + planToTierRecordForGating[props.teamPlan] >= + planToTierRecordForGating[authRequiredPlan]; + const form = useForm({ resolver: zodResolver(apiKeyEmbeddedWalletsValidationSchema), values: { @@ -197,7 +201,7 @@ export const InAppWalletSettingsUI: React.FC< redirectUrls: (config.redirectUrls || []).join("\n"), smsEnabledCountryISOs: config.smsEnabledCountryISOs ? config.smsEnabledCountryISOs - : canEditAdvancedFeatures + : canEditSmsCountries ? ["US", "CA"] : [], }, @@ -269,7 +273,9 @@ export const InAppWalletSettingsUI: React.FC< {/* Branding */} @@ -278,22 +284,28 @@ export const InAppWalletSettingsUI: React.FC<
@@ -310,10 +322,10 @@ export const InAppWalletSettingsUI: React.FC< function BrandingFieldset(props: { form: UseFormReturn; - canEditAdvancedFeatures: boolean; + teamPlan: Team["billingPlan"]; + teamSlug: string; + requiredPlan: Team["billingPlan"]; }) { - const { form, canEditAdvancedFeatures } = props; - return (
- form.setValue( - "branding", - checked - ? { - applicationImageUrl: "", - applicationName: "", - } - : undefined, - ) - } + currentPlan={props.teamPlan} + switchProps={{ + id: "branding-switch", + checked: !!props.form.watch("branding"), + onCheckedChange: (checked) => + props.form.setValue( + "branding", + checked + ? { + applicationImageUrl: "", + applicationName: "", + } + : undefined, + ), + }} /> - {/* Application Image */} ( @@ -358,9 +375,9 @@ function BrandingFieldset(props: { { - form.setValue("branding.applicationImageUrl", uri, { + props.form.setValue("branding.applicationImageUrl", uri, { shouldDirty: true, shouldTouch: true, }); @@ -374,7 +391,7 @@ function BrandingFieldset(props: { {/* Application Name */} ( @@ -391,7 +408,7 @@ function BrandingFieldset(props: { )} /> - +
); } @@ -453,8 +470,10 @@ function AppImageFormControl(props: { function SMSCountryFields(props: { form: UseFormReturn; - canEditAdvancedFeatures: boolean; smsCountryTiers: SMSCountryTiers; + teamPlan: Team["billingPlan"]; + requiredPlan: Team["billingPlan"]; + teamSlug: string; }) { return (
@@ -464,31 +483,30 @@ function SMSCountryFields(props: { description="Optionally allow users in selected countries to login via SMS OTP." > + props.form.setValue( + "smsEnabledCountryISOs", + checked + ? // by default, enable US and CA only + ["US", "CA"] + : [], + ), + }} trackingLabel="sms" - checked={ - !!props.form.watch("smsEnabledCountryISOs").length && - props.canEditAdvancedFeatures - } - upgradeRequired={!props.canEditAdvancedFeatures} - onCheckedChange={(checked) => - props.form.setValue( - "smsEnabledCountryISOs", - checked - ? // by default, enable US and CA only - ["US", "CA"] - : [], - ) - } /> - )} /> - +
); } function JSONWebTokenFields(props: { form: UseFormReturn; - canEditAdvancedFeatures: boolean; + teamPlan: Team["billingPlan"]; + teamSlug: string; + requiredPlan: Team["billingPlan"]; }) { - const { form, canEditAdvancedFeatures } = props; - return (
{ - form.setValue( - "customAuthentication", - checked - ? { - jwksUri: "", - aud: "", - } - : undefined, - ); + currentPlan={props.teamPlan} + requiredPlan={props.requiredPlan} + teamSlug={props.teamSlug} + switchProps={{ + id: "authentication-switch", + checked: !!props.form.watch("customAuthentication"), + onCheckedChange: (checked) => { + props.form.setValue( + "customAuthentication", + checked ? { jwksUri: "", aud: "" } : undefined, + ); + }, }} /> - ( @@ -576,7 +593,7 @@ function JSONWebTokenFields(props: { /> ( @@ -591,19 +608,19 @@ function JSONWebTokenFields(props: { )} /> - +
); } function AuthEndpointFields(props: { form: UseFormReturn; - canEditAdvancedFeatures: boolean; + teamPlan: Team["billingPlan"]; + teamSlug: string; + requiredPlan: Team["billingPlan"]; }) { - const { form, canEditAdvancedFeatures } = props; - const expandCustomAuthEndpointField = - form.watch("customAuthEndpoint") !== undefined && canEditAdvancedFeatures; + props.form.watch("customAuthEndpoint") !== undefined; return (
@@ -628,28 +645,31 @@ function AuthEndpointFields(props: { > { - form.setValue( - "customAuthEndpoint", - checked - ? { - authEndpoint: "", - customHeaders: [], - } - : undefined, - ); + switchProps={{ + id: "auth-endpoint-switch", + checked: expandCustomAuthEndpointField, + onCheckedChange: (checked) => { + props.form.setValue( + "customAuthEndpoint", + checked + ? { + authEndpoint: "", + customHeaders: [], + } + : undefined, + ); + }, }} + currentPlan={props.teamPlan} + requiredPlan={props.requiredPlan} + teamSlug={props.teamSlug} /> + {/* useFieldArray used on this component - it creates empty customAuthEndpoint.customHeaders array on mount */} {/* So only mount if expandCustomAuthEndpointField is true */} {expandCustomAuthEndpointField && ( - + )}
); @@ -657,19 +677,16 @@ function AuthEndpointFields(props: { function AuthEndpointFieldsContent(props: { form: UseFormReturn; - canEditAdvancedFeatures: boolean; }) { - const { form } = props; - const customHeaderFields = useFieldArray({ - control: form.control, + control: props.form.control, name: "customAuthEndpoint.customHeaders", }); return (
( @@ -698,14 +715,14 @@ function AuthEndpointFieldsContent(props: { @@ -781,12 +798,18 @@ function NativeAppsFieldset(props: { ); } -function AdvancedConfigurationContainer(props: { +function GatedCollapsibleContainer(props: { children: React.ReactNode; - show: boolean; + isExpanded: boolean; className?: string; + requiredPlan: Team["billingPlan"]; + currentPlan: Team["billingPlan"]; }) { - if (!props.show) { + const upgradeRequired = + planToTierRecordForGating[props.currentPlan] < + planToTierRecordForGating[props.requiredPlan]; + + if (!props.isExpanded || upgradeRequired) { return null; } @@ -800,7 +823,7 @@ function Fieldset(props: { return (
- {/* put inside div to remove defualt styles on legend */} + {/* put inside div to remove default styles on legend */}
{props.legend}
diff --git a/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.stories.tsx b/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.stories.tsx index 7c6d3e68a69..1991e7923b2 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.stories.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.stories.tsx @@ -1,5 +1,14 @@ -import { Separator } from "@/components/ui/separator"; +import type { Team } from "@/api/team"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; import { BadgeContainer } from "../../../../stories/utils"; import { GatedSwitch } from "./GatedSwitch"; @@ -18,41 +27,52 @@ export const AllVariants: Story = { }; function Variants() { - return ( -
-
- - - - - - - + const [requiredPlan, setRequiredPlan] = useState< + "free" | "growth" | "accelerate" | "pro" + >("accelerate"); - - - - - - - - - + const plans: Team["billingPlan"][] = [ + "free", + "starter_legacy", + "starter", + "growth_legacy", + "growth", + "accelerate", + "pro", + ]; - - - - - - - - - + return ( +
+
+ + +
- - + {plans.map((currentPlan) => ( + + -
+ ))}
); } diff --git a/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.tsx b/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.tsx index f942f011d3b..dfb688ae7fb 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.tsx @@ -1,47 +1,56 @@ -import { Badge } from "@/components/ui/badge"; +import type { Team } from "@/api/team"; import { Switch } from "@/components/ui/switch"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { TrackedLinkTW } from "@/components/ui/tracked-link"; +import { cn } from "@/lib/utils"; +import { TeamPlanBadge } from "../../../../app/(app)/components/TeamPlanBadge"; +import { planToTierRecordForGating } from "./planToTierRecord"; type SwitchProps = React.ComponentProps; -interface GatedSwitchProps extends SwitchProps { +type GatedSwitchProps = { trackingLabel?: string; - upgradeRequired: boolean; -} + currentPlan: Team["billingPlan"]; + requiredPlan: Team["billingPlan"]; + teamSlug: string; + switchProps?: SwitchProps; +}; export const GatedSwitch: React.FC = ( - allProps: GatedSwitchProps, + props: GatedSwitchProps, ) => { - const { upgradeRequired, trackingLabel, checked, ...props } = allProps; + const isUpgradeRequired = + planToTierRecordForGating[props.currentPlan] < + planToTierRecordForGating[props.requiredPlan]; return ( - To access this feature, you need to upgrade to the{" "} + isUpgradeRequired ? ( + + To access this feature,
Upgrade to the{" "} - Growth plan + {props.requiredPlan} plan - . -
+ ) : undefined } >
- {upgradeRequired && Growth} + {isUpgradeRequired && }
diff --git a/apps/dashboard/src/components/settings/Account/Billing/planToTierRecord.ts b/apps/dashboard/src/components/settings/Account/Billing/planToTierRecord.ts new file mode 100644 index 00000000000..15b412c96fb --- /dev/null +++ b/apps/dashboard/src/components/settings/Account/Billing/planToTierRecord.ts @@ -0,0 +1,13 @@ +import type { Team } from "@/api/team"; + +// Note: Growth legacy is considered higher tier in this hierarchy +export const planToTierRecordForGating: Record = { + free: 0, + starter_legacy: 1, + starter: 2, + growth: 3, + accelerate: 4, + growth_legacy: 5, + scale: 6, + pro: 7, +}; diff --git a/apps/dashboard/src/components/settings/ApiKeys/Create/index.tsx b/apps/dashboard/src/components/settings/ApiKeys/Create/index.tsx index 04e2cc87cc1..14fc97ece6e 100644 --- a/apps/dashboard/src/components/settings/ApiKeys/Create/index.tsx +++ b/apps/dashboard/src/components/settings/ApiKeys/Create/index.tsx @@ -32,7 +32,7 @@ import { useMutation } from "@tanstack/react-query"; import type { ProjectService } from "@thirdweb-dev/service-utils"; import { SERVICES } from "@thirdweb-dev/service-utils"; import { useTrack } from "hooks/analytics/useTrack"; -import { ArrowLeftIcon, ExternalLinkIcon } from "lucide-react"; +import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -529,7 +529,7 @@ function CreatedProjectDetails(props: { className="min-w-28 gap-2" > View Project - + )} diff --git a/apps/dashboard/src/components/smart-wallets/SponsorshipPolicies/index.tsx b/apps/dashboard/src/components/smart-wallets/SponsorshipPolicies/index.tsx index ad69309bb88..2f528bdb01a 100644 --- a/apps/dashboard/src/components/smart-wallets/SponsorshipPolicies/index.tsx +++ b/apps/dashboard/src/components/smart-wallets/SponsorshipPolicies/index.tsx @@ -41,6 +41,7 @@ type AccountAbstractionSettingsPageProps = { project: Project; trackingCategory: string; teamId: string; + teamSlug: string; validTeamPlan: Team["billingPlan"]; client: ThirdwebClient; }; @@ -548,7 +549,12 @@ export function AccountAbstractionSettingsPage(
- Server verifier + + Server verifier + Specify your own endpoint that will verify each transaction and decide whether it should be sponsored or not.
This @@ -566,18 +572,20 @@ export function AccountAbstractionSettingsPage(
{ - form.setValue( - "serverVerifier", - !checked - ? { enabled: false, url: null, headers: null } - : { enabled: true, url: "", headers: [] }, - ); + requiredPlan="accelerate" + currentPlan={props.validTeamPlan} + teamSlug={props.teamSlug} + switchProps={{ + id: "server-verifier-switch", + checked: form.watch("serverVerifier").enabled, + onCheckedChange: (checked) => { + form.setValue( + "serverVerifier", + !checked + ? { enabled: false, url: null, headers: null } + : { enabled: true, url: "", headers: [] }, + ); + }, }} />