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/index.tsx b/src/ageAssurance/index.tsx index d16827f28d..ab2b6c97f8 100644 --- a/src/ageAssurance/index.tsx +++ b/src/ageAssurance/index.tsx @@ -4,8 +4,8 @@ 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 { @@ -26,7 +26,7 @@ import { export { prefetchConfig as prefetchAgeAssuranceConfig, - prefetchAgeAssuranceData, + prefetchAgeAssuranceServerData, refetchServerState as refetchAgeAssuranceServerState, usePatchOtherRequiredData as usePatchAgeAssuranceOtherRequiredData, usePatchServerState as usePatchAgeAssuranceServerState, @@ -67,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( @@ -107,11 +107,11 @@ function InnerProvider({children}: {children: React.ReactNode}) { state, flags: computeAgeAssuranceFlags({ state, - config, - data, + regionConfig, + metadata, }), } - }, [state, data, config])}> + }, [state, metadata, regionConfig])}> {children} ) diff --git a/src/ageAssurance/state.ts b/src/ageAssurance/state.ts index aff218292a..bb848f910a 100644 --- a/src/ageAssurance/state.ts +++ b/src/ageAssurance/state.ts @@ -1,18 +1,21 @@ 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, @@ -32,16 +35,16 @@ import {device} from '#/storage' */ 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 @@ -91,7 +94,10 @@ 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 @@ -106,7 +112,7 @@ function computeAgeAssuranceState({ logger.debug('debug useAgeAssuranceState', { region, state, - data, + metadata, computed, }) return computed @@ -132,7 +138,7 @@ export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) { } const region = getAgeAssuranceRegionConfigWithFallback(config, geolocation) - const data = { + const metadata: AgeAssuranceMetadata = { accountCreatedAt: state.metadata?.accountCreatedAt, declaredAge: requiredData?.birthdate ? getAge(new Date(requiredData.birthdate)) @@ -144,15 +150,15 @@ export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) { config, geolocation, state: state.state, - data, + metadata, }) return { state: computed, flags: computeAgeAssuranceFlags({ state: computed, - config: region, - data, + regionConfig: region, + metadata, }), } } @@ -160,7 +166,7 @@ export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) { export function useAgeAssuranceState(): AgeAssuranceState { const {hasSession} = useSession() const geolocation = useGeolocation() - const {config, state, data} = useAgeAssuranceDataContext() + const {config, state, metadata} = useAgeAssuranceServerDataContext() return useMemo( () => @@ -169,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 d6a5b7889c..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 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 151fb1ebfa..b0e601d6c7 100644 --- a/src/ageAssurance/util.ts +++ b/src/ageAssurance/util.ts @@ -11,14 +11,14 @@ import {getAge} from '#/lib/strings/time' import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' import { - type AgeAssuranceData, getDidFromAgentSession, getOtherRequiredDataFromCache, - useAgeAssuranceDataContext, + useAgeAssuranceServerDataContext, } from '#/ageAssurance/data' import { AgeAssuranceAccess, type AgeAssuranceFlags, + type AgeAssuranceMetadata, type AgeAssuranceState, } from '#/ageAssurance/types' import {type Geolocation, useGeolocation} from '#/geolocation' @@ -67,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 @@ -136,22 +136,22 @@ export function maybeRestrictChatSettings({agent}: {agent: AtpAgent}) { export function computeAgeAssuranceFlags({ state, - config, - data, + regionConfig, + metadata, }: { state: AgeAssuranceState - config: AppBskyAgeassuranceDefs.ConfigRegion - data: AgeAssuranceData['data'] + regionConfig: AppBskyAgeassuranceDefs.ConfigRegion + metadata?: AgeAssuranceMetadata }): AgeAssuranceFlags { const chatDisabled = state.access !== AgeAssuranceAccess.Full - const isDeclaredUnderAdultAge = data?.birthdate - ? isUnderAge(data.birthdate, 18) + const isDeclaredUnderAdultAge = metadata?.declaredAge + ? metadata.declaredAge < 18 : true - const isOverRegionMinAccessAge = data?.birthdate - ? !isUnderAge(data.birthdate, config.minAccessAge) + const isOverRegionMinAccessAge = metadata?.declaredAge + ? metadata.declaredAge >= regionConfig.minAccessAge : false - const isOverAppMinAccessAge = data?.birthdate - ? !isUnderAge(data.birthdate, MIN_ACCESS_AGE) + const isOverAppMinAccessAge = metadata?.declaredAge + ? metadata.declaredAge >= MIN_ACCESS_AGE : false const adultContentDisabled = state.access !== AgeAssuranceAccess.Full || isDeclaredUnderAdultAge diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index e986dc923f..0abc0ca6cf 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -24,7 +24,7 @@ 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' @@ -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. 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], )