From 6603af633318f12e86e75d2200aaacb07f50c8cf Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 2 Jul 2026 10:40:52 -0500 Subject: [PATCH] Share logic in account card --- .../ageAssurance/AgeAssuranceAccountCard.tsx | 153 +++++++++++++++--- 1 file changed, 135 insertions(+), 18 deletions(-) diff --git a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx index e5a4b5eaf2..f54fdc371f 100644 --- a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx +++ b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx @@ -1,7 +1,10 @@ +import {useCallback, useState} from 'react' import {View} from 'react-native' +import type * as AgeRange from 'expo-age-range' import {Trans, useLingui} from '@lingui/react/macro' import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' +import {useSession} from '#/state/session' import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' import {Admonition} from '#/components/Admonition' import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' @@ -12,16 +15,25 @@ import { useDialogControl, } from '#/components/ageAssurance/AgeAssuranceInitDialog' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' -import {Button, ButtonText} from '#/components/Button' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog' import {Divider} from '#/components/Divider' +import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield' import {createStaticClick, InlineLinkText} from '#/components/Link' +import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAgeAssurance} from '#/ageAssurance' +import {getDeviceSignals, setDeviceSignalsForRegion} from '#/ageAssurance/data' +import {logger} from '#/ageAssurance/logger' +import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state' import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' -import {createGeolocationString} from '#/ageAssurance/util' +import { + createGeolocationString, + getAgeAssuranceDataFromDeviceSignals, + useAgeAssuranceRegionConfig, +} from '#/ageAssurance/util' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' import {useDeviceGeolocationApi, useGeolocation} from '#/geolocation' @@ -47,6 +59,8 @@ function Inner({style}: ViewStyleProp & {}) { const appealControl = Dialog.useDialogControl() const getTimeAgo = useGetTimeAgo() const {gtPhone} = useBreakpoints() + const {currentAccount} = useSession() + const region = useAgeAssuranceRegionConfig() const copy = useAgeAssuranceCopy() const aa = useAgeAssurance() @@ -60,6 +74,101 @@ function Inner({style}: ViewStyleProp & {}) { const diff = lastInitiatedAt ? dateDiff(lastInitiatedAt, new Date(), 'down') : null + const allowsDeviceVerification = region && aa.flags.allowsDeviceVerification + const verifyCta = allowsDeviceVerification + ? l`Share age data` + : hasInitiated + ? l`Verify again` + : l`Verify now` + + const [isVerifyingDevice, setIsVerifyingDevice] = useState(false) + + const openKwsDialog = useCallback(() => { + control.open() + ax.metric('ageAssurance:initDialogOpen', { + hasInitiatedPreviously: hasInitiated, + }) + }, [control, ax, hasInitiated]) + + const onPressVerify = useCallback(async () => { + const did = currentAccount?.did + + // Just for typescript, this card won't be shown without a logged in user + if (!did) return + + /* + * In regions that permit on-device verification, try the native age API + * first. We tag the result with the current region (device assurance is + * region-bound — a TX grant only counts in TX) and, if it's sufficient, + * persist it client-side so the AA state recompute lifts the gate. + * + * Once the OS returns a response we stay on the device path and report the + * outcome via a toast (sufficient, under-age, or no usable data) rather than + * silently falling back — users can still opt into KWS via the inline link. + * We only fall through to the KWS dialog below when the device can't give us + * a response at all: `getDeviceSignals` handles its own errors and returns + * undefined (e.g. on web or failure). + */ + if (allowsDeviceVerification) { + // Show a loading state while the OS age prompt is up. + setIsVerifyingDevice(true) + let signals: AgeRange.AgeRangeResponse | undefined + try { + signals = await getDeviceSignals() + } finally { + setIsVerifyingDevice(false) + } + if (signals) { + const {assuredAge} = getAgeAssuranceDataFromDeviceSignals( + region, + signals, + ) + if (assuredAge !== undefined) { + // Persist (keyed by this region) so the AA state recomputes from the + // cache write. Recompute here too so we can react to the outcome: a + // sufficient age lifts the gate (nothing more to do), but the device + // may report an age below the region's threshold, in which case + // access stays `none` and we tell the user. + setDeviceSignalsForRegion({did, region, signals}) + const {state} = unsafeGetAndComputeAgeAssurance({did}) + if (state.access === aa.Access.None) { + Toast.show( + l`We're sorry, but based on the data shared by your device, you are not old enough to access Bluesky.`, + {type: 'info'}, + ) + } else if (state.access === aa.Access.Unknown) { + Toast.show( + l`Hmm, it seems we weren't able to compute your level of access. Please try again.`, + {type: 'warning'}, + ) + } else { + Toast.show(l`Thanks! You're all set.`, { + type: 'success', + }) + } + return + } + // We got a device response but it carried no usable age information. + Toast.show( + l`Hmm, it seems your device was unable to share age information with us.`, + {type: 'warning'}, + ) + return + } + logger.debug( + `onPressVerify: no device signals available (web/error), falling back to KWS`, + ) + } + + openKwsDialog() + }, [ + region, + currentAccount?.did, + openKwsDialog, + allowsDeviceVerification, + aa, + l, + ]) return ( <> @@ -138,26 +247,34 @@ function Inner({style}: ViewStyleProp & {}) { : [a.gap_md], ]}> - {lastInitiatedAt && timeAgo && diff ? ( + {allowsDeviceVerification ? ( + + + Sharing your age data uses information stored on your + device, and will therefore only work on this device. + Alternatively,{' '} + { + openKwsDialog() + })}> + you can use our trusted partner, KWS + + , to complete your verification and enable access on all + platforms. + + + ) : lastInitiatedAt && timeAgo && diff ? (