Provide geo-gated users optional GPS fallback for precise location data (#8973)
This commit is contained in:
@@ -6,16 +6,27 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useDeviceGeolocationApi} from '#/state/geolocation'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
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 {Full as Logo, Mark} from '#/components/icons/Logo'
|
||||
import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation'
|
||||
import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link'
|
||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {BottomSheetOutlet} from '#/../modules/bottom-sheet'
|
||||
|
||||
export function BlockedGeoOverlay() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const insets = useSafeAreaInsets()
|
||||
const geoDialog = Dialog.useDialogControl()
|
||||
const {setDeviceGeolocation} = useDeviceGeolocationApi()
|
||||
|
||||
useEffect(() => {
|
||||
// just counting overall hits here
|
||||
@@ -51,59 +62,133 @@ export function BlockedGeoOverlay() {
|
||||
]
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
a.px_2xl,
|
||||
{
|
||||
paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.mx_auto,
|
||||
web({
|
||||
maxWidth: 440,
|
||||
paddingTop: gtPhone ? '8vh' : undefined,
|
||||
}),
|
||||
<>
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
a.px_2xl,
|
||||
{
|
||||
paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
]}>
|
||||
<View style={[a.align_start]}>
|
||||
<View
|
||||
style={[
|
||||
a.pl_md,
|
||||
a.pr_lg,
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
{
|
||||
backgroundColor: t.palette.primary_25,
|
||||
},
|
||||
]}>
|
||||
<Mark fill={t.palette.primary_600} width={14} />
|
||||
<Text
|
||||
<View
|
||||
style={[
|
||||
a.mx_auto,
|
||||
web({
|
||||
maxWidth: 380,
|
||||
paddingTop: gtPhone ? '8vh' : undefined,
|
||||
}),
|
||||
]}>
|
||||
<View style={[a.align_start]}>
|
||||
<View
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.pl_md,
|
||||
a.pr_lg,
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
{
|
||||
color: t.palette.primary_600,
|
||||
backgroundColor: t.palette.primary_25,
|
||||
},
|
||||
]}>
|
||||
<Trans>Announcement</Trans>
|
||||
</Text>
|
||||
<Mark fill={t.palette.primary_600} width={14} />
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
{
|
||||
color: t.palette.primary_600,
|
||||
},
|
||||
]}>
|
||||
<Trans>Announcement</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[a.gap_lg, {paddingTop: 32}]}>
|
||||
{blocks.map((block, index) => (
|
||||
<Text key={index} style={[textStyles]}>
|
||||
{block}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{!isWeb && (
|
||||
<>
|
||||
<View style={[a.pt_2xl]}>
|
||||
<Divider />
|
||||
</View>
|
||||
|
||||
<View style={[a.mt_xl, a.align_start]}>
|
||||
<Text
|
||||
style={[a.text_lg, a.font_heavy, a.leading_snug, a.pb_xs]}>
|
||||
<Trans>Not in Mississippi?</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.pb_md,
|
||||
]}>
|
||||
<Trans>
|
||||
Confirm your location with GPS. Your location data is not
|
||||
tracked and does not leave your device.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Confirm your location`)}
|
||||
onPress={() => geoDialog.open()}
|
||||
size="small"
|
||||
color="primary_subtle">
|
||||
<ButtonIcon icon={LocationIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Confirm your location</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<DeviceLocationRequestDialog
|
||||
control={geoDialog}
|
||||
onLocationAcquired={props => {
|
||||
if (props.geolocationStatus.isAgeBlockedGeo) {
|
||||
props.disableDialogAction()
|
||||
props.setDialogError(
|
||||
_(
|
||||
msg`We're sorry, but based on your device's location, you are currently located in a region we cannot provide access at this time.`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
props.closeDialog(() => {
|
||||
// set this after close!
|
||||
setDeviceGeolocation({
|
||||
countryCode: props.geolocationStatus.countryCode,
|
||||
regionCode: props.geolocationStatus.regionCode,
|
||||
})
|
||||
Toast.show(_(msg`Thanks! You're all set.`), {
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={[{paddingTop: 48}]}>
|
||||
<Logo width={120} textFill={t.atoms.text.color} />
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={[a.gap_lg, {paddingTop: 32, paddingBottom: 48}]}>
|
||||
{blocks.map((block, index) => (
|
||||
<Text key={index} style={[textStyles]}>
|
||||
{block}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Logo width={120} textFill={t.atoms.text.color} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
{/*
|
||||
* While this blocking overlay is up, other dialogs in the shell
|
||||
* are not mounted, so it _should_ be safe to use these here
|
||||
* without fear of other modals showing up.
|
||||
*/}
|
||||
<BottomSheetOutlet />
|
||||
<PortalOutlet />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||
import {logger} from '#/state/ageAssurance/util'
|
||||
import {useDeviceGeolocationApi} from '#/state/geolocation'
|
||||
import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
|
||||
@@ -16,8 +18,10 @@ import {
|
||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) {
|
||||
@@ -35,8 +39,10 @@ function Inner({style}: ViewStyleProp & {}) {
|
||||
const {_, i18n} = useLingui()
|
||||
const control = useDialogControl()
|
||||
const appealControl = Dialog.useDialogControl()
|
||||
const locationControl = Dialog.useDialogControl()
|
||||
const getTimeAgo = useGetTimeAgo()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const {setDeviceGeolocation} = useDeviceGeolocationApi()
|
||||
|
||||
const copy = useAgeAssuranceCopy()
|
||||
const {status, lastInitiatedAt} = useAgeAssurance()
|
||||
@@ -71,8 +77,50 @@ function Inner({style}: ViewStyleProp & {}) {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[a.pb_md]}>
|
||||
<View style={[a.pb_md, a.gap_xs]}>
|
||||
<Text style={[a.text_sm, a.leading_snug]}>{copy.notice}</Text>
|
||||
|
||||
{isNative && (
|
||||
<>
|
||||
<Text style={[a.text_sm, a.leading_snug]}>
|
||||
<Trans>
|
||||
Is your location not accurate?{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Confirm your location`)}
|
||||
{...createStaticClick(() => {
|
||||
locationControl.open()
|
||||
})}>
|
||||
Click here to confirm your location.
|
||||
</InlineLinkText>{' '}
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<DeviceLocationRequestDialog
|
||||
control={locationControl}
|
||||
onLocationAcquired={props => {
|
||||
if (props.geolocationStatus.isAgeRestrictedGeo) {
|
||||
props.disableDialogAction()
|
||||
props.setDialogError(
|
||||
_(
|
||||
msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
props.closeDialog(() => {
|
||||
// set this after close!
|
||||
setDeviceGeolocation({
|
||||
countryCode: props.geolocationStatus.countryCode,
|
||||
regionCode: props.geolocationStatus.regionCode,
|
||||
})
|
||||
Toast.show(_(msg`Thanks! You're all set.`), {
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{isBlocked ? (
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {isNetworkError, useCleanError} from '#/lib/hooks/useCleanError'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {
|
||||
computeGeolocationStatus,
|
||||
type GeolocationStatus,
|
||||
useGeolocationConfig,
|
||||
} from '#/state/geolocation'
|
||||
import {useRequestDeviceLocation} from '#/state/geolocation/useRequestDeviceLocation'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export type Props = {
|
||||
onLocationAcquired?: (props: {
|
||||
geolocationStatus: GeolocationStatus
|
||||
setDialogError: (error: string) => void
|
||||
disableDialogAction: () => void
|
||||
closeDialog: (callback?: () => void) => void
|
||||
}) => void
|
||||
}
|
||||
|
||||
export function DeviceLocationRequestDialog({
|
||||
control,
|
||||
onLocationAcquired,
|
||||
}: Props & {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Confirm your location`)}
|
||||
style={[web({maxWidth: 380})]}>
|
||||
<DeviceLocationRequestDialogInner
|
||||
onLocationAcquired={onLocationAcquired}
|
||||
/>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function DeviceLocationRequestDialogInner({onLocationAcquired}: Props) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {close} = Dialog.useDialogContext()
|
||||
const requestDeviceLocation = useRequestDeviceLocation()
|
||||
const {config} = useGeolocationConfig()
|
||||
const cleanError = useCleanError()
|
||||
|
||||
const [isRequesting, setIsRequesting] = useState(false)
|
||||
const [error, setError] = useState<string>('')
|
||||
const [dialogDisabled, setDialogDisabled] = useState(false)
|
||||
|
||||
const onPressConfirm = async () => {
|
||||
setError('')
|
||||
setIsRequesting(true)
|
||||
|
||||
try {
|
||||
const req = await wait(1e3, requestDeviceLocation())
|
||||
|
||||
if (req.granted) {
|
||||
const location = req.location
|
||||
|
||||
if (location && location.countryCode) {
|
||||
const geolocationStatus = computeGeolocationStatus(location, config)
|
||||
onLocationAcquired?.({
|
||||
geolocationStatus,
|
||||
setDialogError: setError,
|
||||
disableDialogAction: () => setDialogDisabled(true),
|
||||
closeDialog: close,
|
||||
})
|
||||
} else {
|
||||
setError(_(msg`Failed to resolve location. Please try again.`))
|
||||
}
|
||||
} else {
|
||||
setError(
|
||||
_(
|
||||
msg`Unable to access location. You'll need to visit your system settings to enable location services for Bluesky`,
|
||||
),
|
||||
)
|
||||
}
|
||||
} catch (e: any) {
|
||||
const {clean, raw} = cleanError(e)
|
||||
setError(clean || raw || e.message)
|
||||
if (!isNetworkError(e)) {
|
||||
logger.error(`blockedGeoOverlay: unexpected error`, {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setIsRequesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.gap_md]}>
|
||||
<Text style={[a.text_xl, a.font_heavy]}>
|
||||
<Trans>Confirm your location</Trans>
|
||||
</Text>
|
||||
<View style={[a.gap_sm, a.pb_xs]}>
|
||||
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Click below to allow Bluesky to access your GPS location. We will
|
||||
then use that data to more accurately determine the content and
|
||||
features available in your region.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.pb_xs,
|
||||
]}>
|
||||
<Trans>
|
||||
Your location data is not tracked and does not leave your device.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{error && (
|
||||
<View style={[a.pb_xs]}>
|
||||
<Admonition type="error">{error}</Admonition>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[a.gap_sm]}>
|
||||
{!dialogDisabled && (
|
||||
<Button
|
||||
disabled={isRequesting}
|
||||
label={_(msg`Confirm your location`)}
|
||||
onPress={onPressConfirm}
|
||||
size={isWeb ? 'small' : 'large'}
|
||||
color="primary">
|
||||
<ButtonIcon icon={isRequesting ? Loader : LocationIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Allow location access</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isWeb && (
|
||||
<Button
|
||||
label={_(msg`Confirm your location`)}
|
||||
onPress={() => close()}
|
||||
size={isWeb ? 'small' : 'large'}
|
||||
color="secondary">
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const PinLocation_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 2a8 8 0 0 1 8 8c0 3.305-1.953 6.29-3.745 8.355a25.964 25.964 0 0 1-3.333 3.197c-.101.08-.181.142-.237.184l-.067.05-.018.014-.005.004-.002.002h-.001c-.003-.004-.042-.055-.592-.806l.592.807a1.001 1.001 0 0 1-1.184 0v-.001l-.003-.002-.005-.004-.018-.014-.067-.05a23.449 23.449 0 0 1-1.066-.877 25.973 25.973 0 0 1-2.504-2.503C5.953 16.29 4 13.305 4 10a8 8 0 0 1 8-8Zm0 2a6 6 0 0 0-6 6c0 2.56 1.547 5.076 3.255 7.044A23.978 23.978 0 0 0 12 19.723a23.976 23.976 0 0 0 2.745-2.679C16.453 15.076 18 12.56 18 10a6 6 0 0 0-6-6Zm-.002 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z',
|
||||
})
|
||||
|
||||
export const PinLocationFilled_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12.591 21.806h.002l.001-.002.006-.004.018-.014a10.028 10.028 0 0 0 .304-.235 25.952 25.952 0 0 0 3.333-3.196C18.048 16.29 20 13.305 20 10a8 8 0 1 0-16 0c0 3.305 1.952 6.29 3.745 8.355a25.955 25.955 0 0 0 3.333 3.196 15.733 15.733 0 0 0 .304.235l.018.014.006.004.002.002a1 1 0 0 0 1.183 0Zm-.593-9.306a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z',
|
||||
})
|
||||
Reference in New Issue
Block a user