import {useMemo} from 'react' import { ageAssuranceRuleIDs as ids, type AppBskyAgeassuranceDefs, getAgeAssuranceRegionConfig, type ModerationPrefs, } from '@atproto/api' import {getAge} from '#/lib/strings/time' import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' import {useAgeAssuranceDataContext} from '#/ageAssurance/data' import {AgeAssuranceAccess} from '#/ageAssurance/types' import {type Geolocation, useGeolocation} from '#/geolocation' export const MIN_ACCESS_AGE = 13 const FALLBACK_REGION_CONFIG: AppBskyAgeassuranceDefs.ConfigRegion = { countryCode: '*', regionCode: undefined, minAccessAge: MIN_ACCESS_AGE, rules: [ { $type: ids.IfDeclaredOverAge, age: MIN_ACCESS_AGE, access: AgeAssuranceAccess.Full, }, { $type: ids.Default, access: AgeAssuranceAccess.None, }, ], } /** * Get age assurance region config based on geolocation, with fallback to * app defaults if no region config is found. * * See {@link getAgeAssuranceRegionConfig} for the generic option, which can * return undefined if the geolocation does not match any AA region. */ export function getAgeAssuranceRegionConfigWithFallback( config: AppBskyAgeassuranceDefs.Config, geolocation: Geolocation, ): AppBskyAgeassuranceDefs.ConfigRegion { const region = getAgeAssuranceRegionConfig(config, { countryCode: geolocation.countryCode ?? '', regionCode: geolocation.regionCode, }) return region || FALLBACK_REGION_CONFIG } /** * Hook to get the age assurance region config based on current geolocation. * Does not fall-back to our app defaults. If no config is found, returns * undefined, which indicates no regional age assurance rules apply. */ export function useAgeAssuranceRegionConfig() { const geolocation = useGeolocation() const {config} = useAgeAssuranceDataContext() return useMemo(() => { if (!config) return // use generic helper, we want to potentially return undefined return getAgeAssuranceRegionConfig(config, { countryCode: geolocation.countryCode ?? '', regionCode: geolocation.regionCode, }) }, [config, geolocation]) } /** * Hook to get the age assurance region config based on current geolocation. * Falls back to our app defaults if no region config is found. */ export function useAgeAssuranceRegionConfigWithFallback() { return useAgeAssuranceRegionConfig() || FALLBACK_REGION_CONFIG } /** * Some users may have erroneously set their birth date to the current date * if one wasn't set on their account. We previously didn't do validation on * the bday dialog, and it defaulted to the current date. This bug _has_ been * seen in production, so we need to check for it where possible. */ export function isLegacyBirthdateBug(birthDate: string) { return ['2025', '2024', '2023'].includes((birthDate || '').slice(0, 4)) } /** * Returns whether the date (converted to an age as a whole integer) is under * the provided minimum age. */ export function isUnderAge(birthDate: string, age: number) { return getAge(new Date(birthDate)) < age } export function getBirthdateStringFromAge(age: number) { const today = new Date() return new Date( today.getFullYear() - age, today.getMonth(), today.getDate() - 1, // set to day before to ensure age is reached ).toISOString() } export const makeAgeRestrictedModerationPrefs = ( prefs: ModerationPrefs, ): ModerationPrefs => ({ ...prefs, adultContentEnabled: false, labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, })