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: [] }, + ); + }, }} />