Properly paywall the new resource types

This commit is contained in:
Owen
2026-06-02 18:06:42 -07:00
parent 128db20755
commit f2f56dc6c2
17 changed files with 312 additions and 115 deletions

View File

@@ -10,11 +10,14 @@ import {
SettingsSectionTitle
} from "@app/components/Settings";
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { type Selectedsite } from "@app/components/site-selector";
import { Button } from "@app/components/ui/button";
import { toast } from "@app/hooks/useToast";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
import { createApiClient } from "@app/lib/api";
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { useQuery } from "@tanstack/react-query";
@@ -48,13 +51,21 @@ export default function SshSettingsPage(props: {
}) {
const params = use(props.params);
const { resource, updateResource } = useResourceContext();
const { isPaidUser } = usePaidStatus();
const disabled = !isPaidUser(
tierMatrix[TierFeature.AdvancedPublicResources]
);
return (
<SettingsContainer>
<PaidFeaturesAlert
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
/>
<SshServerForm
orgId={params.orgId}
resource={resource}
updateResource={updateResource}
disabled={disabled}
/>
</SettingsContainer>
);
@@ -63,11 +74,13 @@ export default function SshSettingsPage(props: {
function SshServerForm({
orgId,
resource,
updateResource
updateResource,
disabled
}: {
orgId: string;
resource: GetResourceResponse;
updateResource: ResourceContextType["updateResource"];
disabled: boolean;
}) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
@@ -220,31 +233,36 @@ function SshServerForm({
{t("rdpServerDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<BrowserGatewayTargetForm
orgId={orgId}
multiSite={true}
selectedSites={selectedSites}
onSitesChange={setSelectedSites}
destination={bgDestination}
destinationPort={bgDestinationPort}
onDestinationChange={setBgDestination}
onDestinationPortChange={setBgDestinationPort}
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp"
defaultPort={3389}
/>
</SettingsSectionForm>
</SettingsSectionBody>
<form action={formAction} className="flex justify-end mt-4">
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
{t("saveSettings")}
</Button>
</form>
<fieldset
disabled={disabled}
className={disabled ? "opacity-50 pointer-events-none" : ""}
>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<BrowserGatewayTargetForm
orgId={orgId}
multiSite={true}
selectedSites={selectedSites}
onSitesChange={setSelectedSites}
destination={bgDestination}
destinationPort={bgDestinationPort}
onDestinationChange={setBgDestination}
onDestinationPortChange={setBgDestinationPort}
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp"
defaultPort={3389}
/>
</SettingsSectionForm>
</SettingsSectionBody>
<form action={formAction} className="flex justify-end mt-4">
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
{t("saveSettings")}
</Button>
</form>
</fieldset>
</SettingsSection>
);
}

View File

@@ -11,10 +11,13 @@ import {
} from "@app/components/Settings";
import { StrategySelect, StrategyOption } from "@app/components/StrategySelect";
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import {
SitesSelector,
type Selectedsite
} from "@app/components/site-selector";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
import { Button } from "@app/components/ui/button";
import { Input } from "@app/components/ui/input";
import {
@@ -68,13 +71,21 @@ export default function SshSettingsPage(props: {
}) {
const params = use(props.params);
const { resource, updateResource } = useResourceContext();
const { isPaidUser } = usePaidStatus();
const disabled = !isPaidUser(
tierMatrix[TierFeature.AdvancedPublicResources]
);
return (
<SettingsContainer>
<PaidFeaturesAlert
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
/>
<SshServerForm
orgId={params.orgId}
resource={resource}
updateResource={updateResource}
disabled={disabled}
/>
</SettingsContainer>
);
@@ -83,11 +94,13 @@ export default function SshSettingsPage(props: {
function SshServerForm({
orgId,
resource,
updateResource
updateResource,
disabled
}: {
orgId: string;
resource: GetResourceResponse;
updateResource: ResourceContextType["updateResource"];
disabled: boolean;
}) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
@@ -366,6 +379,10 @@ function SshServerForm({
{t("sshServerDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<fieldset
disabled={disabled}
className={disabled ? "opacity-50 pointer-events-none" : ""}
>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<div className="space-y-3">
@@ -520,6 +537,7 @@ function SshServerForm({
{t("saveSettings")}
</Button>
</form>
</fieldset>
</SettingsSection>
);
}

View File

@@ -10,11 +10,14 @@ import {
SettingsSectionTitle
} from "@app/components/Settings";
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { type Selectedsite } from "@app/components/site-selector";
import { Button } from "@app/components/ui/button";
import { toast } from "@app/hooks/useToast";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
import { createApiClient } from "@app/lib/api";
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { useQuery } from "@tanstack/react-query";
@@ -46,13 +49,21 @@ export default function SshSettingsPage(props: {
}) {
const params = use(props.params);
const { resource, updateResource } = useResourceContext();
const { isPaidUser } = usePaidStatus();
const disabled = !isPaidUser(
tierMatrix[TierFeature.AdvancedPublicResources]
);
return (
<SettingsContainer>
<PaidFeaturesAlert
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
/>
<SshServerForm
orgId={params.orgId}
resource={resource}
updateResource={updateResource}
disabled={disabled}
/>
</SettingsContainer>
);
@@ -61,11 +72,13 @@ export default function SshSettingsPage(props: {
function SshServerForm({
orgId,
resource,
updateResource
updateResource,
disabled
}: {
orgId: string;
resource: GetResourceResponse;
updateResource: ResourceContextType["updateResource"];
disabled: boolean;
}) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
@@ -218,31 +231,36 @@ function SshServerForm({
{t("vncServerDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<BrowserGatewayTargetForm
orgId={orgId}
multiSite={true}
selectedSites={selectedSites}
onSitesChange={setSelectedSites}
destination={bgDestination}
destinationPort={bgDestinationPort}
onDestinationChange={setBgDestination}
onDestinationPortChange={setBgDestinationPort}
learnMoreHref="https://docs.pangolin.net/manage/resources/public/vnc"
defaultPort={5900}
/>
</SettingsSectionForm>
</SettingsSectionBody>
<form action={formAction} className="flex justify-end mt-4">
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
{t("saveSettings")}
</Button>
</form>
<fieldset
disabled={disabled}
className={disabled ? "opacity-50 pointer-events-none" : ""}
>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<BrowserGatewayTargetForm
orgId={orgId}
multiSite={true}
selectedSites={selectedSites}
onSitesChange={setSelectedSites}
destination={bgDestination}
destinationPort={bgDestinationPort}
onDestinationChange={setBgDestination}
onDestinationPortChange={setBgDestinationPort}
learnMoreHref="https://docs.pangolin.net/manage/resources/public/vnc"
defaultPort={5900}
/>
</SettingsSectionForm>
</SettingsSectionBody>
<form action={formAction} className="flex justify-end mt-4">
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
{t("saveSettings")}
</Button>
</form>
</fieldset>
</SettingsSection>
);
}

View File

@@ -72,7 +72,10 @@ import {
} from "@app/components/ui/tooltip";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { DockerManager, DockerState } from "@app/lib/docker";
import { orgQueries } from "@app/lib/queries";
@@ -226,6 +229,8 @@ export default function Page() {
orgQueries.sites({ orgId: orgId as string })
);
const { isPaidUser } = usePaidStatus();
const [remoteExitNodes, setRemoteExitNodes] = useState<
ListRemoteExitNodesResponse["remoteExitNodes"]
>([]);
@@ -238,6 +243,14 @@ export default function Page() {
// Resource type state
const [resourceType, setResourceType] = useState<NewResourceType>("http");
const isBrowserGatewayType =
resourceType === "ssh" ||
resourceType === "rdp" ||
resourceType === "vnc";
const browserGatewayDisabled =
isBrowserGatewayType &&
!isPaidUser(tierMatrix[TierFeature.AdvancedPublicResources]);
// Target management state (managed by ProxyResourceTargetsForm; mirrored here for onSubmit)
const [targets, setTargets] = useState<LocalTarget[]>([]);
@@ -870,6 +883,14 @@ export default function Page() {
{/* SSH Server Section */}
{resourceType === "ssh" && (
<SettingsSection>
<PaidFeaturesAlert
tiers={
tierMatrix[
TierFeature
.AdvancedPublicResources
]
}
/>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("sshServer")}
@@ -878,6 +899,14 @@ export default function Page() {
{t("sshServerDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<fieldset
disabled={browserGatewayDisabled}
className={
browserGatewayDisabled
? "opacity-50 pointer-events-none"
: ""
}
>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
{/* Mode */}
@@ -1098,12 +1127,21 @@ export default function Page() {
</div>
</SettingsSectionForm>
</SettingsSectionBody>
</fieldset>
</SettingsSection>
)}
{/* RDP Server Section */}
{resourceType === "rdp" && (
<SettingsSection>
<PaidFeaturesAlert
tiers={
tierMatrix[
TierFeature
.AdvancedPublicResources
]
}
/>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("rdpServer")}
@@ -1112,6 +1150,14 @@ export default function Page() {
{t("rdpServerDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<fieldset
disabled={browserGatewayDisabled}
className={
browserGatewayDisabled
? "opacity-50 pointer-events-none"
: ""
}
>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<BrowserGatewayTargetForm
@@ -1136,12 +1182,21 @@ export default function Page() {
/>
</SettingsSectionForm>
</SettingsSectionBody>
</fieldset>
</SettingsSection>
)}
{/* VNC Server Section */}
{resourceType === "vnc" && (
<SettingsSection>
<PaidFeaturesAlert
tiers={
tierMatrix[
TierFeature
.AdvancedPublicResources
]
}
/>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("vncServer")}
@@ -1150,6 +1205,14 @@ export default function Page() {
{t("vncServerDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<fieldset
disabled={browserGatewayDisabled}
className={
browserGatewayDisabled
? "opacity-50 pointer-events-none"
: ""
}
>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<BrowserGatewayTargetForm
@@ -1174,6 +1237,7 @@ export default function Page() {
/>
</SettingsSectionForm>
</SettingsSectionBody>
</fieldset>
</SettingsSection>
)}
@@ -1225,7 +1289,7 @@ export default function Page() {
}
}}
loading={createLoading}
disabled={!areAllTargetsValid()}
disabled={!areAllTargetsValid() || browserGatewayDisabled}
>
{t("resourceCreate")}
</Button>