Files
Danny White bd02d7f297 feat(www + studio): broaden Select 2026 banner reach and soften www edges (#49569)
## What kind of change does this PR introduce?

Feature polish for the Select 2026 promotion.

## What is the current behavior?

- Studio only shows the Select Banner Stack card inside `ProjectLayout`
(project routes).
- www glyph fields read as hard rectangles on each side of the
announcement banner.

## What is the new behavior?

- Studio registers the Select banner from `AppBannerWrapper`, so it also
appears outside project context (org / account surfaces).
- www fields use per-row widths with edge alignment: top/middle shorter,
bottom longer, growing inward from each side for a softer silhouette.


https://github.com/user-attachments/assets/c5275967-63d0-40dd-a472-e3db08d1c39d

## To test

- **Studio (non-project):** open an org home or account page on the
deploy preview. Confirm the Select Banner Stack card appears in the
usual stack and dismisses as before.
- **Studio (project):** open any project route. Confirm the same card
still appears and does not double-register.
- **www:** open the marketing homepage. On `sm+`, confirm each side of
the cream announcement bar has a 3-row field where the bottom row
reaches further inward than the top, and the middle row is shortest.

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

* **New Features**
* Added the Select 2026 promotional banner to eligible hosted Studio
environments.
* Banner visibility reflects promotion status, platform eligibility,
loading state, and dismissal preferences.

* **Style**
* Refined decorative field layouts with improved row alignment, mirrored
visuals, and flexible sizing.

* **Bug Fixes**
* Updated visibility behavior so the banner is no longer tied to being
inside a specific project.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-26 13:28:15 +08:00

168 lines
5.3 KiB
TypeScript

import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useFlag } from 'common'
import dayjs from 'dayjs'
import { usePathname } from 'next/navigation'
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
import {
SELECT_26_STUDIO_DISMISSAL_KEY,
useSelect26PromotionActive,
} from 'ui-patterns/Banners/Select26Promotion'
import { OrganizationResourceBanner } from '../Organization/HeaderBanner'
import { isLogsOrObservabilityPath } from './AppBannerWrapper.utils'
import { ClockSkewBanner } from '@/components/layouts/AppLayout/ClockSkewBanner'
import { NoticeBanner } from '@/components/layouts/AppLayout/NoticeBanner'
import { StatusPageBanner } from '@/components/layouts/AppLayout/StatusPageBanner'
import { BannerLogsAllDeprecation } from '@/components/ui/BannerStack/Banners/BannerLogsAllDeprecation'
import { BannerSelect2026 } from '@/components/ui/BannerStack/Banners/BannerSelect2026'
import {
SELECT_26_BANNER_PRIORITY,
shouldShowSelect26Banner,
} from '@/components/ui/BannerStack/Banners/BannerSelect2026.utils'
import { BannerTOSUpdate } from '@/components/ui/BannerStack/Banners/BannerTOSUpdate'
import { BANNER_ID, useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useTrack } from '@/lib/telemetry/track'
const TOSUpdateExpiry = new Date('2026-08-29T00:00:00Z')
// Update this whenever the banner content changes so old client bundles stop
// displaying the notice after the removal date passes.
const LogsAllDeprecationExpiry = dayjs('2026-09-24T00:00:00Z')
// setTimeout overflows above ~24.8 days; re-arm until the real expiry.
const MAX_TIMEOUT_MS = 2_147_483_647
export const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
const showNoticeBanner = useFlag('showNoticeBanner')
const clockSkewBanner = useFlag('clockSkewBanner')
const { addBanner, dismissBanner } = useBannerStack()
const pathname = usePathname()
const track = useTrack()
const [TOSUpdateAcknowledged, , { isSuccess }] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.TERMS_OF_SERVICE_UPDATE,
false
)
const [isSelect26BannerDismissed, , { isSuccess: isSelect26DismissalLoaded }] =
useLocalStorageQuery(SELECT_26_STUDIO_DISMISSAL_KEY, false)
const isSelect26PromotionActive = useSelect26PromotionActive()
useEffect(() => {
if (!isSelect26DismissalLoaded) return
const shouldShow = shouldShowSelect26Banner({
isPlatform: IS_PLATFORM,
dismissalLoaded: isSelect26DismissalLoaded,
isActive: isSelect26PromotionActive,
isDismissed: isSelect26BannerDismissed,
})
if (shouldShow) {
addBanner({
id: BANNER_ID.SELECT_26,
isDismissed: false,
content: <BannerSelect2026 />,
priority: SELECT_26_BANNER_PRIORITY,
})
} else {
dismissBanner(BANNER_ID.SELECT_26)
}
}, [
isSelect26DismissalLoaded,
isSelect26PromotionActive,
isSelect26BannerDismissed,
addBanner,
dismissBanner,
])
useEffect(() => {
if (Date.now() >= TOSUpdateExpiry.getTime()) return
if (isSuccess && !TOSUpdateAcknowledged) {
addBanner({
id: 'tos-update-banner',
isDismissed: false,
content: <BannerTOSUpdate />,
priority: 0,
})
} else {
dismissBanner('tos-update-banner')
}
}, [TOSUpdateAcknowledged, isSuccess, addBanner, dismissBanner])
const [isLogsAllDeprecationDismissed, , { isSuccess: isLogsAllDeprecationLoaded }] =
useLocalStorageQuery(LOCAL_STORAGE_KEYS.LOGS_ALL_DEPRECATION_2026_09_23, false)
const [isLogsAllDeprecationExpired, setIsLogsAllDeprecationExpired] = useState(
() => !dayjs().isBefore(LogsAllDeprecationExpiry)
)
useEffect(() => {
if (isLogsAllDeprecationExpired) return
let timeoutId: ReturnType<typeof setTimeout> | undefined
const armExpiryTimer = () => {
const msUntilExpiry = LogsAllDeprecationExpiry.diff(dayjs())
if (msUntilExpiry <= 0) {
setIsLogsAllDeprecationExpired(true)
return
}
timeoutId = setTimeout(armExpiryTimer, Math.min(msUntilExpiry, MAX_TIMEOUT_MS))
}
armExpiryTimer()
return () => clearTimeout(timeoutId)
}, [isLogsAllDeprecationExpired])
const hasTrackedLogsAllExposure = useRef(false)
useEffect(() => {
if (!isLogsAllDeprecationLoaded || pathname == null) return
const shouldShow =
IS_PLATFORM &&
!isLogsAllDeprecationExpired &&
isLogsOrObservabilityPath(pathname) &&
!isLogsAllDeprecationDismissed
if (!shouldShow) {
dismissBanner(BANNER_ID.LOGS_ALL_DEPRECATION)
return
}
addBanner({
id: BANNER_ID.LOGS_ALL_DEPRECATION,
isDismissed: false,
content: <BannerLogsAllDeprecation />,
priority: 4,
})
if (!hasTrackedLogsAllExposure.current) {
hasTrackedLogsAllExposure.current = true
track('logs_all_deprecation_banner_exposed')
}
}, [
pathname,
isLogsAllDeprecationLoaded,
isLogsAllDeprecationDismissed,
isLogsAllDeprecationExpired,
addBanner,
dismissBanner,
track,
])
return (
<div className="flex flex-col">
<div className="shrink-0">
<StatusPageBanner />
{showNoticeBanner && <NoticeBanner />}
<OrganizationResourceBanner />
{clockSkewBanner && <ClockSkewBanner />}
</div>
{children}
</div>
)
}