Bluesky app fork with some witchin' additions 💫

Handle AA config failure (#9660)

* Clarify some comments

* Add error state in case of config failure

* Add retry button and relayout to accommodate

authored by

Eric Bailey and committed by
GitHub
a1857d62 d82592f0

+111 -28
+13
src/ageAssurance/data.tsx
··· 136 } 137 }) 138 } 139 export function useConfigQuery() { 140 return useQuery( 141 { ··· 146 * @see https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#initial-data-from-the-cache-with-initialdataupdatedat 147 */ 148 staleTime: IS_DEV ? 5e3 : 1000 * 60 * 60, 149 initialData: getConfigFromCache(), 150 initialDataUpdatedAt: () => 151 qc.getQueryState(configQueryKey)?.dataUpdatedAt,
··· 136 } 137 }) 138 } 139 + export async function refetchConfig() { 140 + logger.debug(`refetchConfig: fetching...`) 141 + const res = await getConfig() 142 + qc.setQueryData<AppBskyAgeassuranceGetConfig.OutputSchema>( 143 + configQueryKey, 144 + res, 145 + ) 146 + return res 147 + } 148 export function useConfigQuery() { 149 return useQuery( 150 { ··· 155 * @see https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#initial-data-from-the-cache-with-initialdataupdatedat 156 */ 157 staleTime: IS_DEV ? 5e3 : 1000 * 60 * 60, 158 + /** 159 + * N.B. if prefetch failed above, we'll have no `initialData`, and this 160 + * query will run on startup. 161 + */ 162 initialData: getConfigFromCache(), 163 initialDataUpdatedAt: () => 164 qc.getQueryState(configQueryKey)?.dataUpdatedAt,
+9 -2
src/ageAssurance/state.ts
··· 30 access: AgeAssuranceAccess.Safe, 31 } 32 33 - // should never happen, but need to guard 34 if (!config) { 35 logger.warn('useAgeAssuranceState: missing config') 36 return { 37 status: AgeAssuranceStatus.Unknown, 38 - access: AgeAssuranceAccess.Unknown, 39 } 40 } 41
··· 30 access: AgeAssuranceAccess.Safe, 31 } 32 33 + /** 34 + * This can happen if the prefetch fails (such as due to network issues). 35 + * The query handler will try it again, but if it continues to fail, of 36 + * course we won't have config. 37 + * 38 + * In this case, fail open to avoid blocking users. 39 + */ 40 if (!config) { 41 logger.warn('useAgeAssuranceState: missing config') 42 return { 43 status: AgeAssuranceStatus.Unknown, 44 + access: AgeAssuranceAccess.Safe, 45 + error: 'config', 46 } 47 } 48
+1
src/ageAssurance/types.ts
··· 18 lastInitiatedAt?: string 19 status: AgeAssuranceStatus 20 access: AgeAssuranceAccess 21 } 22 23 export function parseStatusFromString(raw: string) {
··· 18 lastInitiatedAt?: string 19 status: AgeAssuranceStatus 20 access: AgeAssuranceAccess 21 + error?: 'config' // maybe other specific cases in the future 22 } 23 24 export function parseStatusFromString(raw: string) {
+8
src/components/ageAssurance/AgeAssuranceAccountCard.tsx
··· 8 import {Admonition} from '#/components/Admonition' 9 import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' 10 import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 11 import { 12 AgeAssuranceInitDialog, 13 useDialogControl, ··· 27 export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) { 28 const aa = useAgeAssurance() 29 if (aa.state.access === aa.Access.Full) return null 30 return <Inner style={style} /> 31 } 32
··· 8 import {Admonition} from '#/components/Admonition' 9 import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' 10 import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 11 + import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors' 12 import { 13 AgeAssuranceInitDialog, 14 useDialogControl, ··· 28 export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) { 29 const aa = useAgeAssurance() 30 if (aa.state.access === aa.Access.Full) return null 31 + if (aa.state.error === 'config') { 32 + return ( 33 + <View style={style}> 34 + <AgeAssuranceConfigUnavailableError /> 35 + </View> 36 + ) 37 + } 38 return <Inner style={style} /> 39 } 40
+4
src/components/ageAssurance/AgeAssuranceAdmonition.tsx
··· 3 import {useLingui} from '@lingui/react' 4 5 import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf' 6 import {useDialogControl} from '#/components/ageAssurance/AgeAssuranceInitDialog' 7 import type * as Dialog from '#/components/Dialog' 8 import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' ··· 19 const aa = useAgeAssurance() 20 21 if (aa.state.access === aa.Access.Full) return null 22 23 return ( 24 <Inner style={style} control={control}>
··· 3 import {useLingui} from '@lingui/react' 4 5 import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf' 6 + import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors' 7 import {useDialogControl} from '#/components/ageAssurance/AgeAssuranceInitDialog' 8 import type * as Dialog from '#/components/Dialog' 9 import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' ··· 20 const aa = useAgeAssurance() 21 22 if (aa.state.access === aa.Access.Full) return null 23 + if (aa.state.error === 'config') { 24 + return <AgeAssuranceConfigUnavailableError style={style} /> 25 + } 26 27 return ( 28 <Inner style={style} control={control}>
+1
src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx
··· 23 const visible = useMemo(() => { 24 if (aa.state.access === aa.Access.Full) return false 25 if (aa.state.lastInitiatedAt) return false 26 if (hidden) return false 27 if (nux && nux.completed) return false 28 return true
··· 23 const visible = useMemo(() => { 24 if (aa.state.access === aa.Access.Full) return false 25 if (aa.state.lastInitiatedAt) return false 26 + if (aa.state.error === 'config') return false 27 if (hidden) return false 28 if (nux && nux.completed) return false 29 return true
+31 -26
src/components/ageAssurance/AgeAssuranceDismissibleNotice.tsx
··· 5 import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' 6 import {atoms as a, type ViewStyleProp} from '#/alf' 7 import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition' 8 import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' 9 import {Button, ButtonIcon} from '#/components/Button' 10 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' ··· 26 27 return ( 28 <View style={style}> 29 - <View> 30 - <AgeAssuranceAdmonition>{copy.notice}</AgeAssuranceAdmonition> 31 32 - <Button 33 - label={_(msg`Don't show again`)} 34 - size="tiny" 35 - variant="solid" 36 - color="secondary_inverted" 37 - shape="round" 38 - onPress={() => { 39 - save({ 40 - id: Nux.AgeAssuranceDismissibleNotice, 41 - completed: true, 42 - data: undefined, 43 - }) 44 - logger.metric('ageAssurance:dismissSettingsNotice', {}) 45 - }} 46 - style={[ 47 - a.absolute, 48 - { 49 - top: 12, 50 - right: 12, 51 - }, 52 - ]}> 53 - <ButtonIcon icon={X} /> 54 - </Button> 55 - </View> 56 </View> 57 ) 58 }
··· 5 import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' 6 import {atoms as a, type ViewStyleProp} from '#/alf' 7 import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition' 8 + import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors' 9 import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' 10 import {Button, ButtonIcon} from '#/components/Button' 11 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' ··· 27 28 return ( 29 <View style={style}> 30 + {aa.state.error === 'config' ? ( 31 + <AgeAssuranceConfigUnavailableError /> 32 + ) : ( 33 + <View> 34 + <AgeAssuranceAdmonition>{copy.notice}</AgeAssuranceAdmonition> 35 36 + <Button 37 + label={_(msg`Don't show again`)} 38 + size="tiny" 39 + variant="solid" 40 + color="secondary_inverted" 41 + shape="round" 42 + onPress={() => { 43 + save({ 44 + id: Nux.AgeAssuranceDismissibleNotice, 45 + completed: true, 46 + data: undefined, 47 + }) 48 + logger.metric('ageAssurance:dismissSettingsNotice', {}) 49 + }} 50 + style={[ 51 + a.absolute, 52 + { 53 + top: 12, 54 + right: 12, 55 + }, 56 + ]}> 57 + <ButtonIcon icon={X} /> 58 + </Button> 59 + </View> 60 + )} 61 </View> 62 ) 63 }
+37
src/components/ageAssurance/AgeAssuranceErrors.tsx
···
··· 1 + import {msg, Trans} from '@lingui/macro' 2 + import {useLingui} from '@lingui/react' 3 + 4 + import {type ViewStyleProp} from '#/alf' 5 + import * as Admonition from '#/components/Admonition' 6 + import {ButtonIcon, ButtonText} from '#/components/Button' 7 + import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate' 8 + import {refetchConfig} from '#/ageAssurance/data' 9 + 10 + export function AgeAssuranceConfigUnavailableError(props: ViewStyleProp) { 11 + const {_} = useLingui() 12 + return ( 13 + <Admonition.Outer type="error" style={props.style}> 14 + <Admonition.Row> 15 + <Admonition.Icon /> 16 + <Admonition.Content> 17 + <Admonition.Text> 18 + <Trans> 19 + We were unable to load the age assurance configuration for your 20 + region, probably due to a network error. Some content and features 21 + may be unavailable temporarily. Please try again later. 22 + </Trans> 23 + </Admonition.Text> 24 + </Admonition.Content> 25 + <Admonition.Button 26 + color="negative_subtle" 27 + label={_(msg`Retry`)} 28 + onPress={() => refetchConfig().catch(() => {})}> 29 + <ButtonText> 30 + <Trans>Retry</Trans> 31 + </ButtonText> 32 + <ButtonIcon icon={RetryIcon} /> 33 + </Admonition.Button> 34 + </Admonition.Row> 35 + </Admonition.Outer> 36 + ) 37 + }
+7
src/components/ageAssurance/AgeRestrictedScreen.tsx
··· 5 import {atoms as a} from '#/alf' 6 import {Admonition} from '#/components/Admonition' 7 import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 8 import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' 9 import {ButtonIcon, ButtonText} from '#/components/Button' 10 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' ··· 44 </Layout.Header.Outer> 45 <Layout.Content> 46 <View style={[a.p_lg]}> 47 <View style={[a.align_start, a.pb_lg]}> 48 <AgeAssuranceBadge /> 49 </View>
··· 5 import {atoms as a} from '#/alf' 6 import {Admonition} from '#/components/Admonition' 7 import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 8 + import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors' 9 import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' 10 import {ButtonIcon, ButtonText} from '#/components/Button' 11 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' ··· 45 </Layout.Header.Outer> 46 <Layout.Content> 47 <View style={[a.p_lg]}> 48 + {aa.state.error === 'config' && ( 49 + <View style={[a.pb_lg]}> 50 + <AgeAssuranceConfigUnavailableError /> 51 + </View> 52 + )} 53 + 54 <View style={[a.align_start, a.pb_lg]}> 55 <AgeAssuranceBadge /> 56 </View>