Skip to content

Commit fdfffce

Browse files
committed
improvement(whitelabeling): gate the settings page on the whitelabel read's entitlement
The page blocked on the full organization billing read (member page plus each member's usage ledger for the period) only to check the plan name. The whitelabel read now also returns the entitlement the update enforces, as the session-policy and data-retention reads do, as an additive field beside `data` so clients from before it parse the same response. The page renders after that one read, which the workspace branding provider usually has cached, and its gate now matches what saving allows.
1 parent efbb4f5 commit fdfffce

8 files changed

Lines changed: 48 additions & 42 deletions

File tree

‎apps/sim/app/api/organizations/[id]/whitelabel/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const GET = defineInternalJsonRoute({
2525
errorPolicy: internalOrchestrationErrorPolicy,
2626
mapInput: ({ params }) => ({ organizationId: params.id }),
2727
useCase: getOrganizationWhitelabel,
28-
present: (data) => ({ success: true, data }),
28+
present: ({ settings, isEnterprise }) => ({ success: true, data: settings, isEnterprise }),
2929
})
3030

3131
export const PUT = defineInternalJsonRoute({

‎apps/sim/ee/whitelabeling/components/branding-provider.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ export function BrandingProvider({
3838
viewerIsHostOrganizationMember,
3939
initialOrgSettings,
4040
}: BrandingProviderProps) {
41-
const { data: orgSettings } = useWhitelabelSettings(
41+
const { data: orgWhitelabel } = useWhitelabelSettings(
4242
viewerIsHostOrganizationMember ? (hostOrganizationId ?? undefined) : undefined
4343
)
44+
const orgSettings = orgWhitelabel?.settings
4445

4546
const effectiveOrgSettings =
4647
orgSettings !== undefined ? orgSettings : (initialOrgSettings ?? null)

‎apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx‎

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ import { createLogger } from '@sim/logger'
77
import { getErrorMessage, toError } from '@sim/utils/errors'
88
import Image from 'next/image'
99
import { saveDiscardActions } from '@/components/settings/save-discard-actions'
10-
import { isEnterprise } from '@/lib/billing/plan-helpers'
1110
import { HEX_COLOR_REGEX } from '@/lib/branding'
1211
import type { OrganizationWhitelabelSettings } from '@/lib/branding/types'
13-
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
1412
import { DropZone } from '@/app/workspace/[workspaceId]/components/drop-zone'
1513
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
1614
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
@@ -23,7 +21,6 @@ import {
2321
useWhitelabelSettings,
2422
type WhitelabelSettingsPayload,
2523
} from '@/ee/whitelabeling/hooks/whitelabel'
26-
import { useOrganizationBilling } from '@/hooks/queries/organization'
2724
import { useWorkspacesQuery } from '@/hooks/queries/workspace'
2825

2926
const logger = createLogger('WhitelabelingSettings')
@@ -407,18 +404,13 @@ function WhitelabelingForm({ initialSettings, orgId, uploadWorkspaceId }: Whitel
407404
)
408405
}
409406

407+
/** Gates on the entitlement returned by the whitelabel read — the same check saving enforces. */
410408
export function WhitelabelingSettings({ organizationId: orgId }: WhitelabelingSettingsProps) {
411-
const { billingEnabled } = useDeploymentShape()
412-
const {
413-
data: organizationBillingData,
414-
isPending: organizationBillingLoading,
415-
error: organizationBillingError,
416-
} = useOrganizationBilling(orgId, { enabled: billingEnabled })
417409
const { data: workspaces } = useWorkspacesQuery(true)
418410
const uploadWorkspaceId = workspaces?.find((workspace) => workspace.organizationId === orgId)?.id
419-
const { data: savedSettings, error: settingsError, isLoading } = useWhitelabelSettings(orgId)
411+
const { data: whitelabel, error: settingsError, isLoading } = useWhitelabelSettings(orgId)
420412

421-
if (isLoading || (billingEnabled && organizationBillingLoading)) {
413+
if (isLoading) {
422414
return (
423415
<SettingsPanel
424416
actions={saveDiscardActions({
@@ -432,32 +424,24 @@ export function WhitelabelingSettings({ organizationId: orgId }: WhitelabelingSe
432424
)
433425
}
434426

435-
if (!savedSettings) {
427+
if (!whitelabel) {
436428
return (
437429
<SettingsEmptyState tone='error'>
438430
{getErrorMessage(settingsError, 'Failed to load whitelabeling settings')}
439431
</SettingsEmptyState>
440432
)
441433
}
442434

443-
if (billingEnabled && organizationBillingData === undefined && organizationBillingError) {
435+
if (!whitelabel.isEnterprise) {
444436
return (
445-
<SettingsEmptyState tone='error'>
446-
{getErrorMessage(organizationBillingError, 'Failed to load organization billing')}
447-
</SettingsEmptyState>
448-
)
449-
}
450-
451-
if (billingEnabled && !isEnterprise(organizationBillingData?.data?.subscriptionPlan)) {
452-
return (
453-
<SettingsEmptyState>Whitelabeling is available on Enterprise plans only.</SettingsEmptyState>
437+
<SettingsEmptyState>Whitelabeling requires an active Enterprise plan.</SettingsEmptyState>
454438
)
455439
}
456440

457441
return (
458442
<WhitelabelingForm
459443
key={orgId}
460-
initialSettings={savedSettings}
444+
initialSettings={whitelabel.settings}
461445
orgId={orgId}
462446
uploadWorkspaceId={uploadWorkspaceId}
463447
/>

‎apps/sim/ee/whitelabeling/hooks/whitelabel.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,25 @@ export const whitelabelKeys = {
2929
settings: (orgId: string) => [...whitelabelKeys.settingsList(), orgId] as const,
3030
}
3131

32+
export interface OrganizationWhitelabel {
33+
/** Whether the organization may save whitelabel settings: the entitlement the update enforces. */
34+
isEnterprise: boolean
35+
settings: OrganizationWhitelabelSettings
36+
}
37+
3238
async function fetchWhitelabelSettings(
3339
orgId: string,
3440
signal?: AbortSignal
35-
): Promise<OrganizationWhitelabelSettings> {
36-
const { data } = await requestJson(getOrganizationWhitelabelContract, {
41+
): Promise<OrganizationWhitelabel> {
42+
const { data, isEnterprise } = await requestJson(getOrganizationWhitelabelContract, {
3743
params: { id: orgId },
3844
signal,
3945
})
40-
return data
46+
return { isEnterprise, settings: data }
4147
}
4248

4349
/**
44-
* Hook to fetch whitelabel settings for an organization.
50+
* Hook to fetch an organization's whitelabel settings and whether it is entitled to change them.
4551
*/
4652
export function useWhitelabelSettings(orgId: string | undefined) {
4753
return useQuery({

‎apps/sim/lib/api/contracts/organization.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,13 +672,23 @@ const organizationWhitelabelEnvelopeResponseSchema = z.object({
672672
data: organizationWhitelabelSettingsResponseSchema,
673673
})
674674

675+
/**
676+
* The read also carries the entitlement the update enforces, so the settings page can gate on it
677+
* without a separate billing read. It sits beside `data` rather than inside it: `data` keeps the
678+
* settings shape clients already parse, so a client from before this field reads the same response.
679+
*/
680+
const organizationWhitelabelReadResponseSchema =
681+
organizationWhitelabelEnvelopeResponseSchema.extend({
682+
isEnterprise: z.boolean(),
683+
})
684+
675685
export const getOrganizationWhitelabelContract = defineRouteContract({
676686
method: 'GET',
677687
path: '/api/organizations/[id]/whitelabel',
678688
params: organizationParamsSchema,
679689
response: {
680690
mode: 'json',
681-
schema: organizationWhitelabelEnvelopeResponseSchema,
691+
schema: organizationWhitelabelReadResponseSchema,
682692
},
683693
})
684694

‎apps/sim/lib/mothership/tools/server/settings.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,13 @@ const adapters: Record<string, SettingAdapter> = {
230230
'organization/members': { get: readSettingsRoster },
231231
'organization/whitelabeling': {
232232
schema: updateOrganizationWhitelabelBodySchema.strict(),
233-
get: (context) =>
234-
getOrganizationWhitelabel.execute({
235-
principal: context.principal,
236-
input: organizationInput(context),
237-
}),
233+
get: async (context) =>
234+
(
235+
await getOrganizationWhitelabel.execute({
236+
principal: context.principal,
237+
input: organizationInput(context),
238+
})
239+
).settings,
238240
update: async (context, changes) =>
239241
(
240242
await updateOrganizationWhitelabel.execute({

‎apps/sim/lib/organizations/application/configuration.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ describe('organization configuration authorization', () => {
158158
queueTableRows(organization, [{ settings: { brandName: 'Acme' } }])
159159
await expect(
160160
getOrganizationWhitelabel.execute({ principal, input: { organizationId: 'org' } })
161-
).resolves.toEqual({ brandName: 'Acme' })
161+
).resolves.toEqual({ isEnterprise: true, settings: { brandName: 'Acme' } })
162162
queueTableRows(member, [])
163163
await expect(
164164
getOrganizationWhitelabel.execute({ principal, input: { organizationId: 'org' } })

‎apps/sim/lib/organizations/application/configuration.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,15 @@ function configuredRetention(
107107
export const getOrganizationWhitelabel = defineOrganizationConfigurationUseCase({
108108
operation: organizationConfigurationOperations.readWhitelabel,
109109
async execute({ input }: { input: OrganizationInput }) {
110-
const [row] = await db
111-
.select({ settings: organization.whitelabelSettings })
112-
.from(organization)
113-
.where(eq(organization.id, input.organizationId))
114-
.limit(1)
115-
return requireOrganization(row).settings ?? {}
110+
const [[row], isEnterprise] = await Promise.all([
111+
db
112+
.select({ settings: organization.whitelabelSettings })
113+
.from(organization)
114+
.where(eq(organization.id, input.organizationId))
115+
.limit(1),
116+
isOrganizationFeatureEntitled(input.organizationId, isWhitelabelingEnabled),
117+
])
118+
return { isEnterprise, settings: requireOrganization(row).settings ?? {} }
116119
},
117120
})
118121

0 commit comments

Comments
 (0)