feat: show a premium paywall on the external auth settings page (#28435)

This commit is contained in:
david-fraley
2026-08-21 11:45:38 -05:00
committed by GitHub
parent 9ce42293b0
commit cb52f5339d
8 changed files with 70 additions and 29 deletions
+2
View File
@@ -23829,6 +23829,7 @@ const docTemplate = `{
"browser_only",
"connection_log",
"custom_roles",
"external_auth",
"groups",
"idp_org_sync",
"idp_sync",
@@ -23850,6 +23851,7 @@ const docTemplate = `{
"PremiumFunnelSourceBrowserOnly",
"PremiumFunnelSourceConnectionLog",
"PremiumFunnelSourceCustomRoles",
"PremiumFunnelSourceExternalAuth",
"PremiumFunnelSourceGroups",
"PremiumFunnelSourceIdpOrgSync",
"PremiumFunnelSourceIdpSync",
+2
View File
@@ -21814,6 +21814,7 @@
"browser_only",
"connection_log",
"custom_roles",
"external_auth",
"groups",
"idp_org_sync",
"idp_sync",
@@ -21835,6 +21836,7 @@
"PremiumFunnelSourceBrowserOnly",
"PremiumFunnelSourceConnectionLog",
"PremiumFunnelSourceCustomRoles",
"PremiumFunnelSourceExternalAuth",
"PremiumFunnelSourceGroups",
"PremiumFunnelSourceIdpOrgSync",
"PremiumFunnelSourceIdpSync",
+2
View File
@@ -22,6 +22,7 @@ const (
PremiumFunnelSourceBrowserOnly PremiumFunnelSource = "browser_only"
PremiumFunnelSourceConnectionLog PremiumFunnelSource = "connection_log"
PremiumFunnelSourceCustomRoles PremiumFunnelSource = "custom_roles"
PremiumFunnelSourceExternalAuth PremiumFunnelSource = "external_auth"
PremiumFunnelSourceGroups PremiumFunnelSource = "groups"
PremiumFunnelSourceIdpOrgSync PremiumFunnelSource = "idp_org_sync"
PremiumFunnelSourceIdpSync PremiumFunnelSource = "idp_sync"
@@ -49,6 +50,7 @@ func PremiumFunnelSources() []PremiumFunnelSource {
PremiumFunnelSourceBrowserOnly,
PremiumFunnelSourceConnectionLog,
PremiumFunnelSourceCustomRoles,
PremiumFunnelSourceExternalAuth,
PremiumFunnelSourceGroups,
PremiumFunnelSourceIdpOrgSync,
PremiumFunnelSourceIdpSync,
+3 -3
View File
@@ -10674,9 +10674,9 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
#### Enumerated Values
| Value(s) |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ai_gateway_keys`, `ai_governance`, `aibridge_session_threads`, `aibridge_sessions`, `appearance`, `audit_log`, `browser_only`, `connection_log`, `custom_roles`, `direct`, `groups`, `idp_org_sync`, `idp_sync`, `multiple_organizations`, `observability`, `provisioner_keys`, `provisioners`, `template_permissions`, `workspace_proxies` |
| Value(s) |
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ai_gateway_keys`, `ai_governance`, `aibridge_session_threads`, `aibridge_sessions`, `appearance`, `audit_log`, `browser_only`, `connection_log`, `custom_roles`, `direct`, `external_auth`, `groups`, `idp_org_sync`, `idp_sync`, `multiple_organizations`, `observability`, `provisioner_keys`, `provisioners`, `template_permissions`, `workspace_proxies` |
## codersdk.PremiumFunnelVariant
+2
View File
@@ -7412,6 +7412,7 @@ export type PremiumFunnelSource =
| "connection_log"
| "custom_roles"
| "direct"
| "external_auth"
| "groups"
| "idp_org_sync"
| "idp_sync"
@@ -7433,6 +7434,7 @@ export const PremiumFunnelSources: PremiumFunnelSource[] = [
"connection_log",
"custom_roles",
"direct",
"external_auth",
"groups",
"idp_org_sync",
"idp_sync",
@@ -1,16 +1,24 @@
import type { FC } from "react";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { useDeploymentConfig } from "#/modules/management/DeploymentConfigProvider";
import { pageTitle } from "#/utils/page";
import { ExternalAuthSettingsPageView } from "./ExternalAuthSettingsPageView";
const ExternalAuthSettingsPage: FC = () => {
const { deploymentConfig } = useDeploymentConfig();
const { permissions } = useAuthenticated();
const { multiple_external_auth: isEntitled } = useFeatureVisibility();
return (
<>
<title>{pageTitle("External Authentication Settings")}</title>
<ExternalAuthSettingsPageView config={deploymentConfig.config} />
<ExternalAuthSettingsPageView
config={deploymentConfig.config}
isEntitled={isEntitled}
canViewPremium={permissions.viewAllLicenses}
/>
</>
);
};
@@ -34,6 +34,8 @@ const meta: Meta<typeof ExternalAuthSettingsPageView> = {
},
],
},
isEntitled: false,
canViewPremium: true,
},
};
@@ -44,13 +46,35 @@ export const Page: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const notice = canvas.getByRole("alert");
await expect(within(notice).getByText("Premium")).toBeVisible();
await expect(
within(notice).getByRole("link", {
name: "Read the External Authentication documentation",
}),
).toBeVisible();
canvas.getByRole("link", { name: "Start trial for free" }),
).toHaveAttribute("href", "/deployment/premium");
},
};
export const Entitled: Story = {
args: {
isEntitled: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.queryByRole("link", { name: "Start trial for free" }),
).not.toBeInTheDocument();
},
};
export const PaywallWithoutLicenseAccess: Story = {
args: {
canViewPremium: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.queryByRole("link", { name: "Start trial for free" }),
).not.toBeInTheDocument();
},
};
@@ -3,7 +3,6 @@ import type {
DeploymentValues,
ExternalAuthConfig,
} from "#/api/typesGenerated";
import { Alert } from "#/components/Alert/Alert";
import {
SettingsHeader,
SettingsHeaderDescription,
@@ -19,15 +18,19 @@ import {
TableRow,
} from "#/components/Table/Table";
import { TableEmpty } from "#/components/TableEmpty/TableEmpty";
import { PremiumPaywallSmall } from "#/modules/paywall/PremiumPaywallSmall";
import { docs } from "#/utils/docs";
type ExternalAuthSettingsPageViewProps = {
config: DeploymentValues;
/** True when the deployment may configure more than one provider. */
isEntitled: boolean;
canViewPremium: boolean;
};
export const ExternalAuthSettingsPageView: FC<
ExternalAuthSettingsPageViewProps
> = ({ config }) => {
> = ({ config, isEntitled, canViewPremium }) => {
return (
<>
<SettingsHeader
@@ -52,23 +55,21 @@ export const ExternalAuthSettingsPageView: FC<
}}
/>
<div className="mt-6 mb-6">
<Alert severity="info">
Integrating with multiple external authentication providers is a
Premium feature.{" "}
<a
href={docs(
"/admin/external-auth#multiple-external-providers-premium",
)}
target="_blank"
rel="noreferrer"
className="text-content-link font-medium"
>
Read the External Authentication documentation
</a>
.
</Alert>
</div>
{!isEntitled && (
<div className="mt-6 mb-6">
<PremiumPaywallSmall
source="external_auth"
message="External Authentication"
description="Connect multiple Git and OAuth providers at once."
features={[
"Connect multiple Git providers at once",
"Match providers by regex per host",
"Separate credentials for each provider",
]}
canViewPremium={canViewPremium}
/>
</div>
)}
<Table className="[&_td]:py-6 [&_td:last-child]:pl-8 [&_th:last-child]:pl-8">
<TableHeader>