about summary refs log tree commit diff
path: root/src/state/ageAssurance/index.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/ageAssurance/index.tsx')
-rw-r--r--src/state/ageAssurance/index.tsx140
1 files changed, 140 insertions, 0 deletions
diff --git a/src/state/ageAssurance/index.tsx b/src/state/ageAssurance/index.tsx
new file mode 100644
index 000000000..aab954e6c
--- /dev/null
+++ b/src/state/ageAssurance/index.tsx
@@ -0,0 +1,140 @@
+import {createContext, useContext, useMemo} from 'react'
+import {type AppBskyUnspeccedDefs} from '@atproto/api'
+import {useQuery} from '@tanstack/react-query'
+
+import {networkRetry} from '#/lib/async/retry'
+import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications'
+import {useGate} from '#/lib/statsig/statsig'
+import {isNetworkError} from '#/lib/strings/errors'
+import {Logger} from '#/logger'
+import {
+  type AgeAssuranceAPIContextType,
+  type AgeAssuranceContextType,
+} from '#/state/ageAssurance/types'
+import {useIsAgeAssuranceEnabled} from '#/state/ageAssurance/useIsAgeAssuranceEnabled'
+import {useGeolocation} from '#/state/geolocation'
+import {useAgent} from '#/state/session'
+
+const logger = Logger.create(Logger.Context.AgeAssurance)
+
+export const createAgeAssuranceQueryKey = (did: string) =>
+  ['ageAssurance', did] as const
+
+const DEFAULT_AGE_ASSURANCE_STATE: AppBskyUnspeccedDefs.AgeAssuranceState = {
+  lastInitiatedAt: undefined,
+  status: 'unknown',
+}
+
+const AgeAssuranceContext = createContext<AgeAssuranceContextType>({
+  status: 'unknown',
+  isReady: false,
+  lastInitiatedAt: undefined,
+  isAgeRestricted: false,
+})
+
+const AgeAssuranceAPIContext = createContext<AgeAssuranceAPIContextType>({
+  // @ts-ignore can't be bothered to type this
+  refetch: () => Promise.resolve(),
+})
+
+/**
+ * Low-level provider for fetching age assurance state on app load. Do not add
+ * any other data fetching in here to avoid complications and reduced
+ * performance.
+ */
+export function Provider({children}: {children: React.ReactNode}) {
+  const gate = useGate()
+  const agent = useAgent()
+  const {geolocation} = useGeolocation()
+  const isAgeAssuranceEnabled = useIsAgeAssuranceEnabled()
+  const getAndRegisterPushToken = useGetAndRegisterPushToken()
+
+  const {data, isFetched, refetch} = useQuery({
+    /**
+     * This is load bearing. We always want this query to run and end in a
+     * "fetched" state, even if we fall back to defaults. This lets the rest of
+     * the app know that we've at least attempted to load the AA state.
+     *
+     * However, it only needs to run if AA is enabled.
+     */
+    enabled: isAgeAssuranceEnabled,
+    queryKey: createAgeAssuranceQueryKey(agent.session?.did ?? 'never'),
+    async queryFn() {
+      if (!agent.session) return null
+
+      try {
+        const {data} = await networkRetry(3, () =>
+          agent.app.bsky.unspecced.getAgeAssuranceState(),
+        )
+        // const {data} = {
+        //   data: {
+        //     lastInitiatedAt: new Date().toISOString(),
+        //     status: 'pending',
+        //   } as AppBskyUnspeccedDefs.AgeAssuranceState,
+        // }
+
+        logger.debug(`fetch`, {
+          data,
+          account: agent.session?.did,
+        })
+
+        if (gate('age_assurance')) {
+          await getAndRegisterPushToken({
+            isAgeRestricted:
+              !!geolocation?.isAgeRestrictedGeo && data.status !== 'assured',
+          })
+        }
+
+        return data
+      } catch (e) {
+        if (!isNetworkError(e)) {
+          logger.error(`ageAssurance: failed to fetch`, {safeMessage: e})
+        }
+        // don't re-throw error, we'll just fall back to defaults
+        return null
+      }
+    },
+  })
+
+  /**
+   * Derive state, or fall back to defaults
+   */
+  const ageAssuranceContext = useMemo<AgeAssuranceContextType>(() => {
+    const {status, lastInitiatedAt} = data || DEFAULT_AGE_ASSURANCE_STATE
+    const ctx: AgeAssuranceContextType = {
+      isReady: isFetched || !isAgeAssuranceEnabled,
+      status,
+      lastInitiatedAt,
+      isAgeRestricted: isAgeAssuranceEnabled ? status !== 'assured' : false,
+    }
+    logger.debug(`context`, ctx)
+    return ctx
+  }, [isFetched, data, isAgeAssuranceEnabled])
+
+  const ageAssuranceAPIContext = useMemo<AgeAssuranceAPIContextType>(
+    () => ({
+      refetch,
+    }),
+    [refetch],
+  )
+
+  return (
+    <AgeAssuranceAPIContext.Provider value={ageAssuranceAPIContext}>
+      <AgeAssuranceContext.Provider value={ageAssuranceContext}>
+        {children}
+      </AgeAssuranceContext.Provider>
+    </AgeAssuranceAPIContext.Provider>
+  )
+}
+
+/**
+ * Access to low-level AA state. Prefer using {@link useAgeInfo} for a
+ * more user-friendly interface.
+ */
+export function useAgeAssuranceContext() {
+  return useContext(AgeAssuranceContext)
+}
+
+export function useAgeAssuranceAPIContext() {
+  return useContext(AgeAssuranceAPIContext)
+}