Handle AA config failure (#9660)

* Clarify some comments

* Add error state in case of config failure

* Add retry button and relayout to accommodate
This commit is contained in:
Eric Bailey
2026-01-08 14:35:43 -06:00
committed by GitHub
parent d82592f072
commit a1857d62bd
9 changed files with 111 additions and 28 deletions
+13
View File
@@ -136,6 +136,15 @@ export async function prefetchConfig() {
} }
}) })
} }
export async function refetchConfig() {
logger.debug(`refetchConfig: fetching...`)
const res = await getConfig()
qc.setQueryData<AppBskyAgeassuranceGetConfig.OutputSchema>(
configQueryKey,
res,
)
return res
}
export function useConfigQuery() { export function useConfigQuery() {
return useQuery( return useQuery(
{ {
@@ -146,6 +155,10 @@ export function useConfigQuery() {
* @see https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#initial-data-from-the-cache-with-initialdataupdatedat * @see https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#initial-data-from-the-cache-with-initialdataupdatedat
*/ */
staleTime: IS_DEV ? 5e3 : 1000 * 60 * 60, staleTime: IS_DEV ? 5e3 : 1000 * 60 * 60,
/**
* N.B. if prefetch failed above, we'll have no `initialData`, and this
* query will run on startup.
*/
initialData: getConfigFromCache(), initialData: getConfigFromCache(),
initialDataUpdatedAt: () => initialDataUpdatedAt: () =>
qc.getQueryState(configQueryKey)?.dataUpdatedAt, qc.getQueryState(configQueryKey)?.dataUpdatedAt,
+9 -2
View File
@@ -30,12 +30,19 @@ export function useAgeAssuranceState(): AgeAssuranceState {
access: AgeAssuranceAccess.Safe, access: AgeAssuranceAccess.Safe,
} }
// should never happen, but need to guard /**
* This can happen if the prefetch fails (such as due to network issues).
* The query handler will try it again, but if it continues to fail, of
* course we won't have config.
*
* In this case, fail open to avoid blocking users.
*/
if (!config) { if (!config) {
logger.warn('useAgeAssuranceState: missing config') logger.warn('useAgeAssuranceState: missing config')
return { return {
status: AgeAssuranceStatus.Unknown, status: AgeAssuranceStatus.Unknown,
access: AgeAssuranceAccess.Unknown, access: AgeAssuranceAccess.Safe,
error: 'config',
} }
} }
+1
View File
@@ -18,6 +18,7 @@ export type AgeAssuranceState = {
lastInitiatedAt?: string lastInitiatedAt?: string
status: AgeAssuranceStatus status: AgeAssuranceStatus
access: AgeAssuranceAccess access: AgeAssuranceAccess
error?: 'config' // maybe other specific cases in the future
} }
export function parseStatusFromString(raw: string) { export function parseStatusFromString(raw: string) {
@@ -8,6 +8,7 @@ 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'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors'
import { import {
AgeAssuranceInitDialog, AgeAssuranceInitDialog,
useDialogControl, useDialogControl,
@@ -27,6 +28,13 @@ import {useDeviceGeolocationApi} from '#/geolocation'
export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) { export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) {
const aa = useAgeAssurance() const aa = useAgeAssurance()
if (aa.state.access === aa.Access.Full) return null if (aa.state.access === aa.Access.Full) return null
if (aa.state.error === 'config') {
return (
<View style={style}>
<AgeAssuranceConfigUnavailableError />
</View>
)
}
return <Inner style={style} /> return <Inner style={style} />
} }
@@ -3,6 +3,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf' import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf'
import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors'
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'
import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
@@ -19,6 +20,9 @@ export function AgeAssuranceAdmonition({
const aa = useAgeAssurance() const aa = useAgeAssurance()
if (aa.state.access === aa.Access.Full) return null if (aa.state.access === aa.Access.Full) return null
if (aa.state.error === 'config') {
return <AgeAssuranceConfigUnavailableError style={style} />
}
return ( return (
<Inner style={style} control={control}> <Inner style={style} control={control}>
@@ -23,6 +23,7 @@ export function useInternalState() {
const visible = useMemo(() => { const visible = useMemo(() => {
if (aa.state.access === aa.Access.Full) return false if (aa.state.access === aa.Access.Full) return false
if (aa.state.lastInitiatedAt) return false if (aa.state.lastInitiatedAt) return false
if (aa.state.error === 'config') return false
if (hidden) return false if (hidden) return false
if (nux && nux.completed) return false if (nux && nux.completed) return false
return true return true
@@ -5,6 +5,7 @@ import {useLingui} from '@lingui/react'
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'
import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors'
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
@@ -26,6 +27,9 @@ export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) {
return ( return (
<View style={style}> <View style={style}>
{aa.state.error === 'config' ? (
<AgeAssuranceConfigUnavailableError />
) : (
<View> <View>
<AgeAssuranceAdmonition>{copy.notice}</AgeAssuranceAdmonition> <AgeAssuranceAdmonition>{copy.notice}</AgeAssuranceAdmonition>
@@ -53,6 +57,7 @@ export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) {
<ButtonIcon icon={X} /> <ButtonIcon icon={X} />
</Button> </Button>
</View> </View>
)}
</View> </View>
) )
} }
@@ -0,0 +1,37 @@
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {type ViewStyleProp} from '#/alf'
import * as Admonition from '#/components/Admonition'
import {ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
import {refetchConfig} from '#/ageAssurance/data'
export function AgeAssuranceConfigUnavailableError(props: ViewStyleProp) {
const {_} = useLingui()
return (
<Admonition.Outer type="error" style={props.style}>
<Admonition.Row>
<Admonition.Icon />
<Admonition.Content>
<Admonition.Text>
<Trans>
We were unable to load the age assurance configuration for your
region, probably due to a network error. Some content and features
may be unavailable temporarily. Please try again later.
</Trans>
</Admonition.Text>
</Admonition.Content>
<Admonition.Button
color="negative_subtle"
label={_(msg`Retry`)}
onPress={() => refetchConfig().catch(() => {})}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
<ButtonIcon icon={RetryIcon} />
</Admonition.Button>
</Admonition.Row>
</Admonition.Outer>
)
}
@@ -5,6 +5,7 @@ import {useLingui} from '@lingui/react'
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'
import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors'
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
import {ButtonIcon, ButtonText} from '#/components/Button' import {ButtonIcon, ButtonText} from '#/components/Button'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
@@ -44,6 +45,12 @@ export function AgeRestrictedScreen({
</Layout.Header.Outer> </Layout.Header.Outer>
<Layout.Content> <Layout.Content>
<View style={[a.p_lg]}> <View style={[a.p_lg]}>
{aa.state.error === 'config' && (
<View style={[a.pb_lg]}>
<AgeAssuranceConfigUnavailableError />
</View>
)}
<View style={[a.align_start, a.pb_lg]}> <View style={[a.align_start, a.pb_lg]}>
<AgeAssuranceBadge /> <AgeAssuranceBadge />
</View> </View>