diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index e06e291052..f6353a899b 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -136,6 +136,15 @@ export async function prefetchConfig() { } }) } +export async function refetchConfig() { + logger.debug(`refetchConfig: fetching...`) + const res = await getConfig() + qc.setQueryData( + configQueryKey, + res, + ) + return res +} export function useConfigQuery() { 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 */ 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(), initialDataUpdatedAt: () => qc.getQueryState(configQueryKey)?.dataUpdatedAt, diff --git a/src/ageAssurance/state.ts b/src/ageAssurance/state.ts index cc8b60ac52..f4d1e41e3a 100644 --- a/src/ageAssurance/state.ts +++ b/src/ageAssurance/state.ts @@ -30,12 +30,19 @@ export function useAgeAssuranceState(): AgeAssuranceState { 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) { logger.warn('useAgeAssuranceState: missing config') return { status: AgeAssuranceStatus.Unknown, - access: AgeAssuranceAccess.Unknown, + access: AgeAssuranceAccess.Safe, + error: 'config', } } diff --git a/src/ageAssurance/types.ts b/src/ageAssurance/types.ts index 9f83975d3e..f34ed10aea 100644 --- a/src/ageAssurance/types.ts +++ b/src/ageAssurance/types.ts @@ -18,6 +18,7 @@ export type AgeAssuranceState = { lastInitiatedAt?: string status: AgeAssuranceStatus access: AgeAssuranceAccess + error?: 'config' // maybe other specific cases in the future } export function parseStatusFromString(raw: string) { diff --git a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx index a75e75d133..ecdff5b381 100644 --- a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx +++ b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx @@ -8,6 +8,7 @@ import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' import {Admonition} from '#/components/Admonition' import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' +import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors' import { AgeAssuranceInitDialog, useDialogControl, @@ -27,6 +28,13 @@ import {useDeviceGeolocationApi} from '#/geolocation' export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) { const aa = useAgeAssurance() if (aa.state.access === aa.Access.Full) return null + if (aa.state.error === 'config') { + return ( + + + + ) + } return } diff --git a/src/components/ageAssurance/AgeAssuranceAdmonition.tsx b/src/components/ageAssurance/AgeAssuranceAdmonition.tsx index 7889070c09..1ef29dfbbb 100644 --- a/src/components/ageAssurance/AgeAssuranceAdmonition.tsx +++ b/src/components/ageAssurance/AgeAssuranceAdmonition.tsx @@ -3,6 +3,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf' +import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors' import {useDialogControl} from '#/components/ageAssurance/AgeAssuranceInitDialog' import type * as Dialog from '#/components/Dialog' import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' @@ -19,6 +20,9 @@ export function AgeAssuranceAdmonition({ const aa = useAgeAssurance() if (aa.state.access === aa.Access.Full) return null + if (aa.state.error === 'config') { + return + } return ( diff --git a/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx b/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx index 3471393451..aa1d527f62 100644 --- a/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx +++ b/src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx @@ -23,6 +23,7 @@ export function useInternalState() { const visible = useMemo(() => { if (aa.state.access === aa.Access.Full) return false if (aa.state.lastInitiatedAt) return false + if (aa.state.error === 'config') return false if (hidden) return false if (nux && nux.completed) return false return true diff --git a/src/components/ageAssurance/AgeAssuranceDismissibleNotice.tsx b/src/components/ageAssurance/AgeAssuranceDismissibleNotice.tsx index 934ac8d14a..14c7ed300f 100644 --- a/src/components/ageAssurance/AgeAssuranceDismissibleNotice.tsx +++ b/src/components/ageAssurance/AgeAssuranceDismissibleNotice.tsx @@ -5,6 +5,7 @@ import {useLingui} from '@lingui/react' import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' import {atoms as a, type ViewStyleProp} from '#/alf' import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition' +import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import {Button, ButtonIcon} from '#/components/Button' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' @@ -26,33 +27,37 @@ export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) { return ( - - {copy.notice} + {aa.state.error === 'config' ? ( + + ) : ( + + {copy.notice} - - + + + )} ) } diff --git a/src/components/ageAssurance/AgeAssuranceErrors.tsx b/src/components/ageAssurance/AgeAssuranceErrors.tsx new file mode 100644 index 0000000000..628153f084 --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceErrors.tsx @@ -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 ( + + + + + + + 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. + + + + refetchConfig().catch(() => {})}> + + Retry + + + + + + ) +} diff --git a/src/components/ageAssurance/AgeRestrictedScreen.tsx b/src/components/ageAssurance/AgeRestrictedScreen.tsx index 85881a3ada..9f9c4c4d5b 100644 --- a/src/components/ageAssurance/AgeRestrictedScreen.tsx +++ b/src/components/ageAssurance/AgeRestrictedScreen.tsx @@ -5,6 +5,7 @@ import {useLingui} from '@lingui/react' import {atoms as a} from '#/alf' import {Admonition} from '#/components/Admonition' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' +import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import {ButtonIcon, ButtonText} from '#/components/Button' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' @@ -44,6 +45,12 @@ export function AgeRestrictedScreen({ + {aa.state.error === 'config' && ( + + + + )} +