diff --git a/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx b/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx
new file mode 100644
index 0000000000..a25b26947c
--- /dev/null
+++ b/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx
@@ -0,0 +1,132 @@
+import {useMemo} from 'react'
+import {View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
+import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
+import {atoms as a, select, useTheme} from '#/alf'
+import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
+import {Button} from '#/components/Button'
+import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
+import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {Link} from '#/components/Link'
+import {Text} from '#/components/Typography'
+
+export function useInternalState() {
+ const {isReady, isDeclaredUnderage, isAgeRestricted, lastInitiatedAt} =
+ useAgeAssurance()
+ const {nux} = useNux(Nux.AgeAssuranceDismissibleHeaderButton)
+ const {mutate: save, variables} = useSaveNux()
+ const hidden = !!variables
+
+ const visible = useMemo(() => {
+ if (!isReady) return false
+ if (isDeclaredUnderage) return false
+ if (!isAgeRestricted) return false
+ if (lastInitiatedAt) return false
+ if (hidden) return false
+ if (nux && nux.completed) return false
+ return true
+ }, [
+ isReady,
+ isDeclaredUnderage,
+ isAgeRestricted,
+ lastInitiatedAt,
+ hidden,
+ nux,
+ ])
+
+ const close = () => {
+ save({
+ id: Nux.AgeAssuranceDismissibleHeaderButton,
+ completed: true,
+ data: undefined,
+ })
+ }
+
+ return {visible, close}
+}
+
+export function AgeAssuranceDismissibleFeedBanner() {
+ const t = useTheme()
+ const {_} = useLingui()
+ const {visible, close} = useInternalState()
+ const copy = useAgeAssuranceCopy()
+
+ if (!visible) return null
+
+ return (
+
+
+
+
+
+
+
+ {copy.banner}
+
+
+
+
+
+ )
+}
diff --git a/src/components/ageAssurance/AgeAssuranceDismissibleHeaderButton.tsx b/src/components/ageAssurance/AgeAssuranceDismissibleHeaderButton.tsx
deleted file mode 100644
index b6505fb0e0..0000000000
--- a/src/components/ageAssurance/AgeAssuranceDismissibleHeaderButton.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import {useMemo} from 'react'
-import {View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
-import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
-import {atoms as a, select, useTheme} from '#/alf'
-import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
-import {Link} from '#/components/Link'
-import {Text} from '#/components/Typography'
-
-export function useInternalState() {
- const {isReady, isDeclaredUnderage, isAgeRestricted, lastInitiatedAt} =
- useAgeAssurance()
- const {nux} = useNux(Nux.AgeAssuranceDismissibleHeaderButton)
- const {mutate: save, variables} = useSaveNux()
- const hidden = !!variables
-
- const visible = useMemo(() => {
- if (!isReady) return false
- if (isDeclaredUnderage) return false
- if (!isAgeRestricted) return false
- if (lastInitiatedAt) return false
- if (hidden) return false
- if (nux && nux.completed) return false
- return true
- }, [
- isReady,
- isDeclaredUnderage,
- isAgeRestricted,
- lastInitiatedAt,
- hidden,
- nux,
- ])
-
- const close = () => {
- save({
- id: Nux.AgeAssuranceDismissibleHeaderButton,
- completed: true,
- data: undefined,
- })
- }
-
- return {visible, close}
-}
-
-export function AgeAssuranceDismissibleHeaderButton() {
- const t = useTheme()
- const {_} = useLingui()
- const {visible, close} = useInternalState()
-
- if (!visible) return null
-
- return (
-
-
-
-
- Age Assurance
-
-
-
- )
-}
diff --git a/src/components/ageAssurance/useAgeAssuranceCopy.ts b/src/components/ageAssurance/useAgeAssuranceCopy.ts
index 045806994e..f3f96cd5c6 100644
--- a/src/components/ageAssurance/useAgeAssuranceCopy.ts
+++ b/src/components/ageAssurance/useAgeAssuranceCopy.ts
@@ -10,6 +10,9 @@ export function useAgeAssuranceCopy() {
notice: _(
msg`The laws in your location require that you verify your age before accessing certain features on Bluesky like adult content and direct messaging.`,
),
+ banner: _(
+ msg`The laws in your location require you verify your age. Tap to learn more.`,
+ ),
chatsInfoText: _(
msg`Don't worry! All existing messages and settings are saved and will be available after you've been verified to be 18 or older.`,
),
diff --git a/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts b/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts
index 5c1a7b1c48..1a9c252dde 100644
--- a/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts
+++ b/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts
@@ -8,6 +8,7 @@ export function useIsAgeAssuranceEnabled() {
const {geolocation} = useGeolocation()
return useMemo(() => {
- return gate('age_assurance') && !!geolocation?.isAgeRestrictedGeo
+ const enabled = __DEV__ || gate('age_assurance')
+ return enabled && !!geolocation?.isAgeRestrictedGeo
}, [geolocation, gate])
}
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
index 1d0649b2ee..a0128e139d 100644
--- a/src/view/com/posts/PostFeed.tsx
+++ b/src/view/com/posts/PostFeed.tsx
@@ -43,11 +43,16 @@ import {
import {useLiveNowConfig} from '#/state/service-config'
import {useSession} from '#/state/session'
import {useProgressGuide} from '#/state/shell/progress-guide'
+import {useSelectedFeed} from '#/state/shell/selected-feed'
import {List, type ListRef} from '#/view/com/util/List'
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
import {useBreakpoints, useLayoutBreakpoints} from '#/alf'
+import {
+ AgeAssuranceDismissibleFeedBanner,
+ useInternalState as useAgeAssuranceBannerState,
+} from '#/components/ageAssurance/AgeAssuranceDismissibleFeedBanner'
import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
import {
PostFeedVideoGridRow,
@@ -131,6 +136,10 @@ type FeedRow =
type: 'showLessFollowup'
key: string
}
+ | {
+ type: 'ageAssuranceBanner'
+ key: string
+ }
export function getItemsForFeedback(feedRow: FeedRow): {
item: FeedPostSliceItem
@@ -335,6 +344,14 @@ let PostFeed = ({
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
+ const ageAssuranceBannerState = useAgeAssuranceBannerState()
+ const selectedFeed = useSelectedFeed()
+ /**
+ * Cached value of whether the current feed was selected at startup. We don't
+ * want this to update when user swipes.
+ */
+ const isCurrentFeedAtStartupSelected = useRef(selectedFeed === feed)
+
const feedItems: FeedRow[] = useMemo(() => {
// wraps a slice item, and replaces it with a showLessFollowup item
// if the user has pressed show less on it
@@ -450,6 +467,21 @@ let PostFeed = ({
type: 'interstitialProgressGuide',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
+ } else {
+ /*
+ * Only insert if Discover was the last selected feed at
+ * startup, the progress guide isn't shown, and the
+ * banner is eligible to be shown.
+ */
+ if (
+ isCurrentFeedAtStartupSelected.current &&
+ ageAssuranceBannerState.visible
+ ) {
+ arr.push({
+ type: 'ageAssuranceBanner',
+ key: 'ageAssuranceBanner-' + sliceIndex,
+ })
+ }
}
if (!rightNavVisible && !trendingDisabled) {
arr.push({
@@ -478,6 +510,17 @@ let PostFeed = ({
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
}
+ } else {
+ /*
+ * Only insert if this feed was the last selected feed at
+ * startup and the banner is eligible to be shown.
+ */
+ if (isCurrentFeedAtStartupSelected.current) {
+ arr.push({
+ type: 'ageAssuranceBanner',
+ key: 'ageAssuranceBanner-' + sliceIndex,
+ })
+ }
}
}
@@ -580,6 +623,7 @@ let PostFeed = ({
isVideoFeed,
areVideoFeedsEnabled,
hasPressedShowLessUris,
+ ageAssuranceBannerState,
])
// events
@@ -666,6 +710,8 @@ let PostFeed = ({
return
} else if (row.type === 'interstitialProgressGuide') {
return
+ } else if (row.type === 'ageAssuranceBanner') {
+ return
} else if (row.type === 'interstitialTrending') {
return
} else if (row.type === 'interstitialTrendingVideos') {