Joshen/fe 3432 show maintenance banners only for affected project regions (#46191)

## Context

There'll be an upcoming shared pooler maintenance for specific regions -
so the PR here informs users about that via the following changes

- Shift Terms of Service update into a float banner
- Same local storage key is used for dismissal so users who already
dismissed it won't see this again
  - This is to prevent 2 notice banners scenario
<img width="342" height="235" alt="image"
src="https://github.com/user-attachments/assets/0a10fa53-46a0-4c71-beef-d66e006503fd"
/>

- Updated NoticeBanner to inform users of the upcoming maintenance
- Only projects in specific regions will be affected, so not all
projects will see this
<img width="658" height="75" alt="image"
src="https://github.com/user-attachments/assets/83aabda5-a774-4118-a945-08052b7c6b3e"
/>
<img width="496" height="152" alt="image"
src="https://github.com/user-attachments/assets/a1ccc440-5179-4a4b-919f-208844bb2227"
/>

- Cleaned up unused local storage keys

### To test

- Can be tested on preview as the notice applies for eu-central-1 and
us-east-1


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Dismissible Terms of Service update banner with “Learn more” dialog so
users can review and acknowledge changes.
* Enhanced maintenance notices showing region-specific maintenance
windows, a status link per region, and formatted UTC timestamps tied to
the selected project.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46191?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Joshen Lim
2026-05-22 17:40:12 +07:00
committed by GitHub
parent 5987421afe
commit 24f62c4402
5 changed files with 207 additions and 85 deletions

View File

@@ -1,16 +1,43 @@
import { useFlag } from 'common'
import { PropsWithChildren } from 'react'
import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
import { PropsWithChildren, useEffect } from 'react'
import { OrganizationResourceBanner } from '../Organization/HeaderBanner'
import { ClockSkewBanner } from '@/components/layouts/AppLayout/ClockSkewBanner'
import { FlyDeprecationBanner } from '@/components/layouts/AppLayout/FlyDeprecationBanner'
import { NoticeBanner } from '@/components/layouts/AppLayout/NoticeBanner'
import { StatusPageBanner } from '@/components/layouts/AppLayout/StatusPageBanner'
import { BannerTOSUpdate } from '@/components/ui/BannerStack/Banners/BannerTOSUpdate'
import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
const TOSUpdateExpiry = new Date('2026-07-04T00:00:00Z')
export const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
const showNoticeBanner = useFlag('showNoticeBanner')
const clockSkewBanner = useFlag('clockSkewBanner')
const { addBanner, dismissBanner } = useBannerStack()
const [TOSUpdateAcknowledged, , { isSuccess }] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.TERMS_OF_SERVICE_UPDATE,
false
)
useEffect(() => {
if (Date.now() >= TOSUpdateExpiry.getTime()) return
if (isSuccess && !TOSUpdateAcknowledged) {
addBanner({
id: 'tos-update-banner',
isDismissed: false,
content: <BannerTOSUpdate />,
priority: 2,
})
} else {
dismissBanner('tos-update-banner')
}
}, [TOSUpdateAcknowledged, isSuccess, addBanner, dismissBanner])
return (
<div className="flex flex-col">
<div className="shrink-0">

View File

@@ -1,43 +1,62 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import dayjs from 'dayjs'
import { useRouter } from 'next/router'
import {
Button,
cn,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
} from 'ui'
import { TimestampInfo } from 'ui-patterns'
import { HeaderBanner } from '@/components/interfaces/Organization/HeaderBanner'
import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
// Update this whenever the banner content below changes so old client bundles
// stop displaying outdated notices after the relevant date passes.
const BANNER_EXPIRES_AT = new Date('2026-07-04T00:00:00Z')
const BANNER_EXPIRES_AT = new Date('2026-06-01T15:00:00Z')
const SUPAVISOR_UPDATE_REGIONS = {
'eu-central-1': {
start: Date.UTC(2026, 4, 26, 13, 0, 0),
end: Date.UTC(2026, 4, 26, 15, 0, 0),
url: 'https://status.supabase.com/incidents/jy1tm4wfs68t',
},
'eu-west-1': {
start: Date.UTC(2026, 4, 27, 13, 0, 0),
end: Date.UTC(2026, 4, 27, 15, 0, 0),
url: 'https://status.supabase.com/incidents/3t293hpd545z',
},
'us-west-1': {
start: Date.UTC(2026, 4, 28, 13, 0, 0),
end: Date.UTC(2026, 4, 28, 15, 0, 0),
url: 'https://status.supabase.com/incidents/8f72bnv3xs8r',
},
'us-east-1': {
start: Date.UTC(2026, 5, 1, 13, 0, 0),
end: Date.UTC(2026, 5, 1, 15, 0, 0),
url: 'https://status.supabase.com/incidents/7zgmgh2p343n',
},
}
/**
* Used to display urgent notices that apply for all users, such as maintenance windows.
*/
export const NoticeBanner = () => {
const router = useRouter()
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const [bannerAcknowledged, setBannerAcknowledged, { isSuccess }] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.TERMS_OF_SERVICE_UPDATE,
LOCAL_STORAGE_KEYS.SUPAVISOR_MAINTENANCE(ref ?? ''),
false
)
const region = project?.region ?? ''
const maintenanceWindow =
SUPAVISOR_UPDATE_REGIONS[region as keyof typeof SUPAVISOR_UPDATE_REGIONS]
if (
Date.now() >= BANNER_EXPIRES_AT.getTime() ||
router.pathname.includes('sign-in') ||
!isSuccess ||
!project ||
!maintenanceWindow ||
bannerAcknowledged
) {
return null
@@ -46,65 +65,23 @@ export const NoticeBanner = () => {
return (
<HeaderBanner
variant="note"
title="We've updated our Terms of Service"
description={<UpdatedTermsOfServiceDialog onDismiss={() => setBannerAcknowledged(true)} />}
title="Upcoming maintenance"
description={
<>
Shared pooler maintenance in{' '}
<a target="_blank" rel="noopener referrer" href={maintenanceWindow.url}>
{project.region}
</a>{' '}
on{' '}
<TimestampInfo
className="text-sm"
utcTimestamp={maintenanceWindow.start}
label={dayjs(maintenanceWindow.start).format('DD MMM, HH:mm')}
/>
.
</>
}
onDismiss={() => setBannerAcknowledged(true)}
/>
)
}
const UpdatedTermsOfServiceDialog = ({ onDismiss }: { onDismiss: () => void }) => {
return (
<Dialog>
<DialogTrigger className={cn(InlineLinkClassName, 'cursor-pointer')}>
Learn more
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Terms of Service update</DialogTitle>
<DialogDescription>
We've updated our Terms of Service to better define the responsibilities of both you and
Supabase in the use of AI.
</DialogDescription>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="text-sm flex flex-col gap-y-2">
<p>
We've clarified how we use AI in our customer support tooling, introduced guidelines for
the responsible use of AI by our users, and updated our indemnification terms to clarify
the allocation of responsibility for claims arising from AI-generated inputs and
outputs.
</p>
<p>
Additionally, we've made an explicit commitment that Supabase will never use the data
you submit to the Supabase services to train or improve any AI without your prior
written consent.
</p>
<p>
The updated Terms (Version 2) will take effect on June 6, 2026. By continuing to use the
Services after that date, you agree to the updated Terms. You can review the changes{' '}
<InlineLink href="https://supabase.com/terms">here</InlineLink>.
</p>
<p>
This notice applies to users on Supabase's standard Terms of Service only. If you are on
an Enterprise plan or with a separately negotiated agreement, your existing terms
continue to govern your use of the Services.
</p>
</DialogSection>
<DialogFooter>
<DialogClose asChild>
<Button type="default" className="opacity-100" onClick={onDismiss}>
Understood
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -7,6 +7,7 @@ export const BANNER_ID = {
RLS_EVENT_TRIGGER: 'rls-event-trigger-banner',
RLS_TESTER: 'rls-tester-banner',
FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner',
TOS_UPDATE: 'tos-update-banner',
} as const
export type BannerId = (typeof BANNER_ID)[keyof typeof BANNER_ID]

View File

@@ -0,0 +1,123 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import {
Badge,
Button,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
} from 'ui'
import { InlineLink } from '../../InlineLink'
import { BannerCard } from '../BannerCard'
import { useBannerStack } from '../BannerStackProvider'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
/**
* [Joshen] TOS update takes place from 6th June onwards, can remove from 4th July onwards as
* previously stated in the NoticeBanner
*/
export const BannerTOSUpdate = () => {
const { dismissBanner } = useBannerStack()
const [, setTOSUpdateAcknowledged] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.TERMS_OF_SERVICE_UPDATE,
false
)
return (
<BannerCard
onDismiss={() => {
setTOSUpdateAcknowledged(true)
dismissBanner('tos-update-banner')
}}
>
<div className="flex flex-col gap-y-2">
<Badge variant="default" className="w-min -ml-0.5 uppercase inline-flex items-center mb-2">
Notice
</Badge>
<div className="flex flex-col gap-y-1 mb-2">
<p className="text-sm font-medium">We've updated our Terms of Service</p>
<p className="text-xs text-foreground-lighter text-balance">
Updates define the responsibilities of both you and Supabase in the use of AI.
</p>
</div>
<UpdatedTermsOfServiceDialog />
</div>
</BannerCard>
)
}
const UpdatedTermsOfServiceDialog = () => {
const [, setTOSUpdateAcknowledged] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.TERMS_OF_SERVICE_UPDATE,
false
)
return (
<Dialog>
<DialogTrigger asChild>
<Button type="default" className="w-min">
Learn more
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Terms of Service update</DialogTitle>
<DialogDescription>
We've updated our Terms of Service to better define the responsibilities of both you and
Supabase in the use of AI.
</DialogDescription>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="text-sm flex flex-col gap-y-2">
<p>
We've clarified how we use AI in our customer support tooling, introduced guidelines for
the responsible use of AI by our users, and updated our indemnification terms to clarify
the allocation of responsibility for claims arising from AI-generated inputs and
outputs.
</p>
<p>
Additionally, we've made an explicit commitment that Supabase will never use the data
you submit to the Supabase services to train or improve any AI without your prior
written consent.
</p>
<p>
The updated Terms (Version 2) will take effect on June 6, 2026. By continuing to use the
Services after that date, you agree to the updated Terms. You can review the changes{' '}
<InlineLink href="https://supabase.com/terms">here</InlineLink>.
</p>
<p>
This notice applies to users on Supabase's standard Terms of Service only. If you are on
an Enterprise plan or with a separately negotiated agreement, your existing terms
continue to govern your use of the Services.
</p>
</DialogSection>
<DialogFooter>
<DialogClose asChild>
<Button
type="default"
className="opacity-100"
onClick={() => setTOSUpdateAcknowledged(true)}
>
Understood
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -5,14 +5,12 @@ export const LOCAL_STORAGE_KEYS = {
AI_ASSISTANT_STATE: (projectRef: string | undefined) =>
`supabase-ai-assistant-state-${projectRef}`,
SIDEBAR_BEHAVIOR: 'supabase-sidebar-behavior',
EDITOR_PANEL_STATE: 'supabase-editor-panel-state',
PROJECTS_VIEW: 'projects-view',
PROJECTS_FILTER: 'projects-filter',
PROJECTS_SORT: 'projects-sort',
FEEDBACK_WIDGET_CONTENT: 'feedback-widget-content',
FEEDBACK_WIDGET_SCREENSHOT: 'feedback-widget-screenshot',
INCIDENT_BANNER_DISMISSED_IDS: 'incident-banner-dismissed-ids',
MAINTENANCE_BANNER_DISMISSED: (id: string) => `maintenance-banner-dismissed-${id}`,
DASHBOARD_PREFERENCES: (ref: string) => `dashboard-preferences-${ref}`,
UNIFIED_LOGS_DOCK: 'unified-logs-dock',
@@ -20,7 +18,6 @@ export const LOCAL_STORAGE_KEYS = {
UI_PREVIEW_CLS: 'supabase-ui-cls',
UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor',
UI_PREVIEW_UNIFIED_LOGS: 'supabase-ui-preview-unified-logs',
UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown',
UI_PREVIEW_ADVISOR_RULES: 'supabase-ui-advisor-rules',
UI_PREVIEW_QUEUE_OPERATIONS: 'supabase-ui-queue-operations',
UI_PREVIEW_PG_DELTA_DIFF: 'supabase-ui-pg-delta-diff',
@@ -29,8 +26,6 @@ export const LOCAL_STORAGE_KEYS = {
UI_PREVIEW_RLS_TESTER: 'supabase-ui-rls-tester',
UI_PREVIEW_MARKETPLACE: 'supabase-ui-marketplace',
NEW_LAYOUT_NOTICE_ACKNOWLEDGED: 'new-layout-notice-acknowledge',
TABS_INTERFACE_ACKNOWLEDGED: 'tabs-interface-acknowledge',
AI_ASSISTANT_MCP_OPT_IN: 'ai-assistant-mcp-opt-in',
DASHBOARD_HISTORY: (ref: string) => `dashboard-history-${ref}`,
@@ -53,7 +48,6 @@ export const LOCAL_STORAGE_KEYS = {
SQL_EDITOR_SORT: (ref: string) => `sql-editor-sort-${ref}`,
LOG_EXPLORER_SPLIT_SIZE: 'supabase_log-explorer-split-size',
GRAPHIQL_RLS_BYPASS_WARNING: 'graphiql-rls-bypass-warning-dismissed',
GRAPHQL_INTROSPECTION_NOTICE_COLLAPSED: (ref: string) =>
`graphql-introspection-notice-collapsed-${ref}`,
CLS_DIFF_WARNING: 'cls-diff-warning-dismissed',
@@ -78,6 +72,7 @@ export const LOCAL_STORAGE_KEYS = {
FLY_DEPRECATION_2026_05_31: 'fly-deprecation-2026-05-31-dismissed',
API_KEYS_FEEDBACK_DISMISSED: (ref: string) => `supabase-api-keys-feedback-dismissed-${ref}`,
TERMS_OF_SERVICE_UPDATE: 'terms-of-service-update-2026-06-06',
SUPAVISOR_MAINTENANCE: (ref: string) => `supavisor-maintenance-2026-05-21-${ref}`,
REPORT_DATERANGE: 'supabase-report-daterange',
PROJECT_PAUSING_STARTED_AT: (ref: string) => `supabase-project-pausing-started-at-${ref}`,
PROJECT_RESTORING_STARTED_AT: (ref: string) => `supabase-project-restoring-started-at-${ref}`,
@@ -119,7 +114,6 @@ export const LOCAL_STORAGE_KEYS = {
`free-micro-upgrade-banner-dismissed-${ref}`,
STORAGE_PUBLIC_BUCKET_SELECT_POLICY_WARNING_DISMISSED: (ref: string, bucketId: string) =>
`storage-public-bucket-select-policy-warning-dismissed-${ref}-${bucketId}`,
PRIVACY_NOTICE_ACKNOWLEDGED: 'privacy-notice-acknowledged-2026-03',
/**
* COMMON