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:
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,33 +27,37 @@ export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={style}>
|
<View style={style}>
|
||||||
<View>
|
{aa.state.error === 'config' ? (
|
||||||
<AgeAssuranceAdmonition>{copy.notice}</AgeAssuranceAdmonition>
|
<AgeAssuranceConfigUnavailableError />
|
||||||
|
) : (
|
||||||
|
<View>
|
||||||
|
<AgeAssuranceAdmonition>{copy.notice}</AgeAssuranceAdmonition>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Don't show again`)}
|
label={_(msg`Don't show again`)}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
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', {})
|
logger.metric('ageAssurance:dismissSettingsNotice', {})
|
||||||
}}
|
}}
|
||||||
style={[
|
style={[
|
||||||
a.absolute,
|
a.absolute,
|
||||||
{
|
{
|
||||||
top: 12,
|
top: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user