Age assurance fast-follows (#8656)
* Add feed banner * Comment * Update nux name * Handle did error * Hide mod settings if underage or age restricted * Add metrics * Remove DEV override * Copy suggestion * Small copy edits * useState * Fix bug * Update src/components/ageAssurance/useAgeAssuranceCopy.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Get rid of debug button --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||||
|
import {logger} from '#/state/ageAssurance/util'
|
||||||
import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
|
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
|
||||||
@@ -83,6 +84,7 @@ function Inner({style}: ViewStyleProp & {}) {
|
|||||||
label={_(msg`Contact our moderation team`)}
|
label={_(msg`Contact our moderation team`)}
|
||||||
{...createStaticClick(() => {
|
{...createStaticClick(() => {
|
||||||
appealControl.open()
|
appealControl.open()
|
||||||
|
logger.metric('ageAssurance:appealDialogOpen', {})
|
||||||
})}>
|
})}>
|
||||||
contact our moderation team
|
contact our moderation team
|
||||||
</InlineLinkText>{' '}
|
</InlineLinkText>{' '}
|
||||||
@@ -109,7 +111,12 @@ function Inner({style}: ViewStyleProp & {}) {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color={hasInitiated ? 'secondary' : 'primary'}
|
color={hasInitiated ? 'secondary' : 'primary'}
|
||||||
onPress={() => control.open()}>
|
onPress={() => {
|
||||||
|
control.open()
|
||||||
|
logger.metric('ageAssurance:initDialogOpen', {
|
||||||
|
hasInitiatedPreviously: hasInitiated,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
{hasInitiated ? (
|
{hasInitiated ? (
|
||||||
<Trans>Verify again</Trans>
|
<Trans>Verify again</Trans>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||||
|
import {logger} from '#/state/ageAssurance/util'
|
||||||
import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf'
|
import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf'
|
||||||
import {useDialogControl} from '#/components/ageAssurance/AgeAssuranceInitDialog'
|
import {useDialogControl} from '#/components/ageAssurance/AgeAssuranceInitDialog'
|
||||||
import type * as Dialog from '#/components/Dialog'
|
import type * as Dialog from '#/components/Dialog'
|
||||||
@@ -87,7 +88,10 @@ function Inner({
|
|||||||
<InlineLinkText
|
<InlineLinkText
|
||||||
label={_(msg`Go to account settings`)}
|
label={_(msg`Go to account settings`)}
|
||||||
to={'/settings/account'}
|
to={'/settings/account'}
|
||||||
style={[a.text_sm, a.leading_snug, a.font_bold]}>
|
style={[a.text_sm, a.leading_snug, a.font_bold]}
|
||||||
|
onPress={() => {
|
||||||
|
logger.metric('ageAssurance:navigateToSettings', {})
|
||||||
|
}}>
|
||||||
account settings.
|
account settings.
|
||||||
</InlineLinkText>
|
</InlineLinkText>
|
||||||
</Trans>
|
</Trans>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/state/ageAssurance/util'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useBreakpoints, web} from '#/alf'
|
import {atoms as a, useBreakpoints, web} from '#/alf'
|
||||||
@@ -45,6 +45,8 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
|
|||||||
|
|
||||||
const {mutate, isPending} = useMutation({
|
const {mutate, isPending} = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
logger.metric('ageAssurance:appealDialogSubmit', {})
|
||||||
|
|
||||||
await agent.createModerationReport(
|
await agent.createModerationReport(
|
||||||
{
|
{
|
||||||
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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 {logger} from '#/state/ageAssurance/util'
|
||||||
|
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.AgeAssuranceDismissibleFeedBanner)
|
||||||
|
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.AgeAssuranceDismissibleFeedBanner,
|
||||||
|
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 (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.px_lg,
|
||||||
|
{
|
||||||
|
paddingVertical: 10,
|
||||||
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.primary_25,
|
||||||
|
dark: t.palette.primary_25,
|
||||||
|
dim: t.palette.primary_25,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Link
|
||||||
|
label={_(msg`Learn more about age assurance`)}
|
||||||
|
to="/settings/account"
|
||||||
|
onPress={() => {
|
||||||
|
close()
|
||||||
|
logger.metric('ageAssurance:navigateToSettings', {})
|
||||||
|
}}
|
||||||
|
style={[a.w_full, a.justify_between, a.align_center, a.gap_md]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.primary_100,
|
||||||
|
dark: t.palette.primary_100,
|
||||||
|
dim: t.palette.primary_100,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Shield size="lg" />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
{
|
||||||
|
paddingRight: 40,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={{maxWidth: 400}}>
|
||||||
|
<Text style={[a.leading_snug]}>{copy.banner}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={_(msg`Don't show again`)}
|
||||||
|
size="small"
|
||||||
|
onPress={() => {
|
||||||
|
close()
|
||||||
|
logger.metric('ageAssurance:dismissFeedBanner', {})
|
||||||
|
}}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
{
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
paddingRight: a.px_md.paddingLeft,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<X
|
||||||
|
width={20}
|
||||||
|
fill={select(t.name, {
|
||||||
|
light: t.palette.primary_600,
|
||||||
|
dark: t.palette.primary_600,
|
||||||
|
dim: t.palette.primary_600,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
|
||||||
<Link
|
|
||||||
label={_(msg`Learn more about age assurance`)}
|
|
||||||
to="/settings/account"
|
|
||||||
onPress={close}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_xs,
|
|
||||||
a.px_sm,
|
|
||||||
a.pr_sm,
|
|
||||||
a.rounded_full,
|
|
||||||
{
|
|
||||||
paddingVertical: 6,
|
|
||||||
backgroundColor: select(t.name, {
|
|
||||||
light: t.palette.primary_100,
|
|
||||||
dark: t.palette.primary_100,
|
|
||||||
dim: t.palette.primary_100,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Shield size="sm" />
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.font_bold,
|
|
||||||
a.leading_snug,
|
|
||||||
{
|
|
||||||
color: select(t.name, {
|
|
||||||
light: t.palette.primary_800,
|
|
||||||
dark: t.palette.primary_800,
|
|
||||||
dim: t.palette.primary_800,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Trans>Age Assurance</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||||
|
import {logger} from '#/state/ageAssurance/util'
|
||||||
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||||
import {atoms as a, type ViewStyleProp} from '#/alf'
|
import {atoms as a, type ViewStyleProp} from '#/alf'
|
||||||
import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition'
|
import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition'
|
||||||
@@ -37,13 +38,14 @@ export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) {
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary_inverted"
|
color="secondary_inverted"
|
||||||
shape="round"
|
shape="round"
|
||||||
onPress={() =>
|
onPress={() => {
|
||||||
save({
|
save({
|
||||||
id: Nux.AgeAssuranceDismissibleNotice,
|
id: Nux.AgeAssuranceDismissibleNotice,
|
||||||
completed: true,
|
completed: true,
|
||||||
data: undefined,
|
data: undefined,
|
||||||
})
|
})
|
||||||
}
|
logger.metric('ageAssurance:dismissSettingsNotice', {})
|
||||||
|
}}
|
||||||
style={[
|
style={[
|
||||||
a.absolute,
|
a.absolute,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {XRPCError} from '@atproto/xrpc'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {validate as validateEmail} from 'email-validator'
|
import {validate as validateEmail} from 'email-validator'
|
||||||
|
|
||||||
import {useCleanError} from '#/lib/hooks/useCleanError'
|
import {useCleanError} from '#/lib/hooks/useCleanError'
|
||||||
|
import {
|
||||||
|
SupportCode,
|
||||||
|
useCreateSupportLink,
|
||||||
|
} from '#/lib/hooks/useCreateSupportLink'
|
||||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||||
import {useTLDs} from '#/lib/hooks/useTLDs'
|
import {useTLDs} from '#/lib/hooks/useTLDs'
|
||||||
import {isEmailMaybeInvalid} from '#/lib/strings/email'
|
import {isEmailMaybeInvalid} from '#/lib/strings/email'
|
||||||
import {type AppLanguage} from '#/locale/languages'
|
import {type AppLanguage} from '#/locale/languages'
|
||||||
import {useAgeAssuranceContext} from '#/state/ageAssurance'
|
import {useAgeAssuranceContext} from '#/state/ageAssurance'
|
||||||
import {useInitAgeAssurance} from '#/state/ageAssurance/useInitAgeAssurance'
|
import {useInitAgeAssurance} from '#/state/ageAssurance/useInitAgeAssurance'
|
||||||
|
import {logger} from '#/state/ageAssurance/util'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
@@ -66,6 +72,7 @@ function Inner() {
|
|||||||
const {lastInitiatedAt} = useAgeAssuranceContext()
|
const {lastInitiatedAt} = useAgeAssuranceContext()
|
||||||
const getTimeAgo = useGetTimeAgo()
|
const getTimeAgo = useGetTimeAgo()
|
||||||
const tlds = useTLDs()
|
const tlds = useTLDs()
|
||||||
|
const createSupportLink = useCreateSupportLink()
|
||||||
|
|
||||||
const wasRecentlyInitiated =
|
const wasRecentlyInitiated =
|
||||||
lastInitiatedAt &&
|
lastInitiatedAt &&
|
||||||
@@ -79,7 +86,7 @@ function Inner() {
|
|||||||
const [language, setLanguage] = useState<string | undefined>(
|
const [language, setLanguage] = useState<string | undefined>(
|
||||||
convertToKWSSupportedLanguage(langPrefs.appLanguage),
|
convertToKWSSupportedLanguage(langPrefs.appLanguage),
|
||||||
)
|
)
|
||||||
const [error, setError] = useState<string>('')
|
const [error, setError] = useState<React.ReactNode>(null)
|
||||||
|
|
||||||
const {mutateAsync: init, isPending} = useInitAgeAssurance()
|
const {mutateAsync: init, isPending} = useInitAgeAssurance()
|
||||||
|
|
||||||
@@ -109,6 +116,8 @@ function Inner() {
|
|||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
setLanguageError(false)
|
setLanguageError(false)
|
||||||
|
|
||||||
|
logger.metric('ageAssurance:initDialogSubmit', {})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {status} = runEmailValidation()
|
const {status} = runEmailValidation()
|
||||||
|
|
||||||
@@ -125,22 +134,35 @@ function Inner() {
|
|||||||
|
|
||||||
setSuccess(true)
|
setSuccess(true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const {clean, raw} = cleanError(e)
|
if (e instanceof XRPCError) {
|
||||||
|
if (e.error === 'InvalidEmail') {
|
||||||
if (clean) {
|
setError(
|
||||||
setError(clean || _(msg`Something went wrong, please try again`))
|
_(
|
||||||
} else {
|
|
||||||
let message = _(msg`Something went wrong, please try again`)
|
|
||||||
|
|
||||||
if (raw) {
|
|
||||||
if (raw.startsWith('This email address is not supported')) {
|
|
||||||
message = _(
|
|
||||||
msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`,
|
msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
logger.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'})
|
||||||
|
} else if (e.error === 'DidTooLong') {
|
||||||
|
setError(
|
||||||
|
<>
|
||||||
|
<Trans>
|
||||||
|
We're having issues initializing the age assurance process for
|
||||||
|
your account. Please{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
to={createSupportLink({code: SupportCode.AA_DID, email})}
|
||||||
|
label={_(msg`Contact support`)}>
|
||||||
|
contact support
|
||||||
|
</InlineLinkText>{' '}
|
||||||
|
for assistance.
|
||||||
|
</Trans>
|
||||||
|
</>,
|
||||||
|
)
|
||||||
|
logger.metric('ageAssurance:initDialogError', {code: 'DidTooLong'})
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
const {clean, raw} = cleanError(e)
|
||||||
setError(message)
|
setError(clean || raw || _(msg`Something went wrong, please try again`))
|
||||||
|
logger.metric('ageAssurance:initDialogError', {code: 'other'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {retry} from '#/lib/async/retry'
|
|||||||
import {wait} from '#/lib/async/wait'
|
import {wait} from '#/lib/async/wait'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useAgeAssuranceAPIContext} from '#/state/ageAssurance'
|
import {useAgeAssuranceAPIContext} from '#/state/ageAssurance'
|
||||||
|
import {logger} from '#/state/ageAssurance/util'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
||||||
@@ -92,6 +93,8 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
|||||||
|
|
||||||
polling.current = true
|
polling.current = true
|
||||||
|
|
||||||
|
logger.metric('ageAssurance:redirectDialogOpen', {})
|
||||||
|
|
||||||
wait(
|
wait(
|
||||||
3e3,
|
3e3,
|
||||||
retry(
|
retry(
|
||||||
@@ -124,12 +127,15 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
|||||||
|
|
||||||
control.clear()
|
control.clear()
|
||||||
control.control.close()
|
control.control.close()
|
||||||
|
|
||||||
|
logger.metric('ageAssurance:redirectDialogSuccess', {})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (unmounted.current) return
|
if (unmounted.current) return
|
||||||
setError(true)
|
setError(true)
|
||||||
// try a refetch anyway
|
// try a refetch anyway
|
||||||
refreshAgeAssuranceState()
|
refreshAgeAssuranceState()
|
||||||
|
logger.metric('ageAssurance:redirectDialogFail', {})
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||||
|
import {logger} from '#/state/ageAssurance/util'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
||||||
@@ -61,13 +62,11 @@ export function AgeRestrictedScreen({
|
|||||||
<View style={[a.gap_sm, a.pb_lg]}>
|
<View style={[a.gap_sm, a.pb_lg]}>
|
||||||
<Text style={[a.text_xl, a.leading_snug, a.font_heavy]}>
|
<Text style={[a.text_xl, a.leading_snug, a.font_heavy]}>
|
||||||
<Trans>
|
<Trans>
|
||||||
You must verify your age in order to access this screen.
|
You must complete age assurance in order to access this screen.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={[a.text_md, a.leading_snug]}>
|
<Text style={[a.text_md, a.leading_snug]}>{copy.notice}</Text>
|
||||||
<Trans>{copy.notice}</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -77,7 +76,10 @@ export function AgeRestrictedScreen({
|
|||||||
to="/settings/account"
|
to="/settings/account"
|
||||||
size="small"
|
size="small"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary">
|
color="primary"
|
||||||
|
onPress={() => {
|
||||||
|
logger.metric('ageAssurance:navigateToSettings', {})
|
||||||
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Go to account settings</Trans>
|
<Trans>Go to account settings</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ export function useAgeAssuranceCopy() {
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
return {
|
||||||
notice: _(
|
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.`,
|
msg`The laws in your location require you to verify you're an adult before accessing certain features on Bluesky, like adult content and direct messaging.`,
|
||||||
|
),
|
||||||
|
banner: _(
|
||||||
|
msg`The laws in your location require you to verify you're an adult. Tap to learn more.`,
|
||||||
),
|
),
|
||||||
chatsInfoText: _(
|
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.`,
|
msg`Don't worry! All existing messages and settings are saved and will be available after you verify you're an adult.`,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}, [_])
|
}, [_])
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import {useCallback} from 'react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
|
||||||
|
export const ZENDESK_SUPPORT_URL =
|
||||||
|
'https://blueskyweb.zendesk.com/hc/requests/new'
|
||||||
|
|
||||||
|
export enum SupportCode {
|
||||||
|
AA_DID = 'AA_DID',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link https://support.zendesk.com/hc/en-us/articles/4408839114522-Creating-pre-filled-ticket-forms}
|
||||||
|
*/
|
||||||
|
export function useCreateSupportLink() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
({code, email}: {code: SupportCode; email?: string}) => {
|
||||||
|
const url = new URL(ZENDESK_SUPPORT_URL)
|
||||||
|
if (currentAccount) {
|
||||||
|
url.search = new URLSearchParams({
|
||||||
|
tf_anonymous_requester_email: email || currentAccount.email || '', // email will be defined
|
||||||
|
tf_description:
|
||||||
|
`[Code: ${code}] — ` + _(msg`Please write your message below:`),
|
||||||
|
/**
|
||||||
|
* Custom field specific to {@link ZENDESK_SUPPORT_URL} form
|
||||||
|
*/
|
||||||
|
tf_17205412673421: currentAccount.handle + ` (${currentAccount.did})`,
|
||||||
|
}).toString()
|
||||||
|
}
|
||||||
|
return url.toString()
|
||||||
|
},
|
||||||
|
[_, currentAccount],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -192,6 +192,9 @@ export function useNotificationsRegistration() {
|
|||||||
* Register the push token with the Bluesky server, whenever it changes.
|
* Register the push token with the Bluesky server, whenever it changes.
|
||||||
* This is also fired any time `getDevicePushTokenAsync` is called.
|
* This is also fired any time `getDevicePushTokenAsync` is called.
|
||||||
*
|
*
|
||||||
|
* Since this is registered immediately after `getAndRegisterPushToken`, it
|
||||||
|
* should also detect that getter and be fired almost immediately after this.
|
||||||
|
*
|
||||||
* According to the Expo docs, there is a chance that the token will change
|
* According to the Expo docs, there is a chance that the token will change
|
||||||
* while the app is open in some rare cases. This will fire
|
* while the app is open in some rare cases. This will fire
|
||||||
* `registerPushToken` whenever that happens.
|
* `registerPushToken` whenever that happens.
|
||||||
|
|||||||
@@ -457,4 +457,20 @@ export type MetricEvents = {
|
|||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
'ageAssurance:navigateToSettings': {}
|
||||||
|
'ageAssurance:dismissFeedBanner': {}
|
||||||
|
'ageAssurance:dismissSettingsNotice': {}
|
||||||
|
'ageAssurance:initDialogOpen': {
|
||||||
|
hasInitiatedPreviously: boolean
|
||||||
|
}
|
||||||
|
'ageAssurance:initDialogSubmit': {}
|
||||||
|
'ageAssurance:initDialogError': {
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
'ageAssurance:redirectDialogOpen': {}
|
||||||
|
'ageAssurance:redirectDialogSuccess': {}
|
||||||
|
'ageAssurance:redirectDialogFail': {}
|
||||||
|
'ageAssurance:appealDialogOpen': {}
|
||||||
|
'ageAssurance:appealDialogSubmit': {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,6 +304,8 @@ export function ModerationScreenInner({
|
|||||||
</Link>
|
</Link>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{declaredAge === undefined && (
|
||||||
|
<>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.pt_2xl,
|
a.pt_2xl,
|
||||||
@@ -315,15 +317,6 @@ export function ModerationScreenInner({
|
|||||||
<Trans>Content filters</Trans>
|
<Trans>Content filters</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<AgeAssuranceAdmonition style={[a.pb_md]}>
|
|
||||||
<Trans>
|
|
||||||
You must complete age assurance in order to access the settings below.
|
|
||||||
</Trans>
|
|
||||||
</AgeAssuranceAdmonition>
|
|
||||||
|
|
||||||
<View style={[a.gap_md]}>
|
|
||||||
{declaredAge === undefined && (
|
|
||||||
<>
|
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Confirm your birthdate`)}
|
label={_(msg`Confirm your birthdate`)}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -344,6 +337,28 @@ export function ModerationScreenInner({
|
|||||||
<BirthDateSettingsDialog control={birthdateDialogControl} />
|
<BirthDateSettingsDialog control={birthdateDialogControl} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isDeclaredUnderage && (
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.pt_2xl,
|
||||||
|
a.pb_md,
|
||||||
|
a.text_md,
|
||||||
|
a.font_bold,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
]}>
|
||||||
|
<Trans>Content filters</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<AgeAssuranceAdmonition style={[a.pb_md]}>
|
||||||
|
<Trans>
|
||||||
|
You must complete age assurance in order to access the settings
|
||||||
|
below.
|
||||||
|
</Trans>
|
||||||
|
</AgeAssuranceAdmonition>
|
||||||
|
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
@@ -351,7 +366,7 @@ export function ModerationScreenInner({
|
|||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
t.atoms.bg_contrast_25,
|
t.atoms.bg_contrast_25,
|
||||||
]}>
|
]}>
|
||||||
{!isDeclaredUnderage && !isAgeRestricted && (
|
{!isDeclaredUnderage && (
|
||||||
<>
|
<>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -367,7 +382,7 @@ export function ModerationScreenInner({
|
|||||||
</Text>
|
</Text>
|
||||||
<Toggle.Item
|
<Toggle.Item
|
||||||
label={_(msg`Toggle to enable or disable adult content`)}
|
label={_(msg`Toggle to enable or disable adult content`)}
|
||||||
disabled={disabledOnIOS}
|
disabled={disabledOnIOS || isAgeRestricted}
|
||||||
name="adultContent"
|
name="adultContent"
|
||||||
value={adultContentEnabled}
|
value={adultContentEnabled}
|
||||||
onChange={onToggleAdultContentEnabled}>
|
onChange={onToggleAdultContentEnabled}>
|
||||||
@@ -403,10 +418,10 @@ export function ModerationScreenInner({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{adultContentEnabled && (
|
{adultContentEnabled && (
|
||||||
<>
|
<>
|
||||||
|
<Divider />
|
||||||
<GlobalLabelPreference labelDefinition={LABELS.porn} />
|
<GlobalLabelPreference labelDefinition={LABELS.porn} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<GlobalLabelPreference labelDefinition={LABELS.sexual} />
|
<GlobalLabelPreference labelDefinition={LABELS.sexual} />
|
||||||
@@ -415,16 +430,18 @@ export function ModerationScreenInner({
|
|||||||
labelDefinition={LABELS['graphic-media']}
|
labelDefinition={LABELS['graphic-media']}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<GlobalLabelPreference
|
<GlobalLabelPreference
|
||||||
disabled={isDeclaredUnderage || isAgeRestricted}
|
disabled={isDeclaredUnderage || isAgeRestricted}
|
||||||
labelDefinition={LABELS.nudity}
|
labelDefinition={LABELS.nudity}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ import {BroomSparkle_Stroke2_Corner2_Rounded as BroomSparkleIcon} from '#/compon
|
|||||||
import {CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon} from '#/components/icons/CodeLines'
|
import {CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon} from '#/components/icons/CodeLines'
|
||||||
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
||||||
import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/icons/Newspaper'
|
import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/icons/Newspaper'
|
||||||
import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
|
|
||||||
import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
|
import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {device} from '#/storage'
|
|
||||||
import {useDemoMode} from '#/storage/hooks/demo-mode'
|
import {useDemoMode} from '#/storage/hooks/demo-mode'
|
||||||
import {useDevMode} from '#/storage/hooks/dev-mode'
|
import {useDevMode} from '#/storage/hooks/dev-mode'
|
||||||
import {OTAInfo} from './components/OTAInfo'
|
import {OTAInfo} from './components/OTAInfo'
|
||||||
@@ -181,20 +179,6 @@ export function AboutSettingsScreen({}: Props) {
|
|||||||
</SettingsList.ItemText>
|
</SettingsList.ItemText>
|
||||||
</SettingsList.PressableItem>
|
</SettingsList.PressableItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SettingsList.PressableItem
|
|
||||||
onPress={() => {
|
|
||||||
device.set(['geolocation'], {
|
|
||||||
countryCode: 'GB',
|
|
||||||
isAgeRestrictedGeo: true,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
label="Simulate age restriction">
|
|
||||||
<SettingsList.ItemIcon icon={Shield} />
|
|
||||||
<SettingsList.ItemText>
|
|
||||||
Simulate age restriction
|
|
||||||
</SettingsList.ItemText>
|
|
||||||
</SettingsList.PressableItem>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SettingsList.Container>
|
</SettingsList.Container>
|
||||||
|
|||||||
@@ -6,17 +6,15 @@ import {networkRetry} from '#/lib/async/retry'
|
|||||||
import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications'
|
import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications'
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {isNetworkError} from '#/lib/strings/errors'
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
import {Logger} from '#/logger'
|
|
||||||
import {
|
import {
|
||||||
type AgeAssuranceAPIContextType,
|
type AgeAssuranceAPIContextType,
|
||||||
type AgeAssuranceContextType,
|
type AgeAssuranceContextType,
|
||||||
} from '#/state/ageAssurance/types'
|
} from '#/state/ageAssurance/types'
|
||||||
import {useIsAgeAssuranceEnabled} from '#/state/ageAssurance/useIsAgeAssuranceEnabled'
|
import {useIsAgeAssuranceEnabled} from '#/state/ageAssurance/useIsAgeAssuranceEnabled'
|
||||||
|
import {logger} from '#/state/ageAssurance/util'
|
||||||
import {useGeolocation} from '#/state/geolocation'
|
import {useGeolocation} from '#/state/geolocation'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
const logger = Logger.create(Logger.Context.AgeAssurance)
|
|
||||||
|
|
||||||
export const createAgeAssuranceQueryKey = (did: string) =>
|
export const createAgeAssuranceQueryKey = (did: string) =>
|
||||||
['ageAssurance', did] as const
|
['ageAssurance', did] as const
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import {useMemo} from 'react'
|
import {useMemo} from 'react'
|
||||||
|
|
||||||
import {Logger} from '#/logger'
|
|
||||||
import {useAgeAssuranceContext} from '#/state/ageAssurance'
|
import {useAgeAssuranceContext} from '#/state/ageAssurance'
|
||||||
|
import {logger} from '#/state/ageAssurance/util'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
|
|
||||||
const logger = Logger.create(Logger.Context.AgeAssurance)
|
|
||||||
|
|
||||||
type AgeAssurance = ReturnType<typeof useAgeAssuranceContext> & {
|
type AgeAssurance = ReturnType<typeof useAgeAssuranceContext> & {
|
||||||
/**
|
/**
|
||||||
* The age the user has declared in their preferences, if any.
|
* The age the user has declared in their preferences, if any.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export function useIsAgeAssuranceEnabled() {
|
|||||||
const {geolocation} = useGeolocation()
|
const {geolocation} = useGeolocation()
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return gate('age_assurance') && !!geolocation?.isAgeRestrictedGeo
|
const enabled = gate('age_assurance')
|
||||||
|
return enabled && !!geolocation?.isAgeRestrictedGeo
|
||||||
}, [geolocation, gate])
|
}, [geolocation, gate])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import {Logger} from '#/logger'
|
||||||
|
|
||||||
|
export const logger = Logger.create(Logger.Context.AgeAssurance)
|
||||||
@@ -8,7 +8,7 @@ export enum Nux {
|
|||||||
InitialVerificationAnnouncement = 'InitialVerificationAnnouncement',
|
InitialVerificationAnnouncement = 'InitialVerificationAnnouncement',
|
||||||
ActivitySubscriptions = 'ActivitySubscriptions',
|
ActivitySubscriptions = 'ActivitySubscriptions',
|
||||||
AgeAssuranceDismissibleNotice = 'AgeAssuranceDismissibleNotice',
|
AgeAssuranceDismissibleNotice = 'AgeAssuranceDismissibleNotice',
|
||||||
AgeAssuranceDismissibleHeaderButton = 'AgeAssuranceDismissibleHeaderButton',
|
AgeAssuranceDismissibleFeedBanner = 'AgeAssuranceDismissibleFeedBanner',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nuxNames = new Set(Object.values(Nux))
|
export const nuxNames = new Set(Object.values(Nux))
|
||||||
@@ -35,7 +35,7 @@ export type AppNux = BaseNux<
|
|||||||
data: undefined
|
data: undefined
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
id: Nux.AgeAssuranceDismissibleHeaderButton
|
id: Nux.AgeAssuranceDismissibleFeedBanner
|
||||||
data: undefined
|
data: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -46,5 +46,5 @@ export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
|||||||
[Nux.InitialVerificationAnnouncement]: undefined,
|
[Nux.InitialVerificationAnnouncement]: undefined,
|
||||||
[Nux.ActivitySubscriptions]: undefined,
|
[Nux.ActivitySubscriptions]: undefined,
|
||||||
[Nux.AgeAssuranceDismissibleNotice]: undefined,
|
[Nux.AgeAssuranceDismissibleNotice]: undefined,
|
||||||
[Nux.AgeAssuranceDismissibleHeaderButton]: undefined,
|
[Nux.AgeAssuranceDismissibleFeedBanner]: undefined,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,11 +43,16 @@ import {
|
|||||||
import {useLiveNowConfig} from '#/state/service-config'
|
import {useLiveNowConfig} from '#/state/service-config'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||||
|
import {useSelectedFeed} from '#/state/shell/selected-feed'
|
||||||
import {List, type ListRef} from '#/view/com/util/List'
|
import {List, type ListRef} from '#/view/com/util/List'
|
||||||
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||||
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||||
import {useBreakpoints, useLayoutBreakpoints} from '#/alf'
|
import {useBreakpoints, useLayoutBreakpoints} from '#/alf'
|
||||||
|
import {
|
||||||
|
AgeAssuranceDismissibleFeedBanner,
|
||||||
|
useInternalState as useAgeAssuranceBannerState,
|
||||||
|
} from '#/components/ageAssurance/AgeAssuranceDismissibleFeedBanner'
|
||||||
import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
|
import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
|
||||||
import {
|
import {
|
||||||
PostFeedVideoGridRow,
|
PostFeedVideoGridRow,
|
||||||
@@ -131,6 +136,10 @@ type FeedRow =
|
|||||||
type: 'showLessFollowup'
|
type: 'showLessFollowup'
|
||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'ageAssuranceBanner'
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
export function getItemsForFeedback(feedRow: FeedRow): {
|
export function getItemsForFeedback(feedRow: FeedRow): {
|
||||||
item: FeedPostSliceItem
|
item: FeedPostSliceItem
|
||||||
@@ -335,6 +344,14 @@ let PostFeed = ({
|
|||||||
|
|
||||||
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
|
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] = useState(selectedFeed === feed)
|
||||||
|
|
||||||
const feedItems: FeedRow[] = useMemo(() => {
|
const feedItems: FeedRow[] = useMemo(() => {
|
||||||
// wraps a slice item, and replaces it with a showLessFollowup item
|
// wraps a slice item, and replaces it with a showLessFollowup item
|
||||||
// if the user has pressed show less on it
|
// if the user has pressed show less on it
|
||||||
@@ -450,6 +467,21 @@ let PostFeed = ({
|
|||||||
type: 'interstitialProgressGuide',
|
type: 'interstitialProgressGuide',
|
||||||
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
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 &&
|
||||||
|
ageAssuranceBannerState.visible
|
||||||
|
) {
|
||||||
|
arr.push({
|
||||||
|
type: 'ageAssuranceBanner',
|
||||||
|
key: 'ageAssuranceBanner-' + sliceIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!rightNavVisible && !trendingDisabled) {
|
if (!rightNavVisible && !trendingDisabled) {
|
||||||
arr.push({
|
arr.push({
|
||||||
@@ -478,6 +510,17 @@ let PostFeed = ({
|
|||||||
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
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 (sliceIndex === 0 && isCurrentFeedAtStartupSelected) {
|
||||||
|
arr.push({
|
||||||
|
type: 'ageAssuranceBanner',
|
||||||
|
key: 'ageAssuranceBanner-' + sliceIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,6 +623,8 @@ let PostFeed = ({
|
|||||||
isVideoFeed,
|
isVideoFeed,
|
||||||
areVideoFeedsEnabled,
|
areVideoFeedsEnabled,
|
||||||
hasPressedShowLessUris,
|
hasPressedShowLessUris,
|
||||||
|
ageAssuranceBannerState,
|
||||||
|
isCurrentFeedAtStartupSelected,
|
||||||
])
|
])
|
||||||
|
|
||||||
// events
|
// events
|
||||||
@@ -666,6 +711,8 @@ let PostFeed = ({
|
|||||||
return <SuggestedFollows feed={feed} />
|
return <SuggestedFollows feed={feed} />
|
||||||
} else if (row.type === 'interstitialProgressGuide') {
|
} else if (row.type === 'interstitialProgressGuide') {
|
||||||
return <ProgressGuide />
|
return <ProgressGuide />
|
||||||
|
} else if (row.type === 'ageAssuranceBanner') {
|
||||||
|
return <AgeAssuranceDismissibleFeedBanner />
|
||||||
} else if (row.type === 'interstitialTrending') {
|
} else if (row.type === 'interstitialTrending') {
|
||||||
return <TrendingInterstitial />
|
return <TrendingInterstitial />
|
||||||
} else if (row.type === 'interstitialTrendingVideos') {
|
} else if (row.type === 'interstitialTrendingVideos') {
|
||||||
|
|||||||
Reference in New Issue
Block a user