chore: image transformation and storage settings entitlements (#42964)

Add entitlement check for storage image transformations to properly
control access based on organization's plan.

### Changes
- Add `storage.image_transformations` entitlement check to Storage
Settings
- Add `storage.max_file_size.configurable` entitlement check to Storage
Settings
- Disable image transformation toggle based on entitlement instead of
just free tier check
- Wait for entitlement loading before initializing form values
- Use entitlement value as fallback when setting default
imageTransformationEnabled state
- Consolidate all loading states into a single isLoading variable for
cleaner code

### Testing
- Head to `/project/_/storage/files/settings` with an Org on the Free
Plan
- Assert that the Image Transformations toggle is disabled
- Head to `/project/_/storage/files/settings` with an Org on the Free
Plan
- Assert that the Image Transformations toggle is enabled
- Toggle it, save and refresh the page to assert that your changes were
correctly saved
This commit is contained in:
Ignacio Dobronich authored and GitHub committed 2026-02-19 15:38:15 -03:00
1 parent d5cc70560d
commit 2942ee9811
1 file changed
+41 -15
@@ -84,7 +84,7 @@ export const StorageSettings = () => {
const {
data: config,
error,
isPending: isLoading,
isPending: isLoadingProjectStorageConfig,
isSuccess,
isError,
} = useProjectStorageConfigQuery({ projectRef })
@@ -104,17 +104,28 @@ export const StorageSettings = () => {
const shouldAutoValidateBucketLimits = sizeLimitCheckCondition === 'auto'
const { data: organization } = useSelectedOrganizationQuery()
const { getEntitlementNumericValue, isEntitlementUnlimited } =
useCheckEntitlements('storage.max_file_size')
const isFreeTier = organization?.plan.id === 'free'
const {
getEntitlementNumericValue,
isEntitlementUnlimited,
isLoading: isLoadingMaxFileSizeEntitlement,
} = useCheckEntitlements('storage.max_file_size')
const { hasAccess: hasAccessToFileSizeConfiguration, isLoading: isLoadingFileSizeConfigurable } =
useCheckEntitlements('storage.max_file_size.configurable')
const {
hasAccess: hasAccessToImageTransformations,
isLoading: isLoadingImageTransformationEntitlement,
} = useCheckEntitlements('storage.image_transformations')
const isSpendCapOn =
organization?.plan.id === 'pro' && organization?.usage_billing_enabled === false
const hasLimitedStorageAccess =
!hasAccessToImageTransformations && !hasAccessToFileSizeConfiguration
const [isUpdating, setIsUpdating] = useState(false)
const [initialValues, setInitialValues] = useState<StorageSettingsState>({
fileSizeLimit: 0,
unit: StorageSizeUnits.MB,
imageTransformationEnabled: !isFreeTier,
imageTransformationEnabled: false,
})
const maxBytes = useMemo(() => {
@@ -125,6 +136,12 @@ export const StorageSettings = () => {
}
}, [organization, isEntitlementUnlimited, getEntitlementNumericValue])
const isLoading =
isLoadingProjectStorageConfig ||
isLoadingPermissions ||
isLoadingMaxFileSizeEntitlement ||
isLoadingFileSizeConfigurable ||
isLoadingImageTransformationEntitlement
const FormSchema = z
.object({
fileSizeLimit: z.coerce.number(),
@@ -218,10 +235,11 @@ export const StorageSettings = () => {
}
useEffect(() => {
if (isSuccess && config) {
if (isSuccess && config && !isLoading) {
const { fileSizeLimit, features } = config
const { value, unit } = convertFromBytes(fileSizeLimit ?? 0)
const imageTransformationEnabled = features?.imageTransformation?.enabled ?? !isFreeTier
const imageTransformationEnabled =
features?.imageTransformation?.enabled ?? hasAccessToImageTransformations
setInitialValues({
fileSizeLimit: value,
@@ -237,7 +255,7 @@ export const StorageSettings = () => {
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess, config])
}, [isSuccess, config, isLoading, hasAccessToImageTransformations])
return (
<PageContainer>
@@ -250,7 +268,7 @@ export const StorageSettings = () => {
title="Storage settings are not available for self-hosted projects"
description="Storage settings are only available for Supabase Platform projects."
/>
) : isLoading || isLoadingPermissions ? (
) : isLoading ? (
<GenericSkeletonLoader />
) : (
<>
@@ -296,7 +314,9 @@ export const StorageSettings = () => {
<FormControl_Shadcn_>
<Switch
size="large"
disabled={isFreeTier}
disabled={
!hasAccessToImageTransformations || !canUpdateStorageSettings
}
checked={field.value}
onCheckedChange={field.onChange}
/>
@@ -344,7 +364,10 @@ export const StorageSettings = () => {
form.clearErrors('fileSizeLimit')
}}
className="w-32 rounded-r-none border-r-0"
disabled={isFreeTier || !canUpdateStorageSettings}
disabled={
!hasAccessToFileSizeConfiguration ||
!canUpdateStorageSettings
}
/>
<FormField_Shadcn_
control={form.control}
@@ -356,7 +379,10 @@ export const StorageSettings = () => {
unitField.onChange(val)
form.clearErrors('fileSizeLimit')
}}
disabled={isFreeTier || !canUpdateStorageSettings}
disabled={
!hasAccessToFileSizeConfiguration ||
!canUpdateStorageSettings
}
>
<SelectTrigger_Shadcn_ className="w-[90px] text-xs font-mono rounded-l-none bg-surface-300">
<SelectValue_Shadcn_ placeholder="Choose a prefix">
@@ -367,7 +393,7 @@ export const StorageSettings = () => {
{Object.values(StorageSizeUnits).map((unit: string) => (
<SelectItem_Shadcn_
key={unit}
disabled={isFreeTier}
disabled={!hasAccessToFileSizeConfiguration}
value={unit}
>
{unit}
@@ -398,7 +424,7 @@ export const StorageSettings = () => {
</FormMessage_Shadcn_>
)}
</CardContent>
{isFreeTier && (
{hasLimitedStorageAccess && (
<UpgradeToPro
fullWidth
variant="primary"
@@ -447,7 +473,7 @@ export const StorageSettings = () => {
</Button>
)}
<Button
type={isFreeTier ? 'default' : 'primary'}
type={hasLimitedStorageAccess ? 'default' : 'primary'}
htmlType="submit"
loading={isUpdating}
disabled={