From dacd3cfbca415be65b285cf314b68c74d05b1c65 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 15 Jul 2025 12:31:27 -0500 Subject: [PATCH] Big refactor --- .eslintrc.js | 1 + .../ageAssurance/AgeAssuranceAccountCard.tsx | 2 +- .../ageAssurance/AgeAssuranceAdmonition.tsx | 100 ++++++++++ .../AgeAssuranceDismissableNotice.tsx | 58 ++++++ .../ageAssurance/AgeAssurancePrompt.tsx | 147 -------------- .../ageAssurance/AgeRestrictedScreen.tsx | 179 ++++++------------ .../ageAssurance/IsAgeRestricted.tsx | 53 ------ src/components/ageAssurance/useAgeInfo.ts | 3 +- src/screens/Moderation/index.tsx | 45 +++-- src/screens/Settings/Settings.tsx | 4 +- src/state/age-assurance/index.tsx | 2 +- 11 files changed, 254 insertions(+), 340 deletions(-) create mode 100644 src/components/ageAssurance/AgeAssuranceAdmonition.tsx create mode 100644 src/components/ageAssurance/AgeAssuranceDismissableNotice.tsx delete mode 100644 src/components/ageAssurance/AgeAssurancePrompt.tsx delete mode 100644 src/components/ageAssurance/IsAgeRestricted.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 726cc56074..59c1b4eb4f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,7 @@ module.exports = { 'P', 'Admonition', 'Admonition.Admonition', + 'AgeAssuranceAdmonition', 'Span', ], impliedTextProps: [], diff --git a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx index 1686bb8e1e..3eae2424fc 100644 --- a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx +++ b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx @@ -23,7 +23,7 @@ export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) { const {isLoaded, assurance, declaredAge} = useAgeInfo() if (!isLoaded) return null - if (declaredAge < 18) return null + if (declaredAge && declaredAge < 18) return null if (!assurance.isAgeRestricted) return null return diff --git a/src/components/ageAssurance/AgeAssuranceAdmonition.tsx b/src/components/ageAssurance/AgeAssuranceAdmonition.tsx new file mode 100644 index 0000000000..a8ec7b043c --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceAdmonition.tsx @@ -0,0 +1,100 @@ +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf' +import {useDialogControl} from '#/components/ageAssurance/AgeAssuranceInitDialog' +import {useAgeInfo} from '#/components/ageAssurance/useAgeInfo' +import type * as Dialog from '#/components/Dialog' +import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' +import {InlineLinkText} from '#/components/Link' +import {Text} from '#/components/Typography' + +export function AgeAssuranceAdmonition({ + children, + style, +}: ViewStyleProp & {children: React.ReactNode}) { + const control = useDialogControl() + const {isLoaded, isUnderage, assurance} = useAgeInfo() + + if (!isLoaded) return null + if (isUnderage) return null + if (!assurance.isAgeRestricted) return null + + return ( + + {children} + + ) +} + +function Inner({ + children, + style, +}: ViewStyleProp & { + children: React.ReactNode + control: Dialog.DialogControlProps +}) { + const t = useTheme() + const {_} = useLingui() + + return ( + <> + + + + + + + {children} + + + Learn more in your{' '} + + account settings. + + + + + + + + ) +} diff --git a/src/components/ageAssurance/AgeAssuranceDismissableNotice.tsx b/src/components/ageAssurance/AgeAssuranceDismissableNotice.tsx new file mode 100644 index 0000000000..03972a9e86 --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceDismissableNotice.tsx @@ -0,0 +1,58 @@ +import {View} from 'react-native' +import {msg} from '@lingui/macro' +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 {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' +import {useAgeInfo} from '#/components/ageAssurance/useAgeInfo' +import {Button, ButtonIcon} from '#/components/Button' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' + +export function AgeAssuranceDismissableNotice({style}: ViewStyleProp & {}) { + const {_} = useLingui() + const {isLoaded, isUnderage, assurance} = useAgeInfo() + const {nux} = useNux(Nux.AgeAssurancePrompt) + const copy = useAgeAssuranceCopy() + const {mutate: save, variables} = useSaveNux() + const hidden = !!variables + + if (!isLoaded) return null + if (isUnderage) return null + if (!assurance.isAgeRestricted) return null + if (assurance.lastInitiatedAt) return null + if (hidden) return null + if (nux && nux.completed) return null + + return ( + + + {copy.notice} + + + + + ) +} diff --git a/src/components/ageAssurance/AgeAssurancePrompt.tsx b/src/components/ageAssurance/AgeAssurancePrompt.tsx deleted file mode 100644 index e371bc17ff..0000000000 --- a/src/components/ageAssurance/AgeAssurancePrompt.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useAgeAssuranceContext} from '#/state/age-assurance' -import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' -import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf' -import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' -import { - AgeAssuranceInitDialog, - useDialogControl, -} from '#/components/ageAssurance/AgeAssuranceInitDialog' -import {IsAgeRestricted} from '#/components/ageAssurance/IsAgeRestricted' -import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import type * as Dialog from '#/components/Dialog' -import {Divider} from '#/components/Divider' -import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -import {Text} from '#/components/Typography' - -export function AgeAssurancePrompt({style}: ViewStyleProp & {}) { - const control = useDialogControl() - const {hasInitiated} = useAgeAssuranceContext() - const {nux} = useNux(Nux.AgeAssurancePrompt) - - if (nux && nux.completed) return null - - return ( - <> - - {!hasInitiated && } - - - - ) -} - -function Inner({ - style, - control, -}: ViewStyleProp & {control: Dialog.DialogControlProps}) { - const t = useTheme() - const {_} = useLingui() - const copy = useAgeAssuranceCopy() - const {mutate: save} = useSaveNux() - - return ( - <> - - - - - - {copy.notice} - - - - - - {copy.noticeSub} - - - - - - - - - - ) -} diff --git a/src/components/ageAssurance/AgeRestrictedScreen.tsx b/src/components/ageAssurance/AgeRestrictedScreen.tsx index f30a5dc35f..5a7b28a7c5 100644 --- a/src/components/ageAssurance/AgeRestrictedScreen.tsx +++ b/src/components/ageAssurance/AgeRestrictedScreen.tsx @@ -1,147 +1,94 @@ -import {useMemo} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {atoms as a, select, useTheme} from '#/alf' +import {atoms as a} from '#/alf' import {Admonition} from '#/components/Admonition' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' -import { - AgeAssuranceInitDialog, - useDialogControl, -} from '#/components/ageAssurance/AgeAssuranceInitDialog' -import {IsAgeRestricted} from '#/components/ageAssurance/IsAgeRestricted' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' -import {Button, ButtonText} from '#/components/Button' -import {Divider} from '#/components/Divider' +import {useAgeInfo} from '#/components/ageAssurance/useAgeInfo' +import {ButtonIcon, ButtonText} from '#/components/Button' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import * as Layout from '#/components/Layout' +import {Link} from '#/components/Link' import {Text} from '#/components/Typography' export function AgeRestrictedScreen({ children, - fallback, screenTitle, infoText, }: { children: React.ReactNode - fallback?: React.ReactNode screenTitle?: string infoText?: string }) { - const t = useTheme() const {_} = useLingui() const copy = useAgeAssuranceCopy() - const control = useDialogControl() - const screenFallback = useMemo(() => { + const {isLoaded, assurance} = useAgeInfo() + + if (assurance.isExempt) return children + if (!isLoaded) { return ( - fallback || ( - - - - - - - - - - ) + + + + + + + + + ) - }, [fallback]) + } + if (!assurance.isAgeRestricted) return children return ( - <> - - - - - - - {screenTitle ?? Unavailable} - - - - - - - - + + + + + + {screenTitle ?? Unavailable} + + + + + + + + + - - - + + + + You must verify your age in order to access this screen. + + - - {copy.notice} - + + {copy.notice} + + - - - You must complete age verification to access this screen. - - + + + + Go to account settings + + + + - - - - - {copy.noticeSub} - - - - - - - {infoText && {infoText}} - - - - - - - {children} - - + {infoText && {infoText}} + + + ) } diff --git a/src/components/ageAssurance/IsAgeRestricted.tsx b/src/components/ageAssurance/IsAgeRestricted.tsx deleted file mode 100644 index 4bf8c9b105..0000000000 --- a/src/components/ageAssurance/IsAgeRestricted.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import {useAgeAssuranceContext} from '#/state/age-assurance' - -export function True({ - children, - fallback, -}: { - children: React.ReactNode - fallback?: React.ReactNode -}) { - const {isLoaded, isAgeRestricted, isExempt} = useAgeAssuranceContext() - /** - * For the true case, if the user is exempt, return nothing - */ - if (isExempt) return null - - const isDefinitelyAgeRestricted = isLoaded && isAgeRestricted - const notSureYet = isAgeRestricted - - return isDefinitelyAgeRestricted - ? children - : notSureYet - ? fallback || null - : null -} - -export function False({ - children, - fallback, -}: { - children: React.ReactNode - fallback?: React.ReactNode -}) { - const {isLoaded, isAgeRestricted, isExempt} = useAgeAssuranceContext() - - /** - * For the false case, if the user is exempt, return children - */ - if (isExempt) return children - - const isDefinitelyNotAgeRestricted = isLoaded && !isAgeRestricted - const notSureYet = !isAgeRestricted - - return isDefinitelyNotAgeRestricted - ? children - : notSureYet - ? fallback || null - : null -} - -export const IsAgeRestricted = { - True, - False, -} diff --git a/src/components/ageAssurance/useAgeInfo.ts b/src/components/ageAssurance/useAgeInfo.ts index b168100496..73eae5d3d9 100644 --- a/src/components/ageAssurance/useAgeInfo.ts +++ b/src/components/ageAssurance/useAgeInfo.ts @@ -7,12 +7,13 @@ export function useAgeInfo() { const ctx = useAgeAssuranceContext() const {isFetched: preferencesLoaded, data: preferences} = usePreferencesQuery() - const declaredAge = useMemo(() => preferences?.userAge || -1, [preferences]) + const declaredAge = useMemo(() => preferences?.userAge, [preferences]) return useMemo(() => { return { isLoaded: ctx.isLoaded && preferencesLoaded, declaredAge, + isUnderage: ctx.isAgeRestricted && (declaredAge || 0) < 18, assurance: ctx, } }, [ctx, preferencesLoaded, declaredAge]) diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index adadcb22c8..200a6a1e44 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -23,7 +23,8 @@ import {isNonConfigurableModerationAuthority} from '#/state/session/additional-m import {useSetMinimalShellMode} from '#/state/shell' import {ViewHeader} from '#/view/com/util/ViewHeader' import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' -import {AgeAssurancePrompt} from '#/components/ageAssurance/AgeAssurancePrompt' +import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition' +import {useAgeInfo} from '#/components/ageAssurance/useAgeInfo' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' @@ -86,8 +87,9 @@ export function ModerationScreen( error: preferencesError, data: preferences, } = usePreferencesQuery() + const {isLoaded: isAgeInfoLoaded} = useAgeInfo() - const isLoading = isPreferencesLoading + const isLoading = isPreferencesLoading || !isAgeInfoLoaded const error = preferencesError return ( @@ -160,6 +162,7 @@ export function ModerationScreenInner({ error: labelersError, } = useMyLabelersQuery() const {isAgeRestricted} = useAgeAssuranceContext() + const {declaredAge, isUnderage, assurance: ageAssurance} = useAgeInfo() useFocusEffect( useCallback(() => { @@ -173,8 +176,6 @@ export function ModerationScreenInner({ (optimisticAdultContent && optimisticAdultContent.enabled) || (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled) ) - const ageNotSet = !preferences.userAge - const isUnderage = (preferences.userAge || 0) < 18 const onToggleAdultContentEnabled = useCallback( async (selected: boolean) => { @@ -309,10 +310,15 @@ export function ModerationScreenInner({ Content filters - + + + You must complete age verification in order to access the settings + below. + + - {ageNotSet && ( + {declaredAge === undefined && ageAssurance.isAgeRestricted && ( <>