From e06c95e2f0d7104ec223ce0699ac8351237f7ffc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Dec 2025 12:33:45 -0600 Subject: [PATCH] WIP --- app.config.js | 1 + package.json | 1 + .../components/NoAccessScreen.tsx | 14 ++- src/ageAssurance/data.tsx | 95 +++++++++++++++++++ src/ageAssurance/debug.ts | 13 ++- 5 files changed, 119 insertions(+), 5 deletions(-) diff --git a/app.config.js b/app.config.js index 790ae490eb..e53765ebb2 100644 --- a/app.config.js +++ b/app.config.js @@ -126,6 +126,7 @@ module.exports = function (_config) { 'com.apple.security.application-groups': 'group.app.bsky', 'com.apple.developer.usernotifications.communication': true, // 'com.apple.developer.device-information.user-assigned-device-name': true, + 'com.apple.developer.declared-age-range': true, }, privacyManifests: { NSPrivacyCollectedDataTypes: [ diff --git a/package.json b/package.json index f261749e28..1fc95c0c83 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "emoji-regex": "^10.4.0", "eventemitter3": "^5.0.1", "expo": "54.0.34", + "expo-age-range": "^0.2.0", "expo-application": "~7.0.8", "expo-blur": "~15.0.8", "expo-build-properties": "~1.0.10", diff --git a/src/ageAssurance/components/NoAccessScreen.tsx b/src/ageAssurance/components/NoAccessScreen.tsx index ac88e59f7a..b2c220104b 100644 --- a/src/ageAssurance/components/NoAccessScreen.tsx +++ b/src/ageAssurance/components/NoAccessScreen.tsx @@ -4,6 +4,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' +import * as AgeRange from 'expo-age-range' import { SupportCode, @@ -348,7 +349,18 @@ function AccessSection() { label={_(msg`Verify now`)} size="large" color={hasInitiated ? 'secondary' : 'primary'} - onPress={() => { + onPress={async () => { + try { + const ageRange = await AgeRange.requestAgeRangeAsync({ + threshold1: 10, + threshold2: 13, + threshold3: 18, + }); + console.log(ageRange) + } catch (e) { + console.error(e) + } + return control.open() ax.metric('ageAssurance:initDialogOpen', { hasInitiatedPreviously: hasInitiated, diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index 114d946602..91e2f9f2d6 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -11,6 +11,7 @@ import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persist import {focusManager, QueryClient, useQuery} from '@tanstack/react-query' import {persistQueryClient} from '@tanstack/react-query-persist-client' import debounce from 'lodash.debounce' +import * as AgeRange from 'expo-age-range'; import {networkRetry} from '#/lib/async/retry' import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' @@ -485,6 +486,99 @@ export function useOtherRequiredDataQuery() { ) } +export function createDeviceSignalsQueryKey({did}: {did: string}) { + return ['device-signals', did] +} +export async function getDeviceSignals(): Promise { + if (debug.enabled) return debug.resolve(debug.deviceSignals) + try { + return AgeRange.requestAgeRangeAsync({ + threshold1: 13, + threshold2: 16, + threshold3: 18, + }); + } catch (e: any) { + logger.error(`getDeviceSignals: failed to get device signals`, { + safeMessage: e.message, + }) + return undefined + } +} +export function getDeviceSignalsFromCache({ + did, +}: { + did: string +}): + | AgeRange.AgeRangeResponse + | undefined { + return qc.getQueryData( + createDeviceSignalsQueryKey({did}), + ) +} +let deviceSignalsPrefetchPromise: Promise | undefined +export async function prefetchDeviceSignals({ + agent, +}: { + agent: AtpAgent +}) { + const did = getDidFromAgentSession(agent) + if (!did) return + + /** + * If we don't have a cache, it's possible the user hasn't granted access. + * We don't want to do this during the prefetch phase, so just exit early, + * the user can potentially enable it later. + */ + const cached = getDeviceSignalsFromCache({did}) + if (!cached) return + + if (deviceSignalsPrefetchPromise) { + logger.debug(`prefetchDeviceSignals: already in progress`) + return + } + + deviceSignalsPrefetchPromise = new Promise(async resolve => { + await cacheHydrationPromise + const cached = getDeviceSignalsFromCache({did}) + + if (cached) { + logger.debug(`prefetchDeviceSignals: using cache`) + resolve() + } else { + try { + logger.debug(`prefetchDeviceSignals: resolving...`) + const res = await getDeviceSignals() + qc.setQueryData( + createDeviceSignalsQueryKey({did}), + res, + ) + } catch (e: any) { + logger.warn(`prefetchDeviceSignals: failed`, { + safeMessage: e.message, + }) + } finally { + resolve() + } + } + }) +} +export function useDeviceSignalsQuery() { + const agent = useAgent() + const did = getDidFromAgentSession(agent) + return useQuery( + { + enabled: !!did, + initialData: getDeviceSignalsFromCache({did: did!}), + queryKey: createDeviceSignalsQueryKey({did: did!}), + async queryFn() { + logger.debug(`useDeviceSignalsQuery: fetching device signals`) + return getDeviceSignals() + }, + }, + qc, + ) +} + /** * Helper to prefetch all age assurance data from the server. */ @@ -494,6 +588,7 @@ export function prefetchAgeAssuranceServerData({agent}: {agent: AtpAgent}) { configPrefetchPromise, prefetchServerState({agent}), prefetchOtherRequiredData({agent}), + prefetchDeviceSignals({agent}), ]) } diff --git a/src/ageAssurance/debug.ts b/src/ageAssurance/debug.ts index 3368ddf147..7d09499a69 100644 --- a/src/ageAssurance/debug.ts +++ b/src/ageAssurance/debug.ts @@ -3,12 +3,13 @@ import { type AppBskyAgeassuranceDefs, type AppBskyAgeassuranceGetState, } from '@atproto/api' +import * as AgeRange from 'expo-age-range' import {type OtherRequiredData} from '#/ageAssurance/data' import {IS_DEV, IS_E2E} from '#/env' import {type Geolocation} from '#/geolocation' -export const enabled = (IS_DEV && false) || IS_E2E +export const enabled = (IS_DEV && true) || IS_E2E export const geolocation: Geolocation | undefined = enabled ? { @@ -35,9 +36,9 @@ export const serverState: AppBskyAgeassuranceGetState.OutputSchema | undefined = serverStateEnabled ? { state: { - lastInitiatedAt: new Date(2025, 1, 1).toISOString(), - status: 'assured', - access: 'full', + lastInitiatedAt: undefined, // new Date(2025, 1, 1).toISOString(), + status: 'unknown', + access: 'unknown', }, metadata: { accountCreatedAt: new Date(2023, 1, 1).toISOString(), @@ -257,6 +258,10 @@ export const config: AppBskyAgeassuranceDefs.Config = { ], } +const deviceSignalsEnabled = true +export const deviceSignals: AgeRange.AgeRangeResponse | undefined = + deviceSignalsEnabled ? {} : undefined + export async function resolve(data: T) { await new Promise(y => setTimeout(y, 500)) // simulate network return data