FlyDeprecationBanner.tsx222 lines · main
1import { LOCAL_STORAGE_KEYS } from 'common'
2import { useRouter } from 'next/router'
3import { useEffect, useRef, type ReactNode } from 'react'
4import {
5 Button,
6 cn,
7 Dialog,
8 DialogClose,
9 DialogContent,
10 DialogDescription,
11 DialogFooter,
12 DialogHeader,
13 DialogSection,
14 DialogSectionSeparator,
15 DialogTitle,
16 DialogTrigger,
17} from 'ui'
18
19import { HeaderBanner } from '@/components/interfaces/Organization/HeaderBanner'
20import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink'
21import {
22 useFlyDeprecationProjects,
23 type FlyDeprecationProject,
24} from '@/hooks/misc/useFlyDeprecationProjects'
25import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
26import { useTrack } from '@/lib/telemetry/track'
27
28const BANNER_EXPIRES_AT = new Date('2026-06-01T00:00:00Z')
29const BACKUP_RESTORE_CLI_URL =
30 'https://supabase.com/docs/guides/platform/migrating-within-supabase/backup-restore'
31const DASHBOARD_RESTORE_URL =
32 'https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore'
33const BRANCHING_DASHBOARD_URL = 'https://supabase.com/docs/guides/deployment/branching/dashboard'
34const SUPPORT_EMAIL = 'success@supabase.io'
35
36export const FlyDeprecationBanner = () => {
37 const router = useRouter()
38
39 const [acknowledged, setAcknowledged, { isSuccess }] = useLocalStorageQuery(
40 LOCAL_STORAGE_KEYS.FLY_DEPRECATION_2026_05_31,
41 false
42 )
43
44 const isExpired = Date.now() >= BANNER_EXPIRES_AT.getTime()
45 const onSignIn = router.pathname.startsWith('/sign-in')
46
47 const shouldEvaluate = !isExpired && !onSignIn && isSuccess && !acknowledged
48
49 const { isReady, primaries, branches } = useFlyDeprecationProjects({ enabled: shouldEvaluate })
50
51 const hasFlyResources = primaries.length > 0 || branches.length > 0
52
53 const track = useTrack()
54
55 const exposedRef = useRef(false)
56 useEffect(() => {
57 if (!shouldEvaluate || !isReady || !hasFlyResources || exposedRef.current) return
58 exposedRef.current = true
59 track('fly_deprecation_banner_exposed', {
60 primaryCount: primaries.length,
61 branchCount: branches.length,
62 })
63 }, [shouldEvaluate, isReady, hasFlyResources, primaries.length, branches.length, track])
64
65 if (!shouldEvaluate || !isReady || !hasFlyResources) return null
66
67 const onDismiss = () => {
68 track('fly_deprecation_banner_dismissed', {
69 primaryCount: primaries.length,
70 branchCount: branches.length,
71 })
72 setAcknowledged(true)
73 }
74
75 const title =
76 primaries.length > 0 && branches.length > 0
77 ? 'Action required: Fly.io project and branch suspensions begin May 31'
78 : primaries.length > 0
79 ? 'Action required: Fly.io project suspensions begin May 31'
80 : 'Action required: Fly.io branch suspensions begin May 31'
81
82 return (
83 <HeaderBanner
84 variant="warning"
85 title={title}
86 description={<FlyDeprecationDialog primaries={primaries} branches={branches} />}
87 onDismiss={onDismiss}
88 />
89 )
90}
91
92const FlyDeprecationDialog = ({
93 primaries,
94 branches,
95}: {
96 primaries: FlyDeprecationProject[]
97 branches: FlyDeprecationProject[]
98}) => {
99 return (
100 <Dialog>
101 <DialogTrigger className={cn(InlineLinkClassName, 'cursor-pointer')}>
102 View affected projects
103 </DialogTrigger>
104 <DialogContent>
105 <DialogHeader>
106 <DialogTitle>Fly.io deprecation: suspensions begin May 31, 2026</DialogTitle>
107 <DialogDescription>
108 Briven will begin suspending projects and branches still running on Fly.io
109 infrastructure on May 31, 2026.
110 </DialogDescription>
111 </DialogHeader>
112
113 <DialogSectionSeparator />
114
115 <ProjectList
116 label="Projects on Fly.io"
117 items={primaries}
118 instructions={
119 <>
120 <p>
121 To preserve your data, migrate each project to Briven's general infrastructure:
122 </p>
123 <ol className="list-decimal pl-5 space-y-1">
124 <li>
125 Back up your database using the{' '}
126 <InlineLink href={BACKUP_RESTORE_CLI_URL}>Briven CLI</InlineLink> (or take a{' '}
127 <InlineLink href={DASHBOARD_RESTORE_URL}>Dashboard backup</InlineLink>).
128 </li>
129 <li>Create a new project on Briven's general infrastructure.</li>
130 <li>Restore the backup into the new project.</li>
131 </ol>
132 </>
133 }
134 />
135
136 <ProjectList
137 label="Branches on Fly.io"
138 items={branches}
139 instructions={
140 <>
141 <p>Merge preview branches before May 31. For persistent branches:</p>
142 <ol className="list-decimal pl-5 space-y-1">
143 <li>
144 Take a{' '}
145 <InlineLink href={BACKUP_RESTORE_CLI_URL}>
146 snapshot of the branch database
147 </InlineLink>{' '}
148 before shutting it down.
149 </li>
150 <li>
151 <InlineLink href={BRANCHING_DASHBOARD_URL}>
152 Deploy a new persistent branch
153 </InlineLink>{' '}
154 on Briven's general infrastructure.
155 </li>
156 <li>Restore your data manually from the snapshot.</li>
157 </ol>
158 </>
159 }
160 />
161
162 <DialogSection className="text-sm">
163 <p>
164 Questions or need an extension? Email{' '}
165 <a className={InlineLinkClassName} href={`mailto:${SUPPORT_EMAIL}`}>
166 {SUPPORT_EMAIL}
167 </a>
168 .
169 </p>
170 </DialogSection>
171
172 <DialogFooter>
173 <DialogClose asChild>
174 <Button type="default">Close</Button>
175 </DialogClose>
176 </DialogFooter>
177 </DialogContent>
178 </Dialog>
179 )
180}
181
182const MAX_LISTED = 5
183
184const ProjectList = ({
185 label,
186 items,
187 instructions,
188}: {
189 label: string
190 items: FlyDeprecationProject[]
191 instructions: ReactNode
192}) => {
193 if (items.length === 0) return null
194 const visible = items.slice(0, MAX_LISTED)
195 const remaining = items.length - visible.length
196 return (
197 <>
198 <DialogSection className="text-sm flex flex-col gap-y-2">
199 <p className="font-medium">
200 {label} ({items.length})
201 </p>
202 {items.length === 1 ? (
203 <p>
204 {items[0].name} <span className="text-foreground-muted">({items[0].orgName})</span>
205 </p>
206 ) : (
207 <ul className="list-disc pl-5 space-y-1">
208 {visible.map((p) => (
209 <li key={p.ref}>
210 {p.name} <span className="text-foreground-muted">({p.orgName})</span>
211 </li>
212 ))}
213 {remaining > 0 && (
214 <li className="text-foreground-muted list-none -ml-5">…and {remaining} more.</li>
215 )}
216 </ul>
217 )}
218 </DialogSection>
219 <DialogSection className="text-sm flex flex-col gap-y-2">{instructions}</DialogSection>
220 </>
221 )
222}