diff --git a/package.json b/package.json
index 7d201e90a5..92b0374720 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
- "@atproto/api": "^0.18.8",
+ "@atproto/api": "^0.18.11",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.6",
diff --git a/src/ageAssurance/components/NoAccessScreen.tsx b/src/ageAssurance/components/NoAccessScreen.tsx
index 3aa69dc789..8b8ad0ccc1 100644
--- a/src/ageAssurance/components/NoAccessScreen.tsx
+++ b/src/ageAssurance/components/NoAccessScreen.tsx
@@ -177,10 +177,19 @@ export function NoAccessScreen() {
+ {!aa.flags.isOverRegionMinAccessAge && (
+
+
+ Unfortunately, your declared age indicates that you
+ are not old enough to access Bluesky in your region.
+
+
+ )}
+
{!isBlocked && birthdateUpdateText}
-
+ {aa.flags.isOverRegionMinAccessAge && }
>
) : (
diff --git a/src/ageAssurance/debug.ts b/src/ageAssurance/debug.ts
index 20ed6174eb..8a97cf84ee 100644
--- a/src/ageAssurance/debug.ts
+++ b/src/ageAssurance/debug.ts
@@ -12,23 +12,26 @@ export const enabled = (IS_DEV && false) || IS_E2E
export const geolocation: Geolocation | undefined = enabled
? {
- countryCode: 'AA',
+ countryCode: 'BB',
regionCode: undefined,
}
: undefined
-export const deviceGeolocation: Geolocation | undefined = enabled
- ? {
- countryCode: 'AA',
- regionCode: undefined,
- }
- : undefined
+const deviceGeolocationEnabled = false
+export const deviceGeolocation: Geolocation | undefined =
+ enabled && deviceGeolocationEnabled
+ ? {
+ countryCode: 'AA',
+ regionCode: undefined,
+ }
+ : undefined
export const config: AppBskyAgeassuranceDefs.Config = {
regions: [
{
countryCode: 'AA',
regionCode: undefined,
+ minAccessAge: 13,
rules: [
{
$type: ids.Default,
@@ -36,6 +39,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
},
],
},
+ {
+ countryCode: 'BB',
+ regionCode: undefined,
+ minAccessAge: 16,
+ rules: [
+ {
+ $type: ids.Default,
+ access: 'none',
+ },
+ ],
+ },
],
}
diff --git a/src/ageAssurance/index.tsx b/src/ageAssurance/index.tsx
index 9a0a9c9d51..68654b384b 100644
--- a/src/ageAssurance/index.tsx
+++ b/src/ageAssurance/index.tsx
@@ -14,7 +14,11 @@ import {
type AgeAssuranceState,
AgeAssuranceStatus,
} from '#/ageAssurance/types'
-import {isUserUnderAdultAge} from '#/ageAssurance/util'
+import {
+ isUnderAge,
+ MIN_ACCESS_AGE,
+ useAgeAssuranceRegionConfigWithFallback,
+} from '#/ageAssurance/util'
export {
prefetchConfig as prefetchAgeAssuranceConfig,
@@ -24,6 +28,7 @@ export {
usePatchServerState as usePatchAgeAssuranceServerState,
} from '#/ageAssurance/data'
export {logger} from '#/ageAssurance/logger'
+export {MIN_ACCESS_AGE} from '#/ageAssurance/util'
const AgeAssuranceStateContext = createContext<{
Access: typeof AgeAssuranceAccess
@@ -32,6 +37,8 @@ const AgeAssuranceStateContext = createContext<{
flags: {
adultContentDisabled: boolean
chatDisabled: boolean
+ isOverRegionMinAccessAge: boolean
+ isOverAppMinAccessAge: boolean
}
}>({
Access: AgeAssuranceAccess,
@@ -44,6 +51,8 @@ const AgeAssuranceStateContext = createContext<{
flags: {
adultContentDisabled: false,
chatDisabled: false,
+ isOverRegionMinAccessAge: false,
+ isOverAppMinAccessAge: false,
},
})
@@ -69,6 +78,7 @@ export function Provider({children}: {children: React.ReactNode}) {
function InnerProvider({children}: {children: React.ReactNode}) {
const state = useAgeAssuranceState()
const {data} = useAgeAssuranceDataContext()
+ const config = useAgeAssuranceRegionConfigWithFallback()
const getAndRegisterPushToken = useGetAndRegisterPushToken()
const handleAccessUpdate = useCallback(
@@ -89,11 +99,17 @@ function InnerProvider({children}: {children: React.ReactNode}) {
{
const chatDisabled = state.access !== AgeAssuranceAccess.Full
- const isUnderage = data?.birthdate
- ? isUserUnderAdultAge(data.birthdate)
+ const isUnderAdultAge = data?.birthdate
+ ? isUnderAge(data.birthdate, 18)
: true
+ const isOverRegionMinAccessAge = data?.birthdate
+ ? !isUnderAge(data.birthdate, config.minAccessAge)
+ : false
+ const isOverAppMinAccessAge = data?.birthdate
+ ? !isUnderAge(data.birthdate, MIN_ACCESS_AGE)
+ : false
const adultContentDisabled =
- state.access !== AgeAssuranceAccess.Full || isUnderage
+ state.access !== AgeAssuranceAccess.Full || isUnderAdultAge
return {
Access: AgeAssuranceAccess,
Status: AgeAssuranceStatus,
@@ -101,9 +117,11 @@ function InnerProvider({children}: {children: React.ReactNode}) {
flags: {
adultContentDisabled,
chatDisabled,
+ isOverRegionMinAccessAge,
+ isOverAppMinAccessAge,
},
}
- }, [state, data])}>
+ }, [state, data, config])}>
{children}
)
diff --git a/src/ageAssurance/util.ts b/src/ageAssurance/util.ts
index e31f89b14e..d55ec61762 100644
--- a/src/ageAssurance/util.ts
+++ b/src/ageAssurance/util.ts
@@ -12,7 +12,23 @@ import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
import {AgeAssuranceAccess} from '#/ageAssurance/types'
import {type Geolocation, useGeolocation} from '#/geolocation'
-const DEFAULT_MIN_AGE = 13
+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
@@ -30,23 +46,7 @@ export function getAgeAssuranceRegionConfigWithFallback(
regionCode: geolocation.regionCode,
})
- return (
- region || {
- countryCode: '*',
- regionCode: undefined,
- rules: [
- {
- $type: ids.IfDeclaredOverAge,
- age: DEFAULT_MIN_AGE,
- access: AgeAssuranceAccess.Full,
- },
- {
- $type: ids.Default,
- access: AgeAssuranceAccess.None,
- },
- ],
- }
- )
+ return region || FALLBACK_REGION_CONFIG
}
/**
@@ -67,6 +67,14 @@ export function useAgeAssuranceRegionConfig() {
}, [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
@@ -78,15 +86,11 @@ export function isLegacyBirthdateBug(birthDate: string) {
}
/**
- * Returns whether the user is under the minimum age required to use the app.
- * This applies to all regions.
+ * Returns whether the date (converted to an age as a whole integer) is under
+ * the provided minimum age.
*/
-export function isUserUnderMinimumAge(birthDate: string) {
- return getAge(new Date(birthDate)) < DEFAULT_MIN_AGE
-}
-
-export function isUserUnderAdultAge(birthDate: string) {
- return getAge(new Date(birthDate)) < 18
+export function isUnderAge(birthDate: string, age: number) {
+ return getAge(new Date(birthDate)) < age
}
export function getBirthdateStringFromAge(age: number) {
diff --git a/src/geolocation/debug.ts b/src/geolocation/debug.ts
index f062a94cef..0d2564c305 100644
--- a/src/geolocation/debug.ts
+++ b/src/geolocation/debug.ts
@@ -5,14 +5,20 @@ import {type Geolocation} from '#/geolocation/types'
const localEnabled = false
export const enabled = IS_DEV && (localEnabled || aaDebug.geolocation)
export const geolocation: Geolocation = aaDebug.geolocation ?? {
- countryCode: 'AU',
- regionCode: undefined,
-}
-export const deviceGeolocation: Geolocation = aaDebug.deviceGeolocation ?? {
- countryCode: 'AU',
- regionCode: undefined,
+ countryCode: 'US',
+ regionCode: 'TX',
}
+const deviceLocalEnabled = false
+export const deviceGeolocation: Geolocation | undefined =
+ aaDebug.deviceGeolocation ||
+ (deviceLocalEnabled
+ ? {
+ countryCode: 'US',
+ regionCode: 'TX',
+ }
+ : undefined)
+
export async function resolve(data: T) {
await new Promise(y => setTimeout(y, 500)) // simulate network
return data
diff --git a/src/geolocation/device.ts b/src/geolocation/device.ts
index d14d4eb677..b98e6a2363 100644
--- a/src/geolocation/device.ts
+++ b/src/geolocation/device.ts
@@ -46,7 +46,8 @@ const useForegroundPermissions = createPermissionHook({
})
export async function getDeviceGeolocation(): Promise {
- if (debug.enabled) return debug.resolve(debug.deviceGeolocation)
+ if (debug.enabled && debug.deviceGeolocation)
+ return debug.resolve(debug.deviceGeolocation)
try {
const geocode = await Location.getCurrentPositionAsync()
@@ -142,3 +143,8 @@ export function useSyncDeviceGeolocationOnStartup(
})
}, [status, sync])
}
+
+export function useIsDeviceGeolocationGranted() {
+ const [status] = useForegroundPermissions()
+ return status?.granted === true
+}
diff --git a/src/geolocation/index.tsx b/src/geolocation/index.tsx
index 231182a579..c9dc0cb1ed 100644
--- a/src/geolocation/index.tsx
+++ b/src/geolocation/index.tsx
@@ -12,7 +12,10 @@ import {type Geolocation} from '#/geolocation/types'
import {mergeGeolocations} from '#/geolocation/util'
import {device, useStorage} from '#/storage'
-export {useRequestDeviceGeolocation} from '#/geolocation/device'
+export {
+ useIsDeviceGeolocationGranted,
+ useRequestDeviceGeolocation,
+} from '#/geolocation/device'
export {resolve} from '#/geolocation/service'
export * from '#/geolocation/types'
diff --git a/src/screens/Signup/StepInfo/Policies.tsx b/src/screens/Signup/StepInfo/Policies.tsx
index 2c609cfc4d..565c2d4cd1 100644
--- a/src/screens/Signup/StepInfo/Policies.tsx
+++ b/src/screens/Signup/StepInfo/Policies.tsx
@@ -11,12 +11,8 @@ import {Text} from '#/components/Typography'
export const Policies = ({
serviceDescription,
- needsGuardian,
- under13,
}: {
serviceDescription: ComAtprotoServerDescribeServer.OutputSchema
- needsGuardian: boolean
- under13: boolean
}) => {
const t = useTheme()
const {_} = useLingui()
@@ -91,30 +87,9 @@ export const Policies = ({
return null
}
- return (
-
- {els ? (
-
- {els}
-
- ) : null}
-
- {under13 ? (
-
-
- You must be 13 years of age or older to create an account.
-
-
- ) : needsGuardian ? (
-
-
- If you are not yet an adult according to the laws of your country,
- your parent or legal guardian must read these Terms on your behalf.
-
-
- ) : undefined}
-
- )
+ return els ? (
+ {els}
+ ) : null
}
function validWebLink(url?: string): string | undefined {
diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx
index e3664c0194..4beced1852 100644
--- a/src/screens/Signup/StepInfo/index.tsx
+++ b/src/screens/Signup/StepInfo/index.tsx
@@ -7,9 +7,13 @@ import type tldts from 'tldts'
import {isEmailMaybeInvalid} from '#/lib/strings/email'
import {logger} from '#/logger'
-import {is13, is18, useSignupContext} from '#/screens/Signup/state'
+import {isNative} from '#/platform/detection'
+import {useSignupContext} from '#/screens/Signup/state'
import {Policies} from '#/screens/Signup/StepInfo/Policies'
import {atoms as a, native} from '#/alf'
+import * as Admonition from '#/components/Admonition'
+import * as Dialog from '#/components/Dialog'
+import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog'
import * as DateField from '#/components/forms/DateField'
import {type DateFieldRef} from '#/components/forms/DateField/types'
import {FormError} from '#/components/forms/FormError'
@@ -18,8 +22,19 @@ import * as TextField from '#/components/forms/TextField'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
+import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {usePreemptivelyCompleteActivePolicyUpdate} from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate'
+import * as Toast from '#/components/Toast'
+import {
+ isUnderAge,
+ MIN_ACCESS_AGE,
+ useAgeAssuranceRegionConfigWithFallback,
+} from '#/ageAssurance/util'
+import {
+ useDeviceGeolocationApi,
+ useIsDeviceGeolocationGranted,
+} from '#/geolocation'
import {BackNextButtons} from '../BackNextButtons'
function sanitizeDate(date: Date): Date {
@@ -57,6 +72,20 @@ export function StepInfo({
const passwordInputRef = useRef(null)
const birthdateInputRef = useRef(null)
+ const aaRegionConfig = useAgeAssuranceRegionConfigWithFallback()
+ const {setDeviceGeolocation} = useDeviceGeolocationApi()
+ const locationControl = Dialog.useDialogControl()
+ const isOverRegionMinAccessAge = state.dateOfBirth
+ ? !isUnderAge(state.dateOfBirth.toISOString(), aaRegionConfig.minAccessAge)
+ : true
+ const isOverAppMinAccessAge = state.dateOfBirth
+ ? !isUnderAge(state.dateOfBirth.toISOString(), MIN_ACCESS_AGE)
+ : true
+ const isOverMinAdultAge = state.dateOfBirth
+ ? !isUnderAge(state.dateOfBirth.toISOString(), 18)
+ : true
+ const isDeviceGeolocationGranted = useIsDeviceGeolocationGranted()
+
const [hasWarnedEmail, setHasWarnedEmail] = React.useState(false)
const tldtsRef = React.useRef(undefined)
@@ -76,7 +105,7 @@ export function StepInfo({
const emailChanged = prevEmailValueRef.current !== email
const password = passwordValueRef.current
- if (!is13(state.dateOfBirth)) {
+ if (!isOverRegionMinAccessAge) {
return
}
@@ -274,16 +303,79 @@ export function StepInfo({
maximumDate={new Date()}
/>
-
+
+
+
+
+ {!isOverRegionMinAccessAge || !isOverAppMinAccessAge ? (
+
+
+
+
+
+ {!isOverAppMinAccessAge ? (
+
+ You must be {MIN_ACCESS_AGE} years of age or older
+ to create an account.
+
+ ) : (
+
+ You must be {aaRegionConfig.minAccessAge} years of
+ age or older to create an account in your region.
+
+ )}
+
+ {isNative &&
+ !isDeviceGeolocationGranted &&
+ isOverAppMinAccessAge && (
+
+
+ Have we got your location wrong?{' '}
+ {
+ locationControl.open()
+ })}>
+ Tap here to confirm your location with GPS.
+
+
+
+ )}
+
+
+
+ ) : !isOverMinAdultAge ? (
+
+
+ If you are not yet an adult according to the laws of your
+ country, your parent or legal guardian must read these Terms
+ on your behalf.
+
+
+ ) : undefined}
+
+
+ {isNative && (
+ {
+ props.closeDialog(() => {
+ // set this after close!
+ setDeviceGeolocation(props.geolocation)
+ Toast.show(_(msg`Your location has been updated.`), {
+ type: 'success',
+ })
+ })
+ }}
+ />
+ )}
>
) : undefined}