From 690b8184a12fa939c687862e5ccbf00b8dbdce5d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 8 Jun 2026 15:34:51 -0500 Subject: [PATCH] Refactor age assurance flags and contexts (#10794) --- src/ageAssurance/__mocks__/data.tsx | 2 +- .../components/NoAccessScreen.tsx | 10 +- src/ageAssurance/data.tsx | 46 ++-- src/ageAssurance/debug.ts | 241 ++++++++++++++++-- src/ageAssurance/index.tsx | 62 ++--- src/ageAssurance/state.ts | 80 +++--- src/ageAssurance/types.ts | 16 ++ .../useComputeAgeAssuranceRegionAccess.ts | 11 +- src/ageAssurance/util.ts | 42 ++- src/state/session/__tests__/session-test.ts | 2 +- src/state/session/agent.ts | 12 +- src/state/session/index.tsx | 12 +- 12 files changed, 390 insertions(+), 146 deletions(-) diff --git a/src/ageAssurance/__mocks__/data.tsx b/src/ageAssurance/__mocks__/data.tsx index b548a2f866..f813e14ccc 100644 --- a/src/ageAssurance/__mocks__/data.tsx +++ b/src/ageAssurance/__mocks__/data.tsx @@ -1,3 +1,3 @@ -export const prefetchAgeAssuranceData = () => {} +export const prefetchAgeAssuranceServerData = () => {} export const setBirthdateForDid = () => {} export const setCreatedAtForDid = () => {} diff --git a/src/ageAssurance/components/NoAccessScreen.tsx b/src/ageAssurance/components/NoAccessScreen.tsx index 84584b2717..ac88e59f7a 100644 --- a/src/ageAssurance/components/NoAccessScreen.tsx +++ b/src/ageAssurance/components/NoAccessScreen.tsx @@ -32,7 +32,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {BottomSheetOutlet} from '#/../modules/bottom-sheet' import {useAgeAssurance} from '#/ageAssurance' -import {useAgeAssuranceDataContext} from '#/ageAssurance/data' +import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data' import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' import { isLegacyBirthdateBug, @@ -53,7 +53,7 @@ export function NoAccessScreen() { const birthdateControl = useDialogControl() const deactivateAccountControl = useDialogControl() const deleteAccountControl = useDialogControl() - const {data} = useAgeAssuranceDataContext() + const {metadata} = useAgeAssuranceServerDataContext() const region = useAgeAssuranceRegionConfig() const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed() const {logoutCurrentAccount} = useSessionApi() @@ -62,15 +62,15 @@ export function NoAccessScreen() { const aa = useAgeAssurance() const isBlocked = aa.state.status === aa.Status.Blocked const isAARegion = !!region - const hasDeclaredAge = data?.declaredAge !== undefined + const hasDeclaredAge = metadata?.declaredAge !== undefined const canUpdateBirthday = - isBirthdateUpdateAllowed || isLegacyBirthdateBug(data?.birthdate || '') + isBirthdateUpdateAllowed || isLegacyBirthdateBug(metadata?.birthdate || '') useEffect(() => { // just counting overall hits here ax.metric(`blockedGeoOverlay:shown`, {}) ax.metric(`ageAssurance:noAccessScreen:shown`, { - accountCreatedAt: data?.accountCreatedAt || 'unknown', + accountCreatedAt: metadata?.accountCreatedAt || 'unknown', isAARegion, hasDeclaredAge, canUpdateBirthday, diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index 36a06fbd22..114d946602 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -24,6 +24,7 @@ import {fetchActorDeclarationRecord} from '#/state/queries/messages/actor-declar import {useAgent, useSession} from '#/state/session' import * as debug from '#/ageAssurance/debug' import {logger} from '#/ageAssurance/logger' +import {type AgeAssuranceMetadata} from '#/ageAssurance/types' import { getBirthdateStringFromAge, isLegacyBirthdateBug, @@ -485,9 +486,9 @@ export function useOtherRequiredDataQuery() { } /** - * Helper to prefetch all age assurance data. + * Helper to prefetch all age assurance data from the server. */ -export function prefetchAgeAssuranceData({agent}: {agent: AtpAgent}) { +export function prefetchAgeAssuranceServerData({agent}: {agent: AtpAgent}) { return Promise.allSettled([ // config fetch initiated at the top of the App.platform.tsx files, awaited here configPrefetchPromise, @@ -496,8 +497,8 @@ export function prefetchAgeAssuranceData({agent}: {agent: AtpAgent}) { ]) } -export function clearAgeAssuranceDataForDid({did}: {did: string}) { - logger.debug(`clearAgeAssuranceDataForDid: ${did}`) +export function clearAgeAssuranceServerDataForDid({did}: {did: string}) { + logger.debug(`clearAgeAssuranceServerDataForDid: ${did}`) qc.removeQueries({queryKey: createServerStateQueryKey({did}), exact: true}) qc.removeQueries({ queryKey: createOtherRequiredDataQueryKey({did}), @@ -505,8 +506,8 @@ export function clearAgeAssuranceDataForDid({did}: {did: string}) { }) } -export function clearAgeAssuranceData() { - logger.debug(`clearAgeAssuranceData`) +export function clearAgeAssuranceServerDataForAll() { + logger.debug(`clearAgeAssuranceServerDataForAll`) qc.clear() } @@ -514,30 +515,30 @@ export function clearAgeAssuranceData() { * Context */ -export type AgeAssuranceData = { +export type AgeAssuranceServerData = { + /** + * The raw config from the appview. + */ config: AppBskyAgeassuranceDefs.Config | undefined + /** + * The raw state from the appview. Must be further processed before being useful. + */ state: AppBskyAgeassuranceDefs.State | undefined - data: - | { - accountCreatedAt: AppBskyAgeassuranceDefs.StateMetadata['accountCreatedAt'] - declaredAge: number | undefined - birthdate: string | undefined - } - | undefined + metadata: AgeAssuranceMetadata | undefined } -export const AgeAssuranceDataContext = createContext({ +const AgeAssuranceServerDataContext = createContext({ config: undefined, state: undefined, - data: { + metadata: { accountCreatedAt: undefined, declaredAge: undefined, birthdate: undefined, }, }) -export function useAgeAssuranceDataContext() { - return useContext(AgeAssuranceDataContext) +export function useAgeAssuranceServerDataContext() { + return useContext(AgeAssuranceServerDataContext) } -export function AgeAssuranceDataProvider({ +export function AgeAssuranceServerDataProvider({ children, }: { children: React.ReactNode @@ -550,7 +551,8 @@ export function AgeAssuranceDataProvider({ () => ({ config, state, - data: { + metadata: { + // yes, it's weird, but accountCreatedAt comes back on the `getState` endpoint accountCreatedAt: metadata?.accountCreatedAt, declaredAge: data?.birthdate ? getAge(new Date(data.birthdate)) @@ -561,8 +563,8 @@ export function AgeAssuranceDataProvider({ [config, state, data, metadata], ) return ( - + {children} - + ) } diff --git a/src/ageAssurance/debug.ts b/src/ageAssurance/debug.ts index 257eeff93d..3368ddf147 100644 --- a/src/ageAssurance/debug.ts +++ b/src/ageAssurance/debug.ts @@ -26,35 +26,8 @@ export const deviceGeolocation: Geolocation | undefined = } : undefined -export const config: AppBskyAgeassuranceDefs.Config = { - regions: [ - { - countryCode: 'AA', - regionCode: undefined, - minAccessAge: 13, - rules: [ - { - $type: ids.Default, - access: 'full', - }, - ], - }, - { - countryCode: 'BB', - regionCode: undefined, - minAccessAge: 16, - rules: [ - { - $type: ids.Default, - access: 'full', - }, - ], - }, - ], -} - export const otherRequiredData: OtherRequiredData = { - birthdate: new Date(2000, 1, 1).toISOString(), + birthdate: new Date(2010, 12, 1).toISOString(), } const serverStateEnabled = false || IS_E2E @@ -72,6 +45,218 @@ export const serverState: AppBskyAgeassuranceGetState.OutputSchema | undefined = } : undefined +export const config: AppBskyAgeassuranceDefs.Config = { + regions: [ + { + countryCode: 'AA', + regionCode: undefined, + minAccessAge: 13, + rules: [ + { + $type: ids.Default, + access: 'full', + }, + ], + }, + { + countryCode: 'GB', + minAccessAge: 13, + rules: [ + { + age: 18, + access: 'full', + $type: ids.IfAssuredOverAge, + }, + { + age: 13, + access: 'safe', + $type: ids.IfDeclaredOverAge, + }, + { + access: 'none', + $type: ids.Default, + }, + ], + }, + { + countryCode: 'AU', + minAccessAge: 16, + rules: [ + { + date: '2025-12-10T00:00:00Z', + access: 'none', + $type: ids.IfAccountNewerThan, + }, + { + age: 18, + access: 'full', + $type: ids.IfAssuredOverAge, + }, + { + age: 16, + access: 'safe', + $type: ids.IfAssuredOverAge, + }, + { + age: 16, + access: 'safe', + $type: ids.IfDeclaredOverAge, + }, + { + access: 'none', + $type: ids.Default, + }, + ], + }, + { + countryCode: 'US', + regionCode: 'SD', + minAccessAge: 13, + rules: [ + { + age: 18, + access: 'full', + $type: ids.IfAssuredOverAge, + }, + { + age: 13, + access: 'safe', + $type: ids.IfDeclaredOverAge, + }, + { + access: 'none', + $type: ids.Default, + }, + ], + }, + { + countryCode: 'US', + regionCode: 'WY', + minAccessAge: 13, + rules: [ + { + age: 18, + access: 'full', + $type: ids.IfAssuredOverAge, + }, + { + age: 13, + access: 'safe', + $type: ids.IfDeclaredOverAge, + }, + { + access: 'none', + $type: ids.Default, + }, + ], + }, + { + countryCode: 'US', + regionCode: 'OH', + minAccessAge: 13, + rules: [ + { + age: 18, + access: 'full', + $type: ids.IfAssuredOverAge, + }, + { + age: 13, + access: 'safe', + $type: ids.IfDeclaredOverAge, + }, + { + access: 'none', + $type: ids.Default, + }, + ], + }, + { + countryCode: 'US', + regionCode: 'MS', + minAccessAge: 18, + rules: [ + { + age: 18, + access: 'full', + $type: ids.IfAssuredOverAge, + }, + { + access: 'none', + $type: ids.Default, + }, + ], + }, + { + countryCode: 'US', + regionCode: 'VA', + minAccessAge: 16, + rules: [ + { + age: 16, + access: 'full', + $type: ids.IfAssuredOverAge, + }, + { + age: 16, + access: 'full', + $type: ids.IfDeclaredOverAge, + }, + { + access: 'none', + $type: ids.Default, + }, + ], + }, + { + countryCode: 'US', + regionCode: 'TN', + minAccessAge: 18, + rules: [ + { + age: 18, + access: 'full', + $type: ids.IfAssuredOverAge, + }, + { + age: 18, + access: 'full', + $type: ids.IfDeclaredOverAge, + }, + { + access: 'none', + $type: ids.Default, + }, + ], + }, + { + countryCode: 'BR', + minAccessAge: 13, + rules: [ + { + age: 18, + access: 'full', + $type: ids.IfAssuredOverAge, + }, + { + age: 18, + access: 'full', + $type: ids.IfDeclaredOverAge, + }, + { + age: 13, + access: 'safe', + $type: ids.IfDeclaredOverAge, + }, + { + access: 'none', + $type: ids.Default, + }, + ], + }, + ], +} + export async function resolve(data: T) { await new Promise(y => setTimeout(y, 500)) // simulate network return data diff --git a/src/ageAssurance/index.tsx b/src/ageAssurance/index.tsx index a5cca327ae..2b10c93a5d 100644 --- a/src/ageAssurance/index.tsx +++ b/src/ageAssurance/index.tsx @@ -1,11 +1,11 @@ -import {createContext, useCallback, useContext, useEffect, useMemo} from 'react' +import {createContext, useCallback, useContext, useMemo} from 'react' import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications' import {useAgent} from '#/state/session' import {Provider as RedirectOverlayProvider} from '#/ageAssurance/components/RedirectOverlay' import { - AgeAssuranceDataProvider, - useAgeAssuranceDataContext, + AgeAssuranceServerDataProvider, + useAgeAssuranceServerDataContext, } from '#/ageAssurance/data' import {logger} from '#/ageAssurance/logger' import { @@ -14,19 +14,19 @@ import { } from '#/ageAssurance/state' import { AgeAssuranceAccess, + type AgeAssuranceFlags, type AgeAssuranceState, AgeAssuranceStatus, } from '#/ageAssurance/types' import { - isUnderAge, + computeAgeAssuranceFlags, maybeRestrictChatSettings, - MIN_ACCESS_AGE, useAgeAssuranceRegionConfigWithFallback, } from '#/ageAssurance/util' export { prefetchConfig as prefetchAgeAssuranceConfig, - prefetchAgeAssuranceData, + prefetchAgeAssuranceServerData, refetchServerState as refetchAgeAssuranceServerState, usePatchOtherRequiredData as usePatchAgeAssuranceOtherRequiredData, usePatchServerState as usePatchAgeAssuranceServerState, @@ -38,13 +38,7 @@ const AgeAssuranceStateContext = createContext<{ Access: typeof AgeAssuranceAccess Status: typeof AgeAssuranceStatus state: AgeAssuranceState - flags: { - adultContentDisabled: boolean - chatDisabled: boolean - isDeclaredUnderAdultAge: boolean - isOverRegionMinAccessAge: boolean - isOverAppMinAccessAge: boolean - } + flags: AgeAssuranceFlags }>({ Access: AgeAssuranceAccess, Status: AgeAssuranceStatus, @@ -73,19 +67,19 @@ export function useAgeAssurance() { export function Provider({children}: {children: React.ReactNode}) { return ( - + {children} - + ) } function InnerProvider({children}: {children: React.ReactNode}) { const agent = useAgent() const state = useAgeAssuranceState() - const {data} = useAgeAssuranceDataContext() - const config = useAgeAssuranceRegionConfigWithFallback() + const {metadata} = useAgeAssuranceServerDataContext() + const regionConfig = useAgeAssuranceRegionConfigWithFallback() const getAndRegisterPushToken = useGetAndRegisterPushToken() const handleAccessUpdate = useCallback( @@ -100,38 +94,22 @@ function InnerProvider({children}: {children: React.ReactNode}) { ) useOnAgeAssuranceAccessUpdate(handleAccessUpdate) - useEffect(() => { - logger.debug(`useAgeAssuranceState`, {state}) - }, [state]) - return ( { - const chatDisabled = state.access !== AgeAssuranceAccess.Full - const isDeclaredUnderAdultAge = data?.birthdate - ? isUnderAge(data.birthdate, 18) - : true - const isOverRegionMinAccessAge = data?.birthdate - ? !isUnderAge(data.birthdate, config.minAccessAge) - : false - const isOverAppMinAccessAge = data?.birthdate - ? !isUnderAge(data.birthdate, MIN_ACCESS_AGE) - : false - const adultContentDisabled = - state.access !== AgeAssuranceAccess.Full || isDeclaredUnderAdultAge - return { + const res = { Access: AgeAssuranceAccess, Status: AgeAssuranceStatus, state, - flags: { - adultContentDisabled, - chatDisabled, - isDeclaredUnderAdultAge, - isOverRegionMinAccessAge, - isOverAppMinAccessAge, - }, + flags: computeAgeAssuranceFlags({ + state, + regionConfig, + metadata, + }), } - }, [state, data, config])}> + logger.debug(`useAgeAssurance`, res) + return res + }, [state, metadata, regionConfig])}> {children} ) diff --git a/src/ageAssurance/state.ts b/src/ageAssurance/state.ts index 5aac40ef44..ff80bca725 100644 --- a/src/ageAssurance/state.ts +++ b/src/ageAssurance/state.ts @@ -1,24 +1,30 @@ import {useEffect, useMemo, useState} from 'react' -import {computeAgeAssuranceRegionAccess} from '@atproto/api' +import { + type AppBskyAgeassuranceDefs, + computeAgeAssuranceRegionAccess, +} from '@atproto/api' import {getAge} from '#/lib/strings/time' import {useSession} from '#/state/session' import { - type AgeAssuranceData, getConfigFromCache, getOtherRequiredDataFromCache, getServerStateFromCache, - useAgeAssuranceDataContext, + useAgeAssuranceServerDataContext, } from '#/ageAssurance/data' import {logger} from '#/ageAssurance/logger' import { AgeAssuranceAccess, + type AgeAssuranceMetadata, type AgeAssuranceState, AgeAssuranceStatus, parseAccessFromString, parseStatusFromString, } from '#/ageAssurance/types' -import {getAgeAssuranceRegionConfigWithFallback} from '#/ageAssurance/util' +import { + computeAgeAssuranceFlags, + getAgeAssuranceRegionConfigWithFallback, +} from '#/ageAssurance/util' import {type Geolocation, useGeolocation} from '#/geolocation' import {device} from '#/storage' @@ -27,18 +33,18 @@ import {device} from '#/storage' * server state before computing access based on AA config from the server + * geolocation and other data. */ -export function computeAgeAssuranceState({ +function computeAgeAssuranceState({ hasSession, - config, geolocation, + config, state, - data, + metadata, }: { hasSession: boolean - config: AgeAssuranceData['config'] geolocation: Geolocation - state: AgeAssuranceData['state'] - data: AgeAssuranceData['data'] + config?: AppBskyAgeassuranceDefs.Config + state?: AppBskyAgeassuranceDefs.State + metadata?: AgeAssuranceMetadata }) { /** * This is where we control logged-out moderation prefs. It's all @@ -88,7 +94,10 @@ export function computeAgeAssuranceState({ * accounts with an accurate birthdate, our default fallback rules should * ensure correct access. */ - const result = computeAgeAssuranceRegionAccess(region, data) + const result = computeAgeAssuranceRegionAccess(region, { + accountCreatedAt: metadata?.accountCreatedAt, + declaredAge: metadata?.declaredAge, + }) const computed = { lastInitiatedAt: state?.lastInitiatedAt, // prefer server state @@ -100,10 +109,10 @@ export function computeAgeAssuranceState({ ? parseAccessFromString(result.access) : AgeAssuranceAccess.Full, } - logger.debug('debug useAgeAssuranceState', { + logger.debug('computeAgeAssuranceState', { region, state, - data, + metadata, computed, }) return computed @@ -113,38 +122,51 @@ export function computeAgeAssuranceState({ * This is a last-ditch helper for out-of-band reads of the AA state, such as * during account creation. Don't use it for anything else. */ -export function getAndComputeAgeAssuranceState({did}: {did: string}) { +export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) { const config = getConfigFromCache() const state = getServerStateFromCache({did}) - const data = getOtherRequiredDataFromCache({did}) + const requiredData = getOtherRequiredDataFromCache({did}) const geolocation = device.get(['mergedGeolocation']) - if (!geolocation || !config || !state || !data) { + if (!geolocation || !config || !state || !requiredData) { return { - status: AgeAssuranceStatus.Unknown, - access: AgeAssuranceAccess.Safe, + state: { + status: AgeAssuranceStatus.Unknown, + access: AgeAssuranceAccess.Safe, + }, } } - return computeAgeAssuranceState({ + const region = getAgeAssuranceRegionConfigWithFallback(config, geolocation) + const metadata: AgeAssuranceMetadata = { + accountCreatedAt: state.metadata?.accountCreatedAt, + declaredAge: requiredData?.birthdate + ? getAge(new Date(requiredData.birthdate)) + : undefined, + birthdate: requiredData?.birthdate, + } + const computed = computeAgeAssuranceState({ hasSession: true, config, geolocation, state: state.state, - data: { - accountCreatedAt: state.metadata?.accountCreatedAt, - declaredAge: data?.birthdate - ? getAge(new Date(data.birthdate)) - : undefined, - birthdate: data?.birthdate, - }, + metadata, }) + + return { + state: computed, + flags: computeAgeAssuranceFlags({ + state: computed, + regionConfig: region, + metadata, + }), + } } export function useAgeAssuranceState(): AgeAssuranceState { const {hasSession} = useSession() const geolocation = useGeolocation() - const {config, state, data} = useAgeAssuranceDataContext() + const {config, state, metadata} = useAgeAssuranceServerDataContext() return useMemo( () => @@ -153,9 +175,9 @@ export function useAgeAssuranceState(): AgeAssuranceState { config, geolocation, state, - data, + metadata, }), - [hasSession, geolocation, config, state, data], + [hasSession, geolocation, config, state, metadata], ) } diff --git a/src/ageAssurance/types.ts b/src/ageAssurance/types.ts index f34ed10aea..12473bc2e6 100644 --- a/src/ageAssurance/types.ts +++ b/src/ageAssurance/types.ts @@ -1,3 +1,5 @@ +import {type computeAgeAssuranceRegionAccess} from '@atproto/api' + import {logger} from '#/ageAssurance/logger' export enum AgeAssuranceAccess { @@ -14,6 +16,12 @@ export enum AgeAssuranceStatus { Blocked = 'blocked', } +export type AgeAssuranceMetadata = Parameters< + typeof computeAgeAssuranceRegionAccess +>[1] & { + birthdate: string | undefined +} + export type AgeAssuranceState = { lastInitiatedAt?: string status: AgeAssuranceStatus @@ -21,6 +29,14 @@ export type AgeAssuranceState = { error?: 'config' // maybe other specific cases in the future } +export type AgeAssuranceFlags = { + adultContentDisabled: boolean + chatDisabled: boolean + isDeclaredUnderAdultAge: boolean + isOverRegionMinAccessAge: boolean + isOverAppMinAccessAge: boolean +} + export function parseStatusFromString(raw: string) { switch (raw) { case 'unknown': diff --git a/src/ageAssurance/useComputeAgeAssuranceRegionAccess.ts b/src/ageAssurance/useComputeAgeAssuranceRegionAccess.ts index e3ea48860f..5ba9e1ba6d 100644 --- a/src/ageAssurance/useComputeAgeAssuranceRegionAccess.ts +++ b/src/ageAssurance/useComputeAgeAssuranceRegionAccess.ts @@ -1,14 +1,14 @@ import {useCallback} from 'react' import {computeAgeAssuranceRegionAccess} from '@atproto/api' -import {useAgeAssuranceDataContext} from '#/ageAssurance/data' +import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data' import {logger} from '#/ageAssurance/logger' import {AgeAssuranceAccess, parseAccessFromString} from '#/ageAssurance/types' import {getAgeAssuranceRegionConfigWithFallback} from '#/ageAssurance/util' import {type Geolocation} from '#/geolocation' export function useComputeAgeAssuranceRegionAccess() { - const {config, data} = useAgeAssuranceDataContext() + const {config, metadata} = useAgeAssuranceServerDataContext() return useCallback( (geolocation: Geolocation) => { if (!config) { @@ -19,11 +19,14 @@ export function useComputeAgeAssuranceRegionAccess() { config, geolocation, ) - const result = computeAgeAssuranceRegionAccess(region, data) + const result = computeAgeAssuranceRegionAccess(region, { + accountCreatedAt: metadata?.accountCreatedAt, + declaredAge: metadata?.declaredAge, + }) return result ? parseAccessFromString(result.access) : AgeAssuranceAccess.Full }, - [config, data], + [config, metadata], ) } diff --git a/src/ageAssurance/util.ts b/src/ageAssurance/util.ts index 310725db8f..b0e601d6c7 100644 --- a/src/ageAssurance/util.ts +++ b/src/ageAssurance/util.ts @@ -13,9 +13,14 @@ import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/ import { getDidFromAgentSession, getOtherRequiredDataFromCache, - useAgeAssuranceDataContext, + useAgeAssuranceServerDataContext, } from '#/ageAssurance/data' -import {AgeAssuranceAccess} from '#/ageAssurance/types' +import { + AgeAssuranceAccess, + type AgeAssuranceFlags, + type AgeAssuranceMetadata, + type AgeAssuranceState, +} from '#/ageAssurance/types' import {type Geolocation, useGeolocation} from '#/geolocation' export const MIN_ACCESS_AGE = 13 @@ -62,7 +67,7 @@ export function getAgeAssuranceRegionConfigWithFallback( */ export function useAgeAssuranceRegionConfig() { const geolocation = useGeolocation() - const {config} = useAgeAssuranceDataContext() + const {config} = useAgeAssuranceServerDataContext() return useMemo(() => { if (!config) return // use generic helper, we want to potentially return undefined @@ -128,3 +133,34 @@ export function maybeRestrictChatSettings({agent}: {agent: AtpAgent}) { if (data?.actorDeclaration?.allowIncoming === 'none') return restrictChatSettings({agent, did}) } + +export function computeAgeAssuranceFlags({ + state, + regionConfig, + metadata, +}: { + state: AgeAssuranceState + regionConfig: AppBskyAgeassuranceDefs.ConfigRegion + metadata?: AgeAssuranceMetadata +}): AgeAssuranceFlags { + const chatDisabled = state.access !== AgeAssuranceAccess.Full + const isDeclaredUnderAdultAge = metadata?.declaredAge + ? metadata.declaredAge < 18 + : true + const isOverRegionMinAccessAge = metadata?.declaredAge + ? metadata.declaredAge >= regionConfig.minAccessAge + : false + const isOverAppMinAccessAge = metadata?.declaredAge + ? metadata.declaredAge >= MIN_ACCESS_AGE + : false + const adultContentDisabled = + state.access !== AgeAssuranceAccess.Full || isDeclaredUnderAdultAge + + return { + adultContentDisabled, + chatDisabled, + isDeclaredUnderAdultAge, + isOverRegionMinAccessAge, + isOverAppMinAccessAge, + } +} diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts index 4b014d6448..4398a90a0b 100644 --- a/src/state/session/__tests__/session-test.ts +++ b/src/state/session/__tests__/session-test.ts @@ -13,7 +13,7 @@ jest.mock('jwt-decode', () => ({ jest.mock('../../birthdate') jest.mock('../../../ageAssurance/data') jest.mock('../../../ageAssurance/state', () => ({ - getAndComputeAgeAssuranceState: () => ({}), + unsafeGetAndComputeAgeAssurance: () => ({state: {}}), })) jest.mock('#/lib/notifications/notifications', () => ({ unregisterPushToken(_agents: BskyAgent[]) { diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 9f2d70927b..0abc0ca6cf 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -24,11 +24,11 @@ import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate' import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' import { - prefetchAgeAssuranceData, + prefetchAgeAssuranceServerData, setBirthdateForDid, setCreatedAtForDid, } from '#/ageAssurance/data' -import {getAndComputeAgeAssuranceState} from '#/ageAssurance/state' +import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state' import {AgeAssuranceAccess} from '#/ageAssurance/types' import {features} from '#/analytics' import {emitNetworkConfirmed, emitNetworkLost} from '../events' @@ -74,7 +74,7 @@ export async function createAgentAndResume( } // after session is attached - const aa = prefetchAgeAssuranceData({agent}) + const aa = prefetchAgeAssuranceServerData({agent}) agent.configureProxy(BLUESKY_PROXY_HEADER.get()) @@ -113,7 +113,7 @@ export async function createAgentAndLogin( const account = agentToSessionAccountOrThrow(agent) const gates = features.refresh({strategy: 'prefer-fresh-gates'}) const moderation = configureModerationForAccount(agent, account) - const aa = prefetchAgeAssuranceData({agent}) + const aa = prefetchAgeAssuranceServerData({agent}) agent.configureProxy(BLUESKY_PROXY_HEADER.get()) @@ -175,7 +175,7 @@ export async function createAgentAndCreateAccount( setBirthdateForDid({did: account.did, birthdate}) snoozeBirthdateUpdateAllowedForDid(account.did) // do this last - const aa = prefetchAgeAssuranceData({agent}) + const aa = prefetchAgeAssuranceServerData({agent}) // Not awaited so that we can still get into onboarding. // This is OK because we won't let you toggle adult stuff until you set the date. @@ -219,7 +219,7 @@ export async function createAgentAndCreateAccount( }), // wait for AA data to load first, then check state aa.then(async () => { - const state = getAndComputeAgeAssuranceState({did: account.did}) + const {state} = unsafeGetAndComputeAgeAssurance({did: account.did}) if (state.access !== AgeAssuranceAccess.Full) { restrictChatSettings({agent, did: account.did}) } diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 09e7bd0db0..fd31261a9d 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -36,8 +36,8 @@ import { } from '#/state/session/types' import {useOnboardingDispatch} from '#/state/shell/onboarding' import { - clearAgeAssuranceData, - clearAgeAssuranceDataForDid, + clearAgeAssuranceServerDataForAll, + clearAgeAssuranceServerDataForDid, } from '#/ageAssurance/data' const StateContext = createContext({ @@ -203,7 +203,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) addSessionDebugLog({type: 'method:end', method: 'logout'}) if (prevState.currentAgentState.did) { - clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did}) + clearAgeAssuranceServerDataForDid({ + did: prevState.currentAgentState.did, + }) void clearPersistedQueryStorage(prevState.currentAgentState.did) } // reset onboarding flow on logout @@ -234,7 +236,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }, ) addSessionDebugLog({type: 'method:end', method: 'logout'}) - clearAgeAssuranceData() + clearAgeAssuranceServerDataForAll() for (const account of prevState.accounts) { void clearPersistedQueryStorage(account.did) } @@ -304,7 +306,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { accountDid: account.did, }) addSessionDebugLog({type: 'method:end', method: 'removeAccount', account}) - clearAgeAssuranceDataForDid({did: account.did}) + clearAgeAssuranceServerDataForDid({did: account.did}) }, [store, cancelPendingTask], )