Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/common/src/services/remote-config/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ export const environmentFlagDefaults: Record<
Partial<FlagDefaults>
> = {
development: {
[FeatureFlags.FAN_CLUB_TEXT_POST_POSTING]: true,
[FeatureFlags.WEEKLY_ROTATION]: true
[FeatureFlags.FAN_CLUB_TEXT_POST_POSTING]: true
},
production: {}
}
Expand All @@ -52,5 +51,9 @@ export const flagDefaults: FlagDefaults = {
[FeatureFlags.LAUNCHPAD_VERIFICATION]: true,
[FeatureFlags.FAN_CLUB_TEXT_POST_POSTING]: false,
[FeatureFlags.QUEUE_NEW_FEATURE_BADGE]: false,
[FeatureFlags.WEEKLY_ROTATION]: false
// Launched 2026-09-13 at 100%. Defaulting on means a shared
// /explore/weekly-rotation/:handle link still renders when the Optimizely
// datafile hasn't loaded (a first visit on a cold cache) instead of
// bouncing the visitor to Explore; the remote flag stays the kill switch.
[FeatureFlags.WEEKLY_ROTATION]: true
}
Binary file modified packages/mobile/src/assets/images/weeklyRotation.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,43 @@ function syncStatusLabel(status: CodePush.SyncStatus): string {

type BannerPhase = 'none' | 'pending'

/**
* How long to keep polling for a pending package after mount/foreground, and
* how often. A production bundle takes tens of seconds to download on a
* phone, and the download is usually owned by the CodePush root HOC's own
* sync (ota-root), not by this component's -- see `prefetchUpdate`.
*/
const PENDING_POLL_INTERVAL_MS = 2000
const PENDING_POLL_WINDOW_MS = 3 * 60 * 1000

/**
* CodePush is configured in ota-root (ON_APP_RESUME) to fetch updates with
* ON_NEXT_RESTART. We only surface UI when an update is already downloaded
* and installed as pending — then the user restarts when they want.
*
* One instance mounts per root tab header, so each tab that is open polls on
* its own. The first tab (Feed) mounts while the root HOC's sync is already
* running, so its own `CodePush.sync` returns SYNC_IN_PROGRESS immediately
* and never observes the install; it has to find the pending package by
* polling. Before the poll window was long enough, the banner only ever
* appeared on a tab mounted *after* the download finished (Trending).
*/
export const OtaUpdateBanner = () => {
const { color, spacing } = useTheme()
const [phase, setPhase] = useState<BannerPhase>('none')
const dismissedRef = useRef(false)
const pendingLoggedRef = useRef(false)
const pollTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([])
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

const refresh = useCallback(async () => {
/** Re-reads CodePush state; resolves to whether a pending package exists. */
const refresh = useCallback(async (): Promise<boolean> => {
if (!isOtaEnabled()) {
if (!FORCE_OTA_BANNER_PREVIEW) {
setPhase('none')
} else {
setPhase('pending')
}
return
return false
}
try {
const pending = await CodePush.getUpdateMetadata(
Expand All @@ -84,34 +101,52 @@ export const OtaUpdateBanner = () => {
)
}
setPhase(dismissedRef.current ? 'none' : 'pending')
return
return true
}
pendingLoggedRef.current = false
setPhase('none')
return false
} catch (e) {
console.warn(
'[OTA] getUpdateMetadata(PENDING) failed',
e,
`lastHistoryEndpoint=${getOtaLastHistoryEndpointForLogs()}`
)
setPhase('none')
return false
}
}, [])

const stopPendingPolls = useCallback(() => {
if (pollTimeoutRef.current) {
clearTimeout(pollTimeoutRef.current)
pollTimeoutRef.current = null
}
}, [])

// Poll until a pending package shows up or the window closes. Polling
// (rather than a fixed handful of early checks) is what lets the tab that
// was open during the download -- normally Feed, the initial tab -- show
// the banner instead of only the tab opened afterwards.
const schedulePendingPolls = useCallback(() => {
pollTimeoutsRef.current.forEach(clearTimeout)
pollTimeoutsRef.current = []
stopPendingPolls()
if (!isOtaEnabled()) {
return
}
const delaysMs = [0, 800, 2000, 4000]
delaysMs.forEach((ms) => {
const id = setTimeout(() => {
refresh().catch(() => {})
}, ms)
pollTimeoutsRef.current.push(id)
})
}, [refresh])
const startedAt = Date.now()
const tick = () => {
pollTimeoutRef.current = null
refresh()
.catch(() => false)
.then((pending) => {
if (pending || Date.now() - startedAt >= PENDING_POLL_WINDOW_MS) {
return
}
pollTimeoutRef.current = setTimeout(tick, PENDING_POLL_INTERVAL_MS)
})
}
tick()
}, [refresh, stopPendingPolls])

const prefetchUpdate = useCallback(async () => {
if (!isOtaEnabled()) {
Expand Down Expand Up @@ -165,11 +200,8 @@ export const OtaUpdateBanner = () => {
refresh().catch(() => {})
prefetchUpdate().catch(() => {})
schedulePendingPolls()
return () => {
pollTimeoutsRef.current.forEach(clearTimeout)
pollTimeoutsRef.current = []
}
}, [refresh, prefetchUpdate, schedulePendingPolls])
return stopPendingPolls
}, [refresh, prefetchUpdate, schedulePendingPolls, stopPendingPolls])

const handleDismiss = useCallback(() => {
dismissedRef.current = true
Expand Down
Binary file modified packages/web/src/assets/img/weeklyRotation.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading