Files
Danny White b5c865521f feat(studio): notify users about the Terms of Service update (#51302)
## Problem

Dashboard users need a notice about the Terms of Service update
alongside #51106 and #51107.

## Solution

Reuse the organisation landing-page notice pattern from #50397. A
compact “We've updated our Terms of Service.” notice opens the
explanation and agreement link through **Learn more**. Closing the
notice or choosing **Got it** remembers dismissal in the browser with a
new version-specific key.

```text
Organisation landing page
  Notice → Learn more → Explanation and Terms of Service link
  Close / Got it → Remember dismissal
```

| After |
| --- |
| <img width="628" height="444" alt="CleanShot 2026-10-06 at 14 47
46@2x"
src="https://github.com/user-attachments/assets/72922712-321f-4972-b20c-1a075e0745d0"
/> |
| _Banner_ |
| <img width="1078" height="718" alt="CleanShot 2026-10-06 at 16 58
18@2x"
src="https://github.com/user-attachments/assets/2109c2ab-e9b7-4855-8acb-42d5232cd002"
/> |
| _Dialog_ |

## Review instructions

1. Open `/organizations` or an organisation's `/org/<slug>` landing page
in the hosted Studio preview. Expect the compact notice.
2. Click **Learn more**. Expect the explanation and a link to the Terms
of Service. Escape closes the dialog without dismissing the notice.
3. Choose **Got it**, then reload. The notice stays dismissed. Repeat in
a fresh browser profile using **Close banner**.
4. Open a project or an organisation settings page. The notice should
not appear.
2026-10-07 09:09:01 +11:00

69 lines
2.3 KiB
TypeScript

import { createContext, useCallback, useContext, useState } from 'react'
export const BANNER_ID = {
INDEX_ADVISOR: 'index-advisor-banner',
TABLE_EDITOR_QUEUE_OPERATIONS: 'table-editor-queue-operations-banner',
RLS_EVENT_TRIGGER: 'rls-event-trigger-banner',
FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner',
LOGS_ALL_DEPRECATION: 'logs-all-deprecation-banner',
TERMS_OF_SERVICE_UPDATE: 'terms-of-service-update-banner',
EXPLORER: 'explorer-banner',
} as const
export type BannerId = (typeof BANNER_ID)[keyof typeof BANNER_ID]
export interface Banner {
id: BannerId
content: React.ReactNode
isDismissed: boolean
priority?: number
onDismiss?: () => void
}
interface BannerStackContextType {
banners: Banner[]
addBanner: (banner: Banner) => void
dismissBanner: (id: BannerId) => void
}
const BannerStackContext = createContext<BannerStackContextType | undefined>(undefined)
export const BannerStackProvider = ({ children }: { children: React.ReactNode }) => {
const [banners, setBanners] = useState<Banner[]>([])
const addBanner = useCallback((banner: Banner) => {
setBanners((prev) => {
const existingIndex = prev.findIndex((b) => b.id === banner.id)
if (existingIndex !== -1) {
if (!prev[existingIndex].isDismissed) return prev
const revived = [...prev]
revived[existingIndex] = banner
return revived.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
}
const newBanners = [...prev, banner]
return newBanners.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
})
}, [])
const dismissBanner = useCallback((id: string) => {
setBanners((prev) => prev.map((b) => (b.id === id ? { ...b, isDismissed: true } : b)))
setTimeout(() => {
// A later addBanner can revive this id before the exit animation
// finishes. Drop it only if it is still dismissed.
setBanners((prev) => prev.filter((b) => b.id !== id || !b.isDismissed))
}, 300)
}, [])
return (
<BannerStackContext.Provider value={{ banners, addBanner, dismissBanner }}>
{children}
</BannerStackContext.Provider>
)
}
export const useBannerStack = () => {
const context = useContext(BannerStackContext)
if (!context) throw new Error('useBannerStack must be used within BannerStackProvider')
return context
}