From 912ab1bd9b771cf14c830203332f3620e661a752 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 22 Aug 2025 14:55:16 -0500 Subject: [LEG-246] Geo overlay (#8881) * Add AgeBlockedGeo * Add MaxMind usage text * Add geo overlay --------- Co-authored-by: rafael --- bskyweb/cmd/bskyweb/server.go | 4 +- src/components/BlockedGeoOverlay.tsx | 109 +++++++++++++++++++++++++++++++++++ src/components/Link.tsx | 88 +++++++++++++++++++++++++++- src/components/icons/Logo.tsx | 37 ++++++++++++ src/logger/metrics.ts | 5 ++ src/state/geolocation.tsx | 2 + src/storage/schema.ts | 1 + src/view/shell/index.tsx | 18 ++++-- src/view/shell/index.web.tsx | 27 +++++---- 9 files changed, 269 insertions(+), 22 deletions(-) create mode 100644 src/components/BlockedGeoOverlay.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 4208eea2d..89cd112cd 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -606,10 +606,10 @@ type IPCCRequest struct { type IPCCResponse struct { CC string `json:"countryCode"` AgeRestrictedGeo bool `json:"isAgeRestrictedGeo,omitempty"` + AgeBlockedGeo bool `json:"isAgeBlockedGeo,omitempty"` } -// IP address data is powered by IPinfo -// https://ipinfo.io +// This product includes GeoLite2 Data created by MaxMind, available from https://www.maxmind.com. func (srv *Server) WebIpCC(c echo.Context) error { realIP := c.RealIP() addr, err := netip.ParseAddr(realIP) diff --git a/src/components/BlockedGeoOverlay.tsx b/src/components/BlockedGeoOverlay.tsx new file mode 100644 index 000000000..ae5790da9 --- /dev/null +++ b/src/components/BlockedGeoOverlay.tsx @@ -0,0 +1,109 @@ +import {useEffect} from 'react' +import {ScrollView, View} from 'react-native' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {Full as Logo, Mark} from '#/components/icons/Logo' +import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link' +import {Text} from '#/components/Typography' + +export function BlockedGeoOverlay() { + const t = useTheme() + const {_} = useLingui() + const {gtPhone} = useBreakpoints() + const insets = useSafeAreaInsets() + + useEffect(() => { + // just counting overall hits here + logger.metric(`blockedGeoOverlay:shown`, {}) + }, []) + + const textStyles = [a.text_md, a.leading_normal] + const links = { + blog: { + to: `https://bsky.social/about/blog/08-22-2025-mississippi-hb1126`, + label: _(msg`Read our blog post`), + overridePresentation: false, + disableMismatchWarning: true, + style: textStyles, + }, + } + + const blocks = [ + _(msg`Unfortunately, Bluesky is unavailable in Mississippi right now.`), + _( + msg`A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies.`, + ), + _( + msg`As a small team, we cannot justify building the expensive infrastructure this requirement demands while legal challenges to this law are pending.`, + ), + _( + msg`For now, we have made the difficult decision to block access to Bluesky in the state of Mississippi.`, + ), + <> + To learn more, read our{' '} + blog post. + , + ] + + return ( + + + + + + + Announcement + + + + + + {blocks.map((block, index) => ( + + {block} + + ))} + + + + + + ) +} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 6954be6a8..421a7fe9d 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react' -import {type GestureResponderEvent} from 'react-native' +import {type GestureResponderEvent, Linking} from 'react-native' import {sanitizeUrl} from '@braintree/sanitize-url' import { type LinkProps as RNLinkProps, @@ -13,6 +13,7 @@ import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types' import {shareUrl} from '#/lib/sharing' import { convertBskyAppUrlIfNeeded, + createProxiedUrl, isBskyDownloadUrl, isExternalUrl, linkRequiresWarning, @@ -407,6 +408,91 @@ export function InlineLinkText({ ) } +/** + * A barebones version of `InlineLinkText`, for use outside a + * `react-navigation` context. + */ +export function SimpleInlineLinkText({ + children, + to, + style, + download, + selectable, + label, + disableUnderline, + shouldProxy, + ...rest +}: Omit< + InlineLinkProps, + | 'to' + | 'action' + | 'disableMismatchWarning' + | 'overridePresentation' + | 'onPress' + | 'onLongPress' + | 'shareOnLongPress' +> & { + to: string +}) { + const t = useTheme() + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const flattenedStyle = flatten(style) || {} + const isExternal = isExternalUrl(to) + + let href = to + if (shouldProxy) { + href = createProxiedUrl(href) + } + + const onPress = () => { + Linking.openURL(href) + } + + return ( + + {children} + + ) +} + export function WebOnlyInlineLinkText({ children, to, diff --git a/src/components/icons/Logo.tsx b/src/components/icons/Logo.tsx index 6f16d8a44..75c5cb420 100644 --- a/src/components/icons/Logo.tsx +++ b/src/components/icons/Logo.tsx @@ -1,5 +1,42 @@ +import Svg, {Path} from 'react-native-svg' + +import {type Props, useCommonSVGProps} from './common' import {createSinglePathSVG} from './TEMPLATE' export const Mark = createSinglePathSVG({ path: 'M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z', }) + +export function Full( + props: Omit & { + markFill?: Props['fill'] + textFill?: Props['fill'] + }, +) { + const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) + const ratio = 123 / 555 + + return ( + + {gradient} + + + + ) +} diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 0c9ea1ef6..4a09d8593 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -475,4 +475,9 @@ export type MetricEvents = { 'ageAssurance:redirectDialogFail': {} 'ageAssurance:appealDialogOpen': {} 'ageAssurance:appealDialogSubmit': {} + + /* + * Specifically for the `BlockedGeoOverlay` + */ + 'blockedGeoOverlay:shown': {} } diff --git a/src/state/geolocation.tsx b/src/state/geolocation.tsx index 4581996a0..a69161324 100644 --- a/src/state/geolocation.tsx +++ b/src/state/geolocation.tsx @@ -25,6 +25,7 @@ const onGeolocationUpdate = ( */ export const DEFAULT_GEOLOCATION: Device['geolocation'] = { countryCode: undefined, + isAgeBlockedGeo: undefined, isAgeRestrictedGeo: false, } @@ -40,6 +41,7 @@ async function getGeolocation(): Promise { if (json.countryCode) { return { countryCode: json.countryCode, + isAgeBlockedGeo: json.isAgeBlockedGeo ?? false, isAgeRestrictedGeo: json.isAgeRestrictedGeo ?? false, } } else { diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 421264ac1..a3f2336cf 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -10,6 +10,7 @@ export type Device = { geolocation?: { countryCode: string | undefined isAgeRestrictedGeo: boolean | undefined + isAgeBlockedGeo: boolean | undefined } trendingBetaEnabled: boolean devMode: boolean diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 04fccc44c..8b4c65b8f 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -13,6 +13,7 @@ import {useNotificationsRegistration} from '#/lib/notifications/notifications' import {isStateAtTabRoot} from '#/lib/routes/helpers' import {isAndroid, isIOS} from '#/platform/detection' import {useDialogFullyExpandedCountContext} from '#/state/dialogs' +import {useGeolocation} from '#/state/geolocation' import {useSession} from '#/state/session' import { useIsDrawerOpen, @@ -26,6 +27,7 @@ import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, select, useTheme} from '#/alf' import {setSystemUITheme} from '#/alf/util/systemUI' import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' +import {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' @@ -180,9 +182,11 @@ function ShellInner() { ) } -export const Shell: React.FC = function ShellImpl() { - const fullyExpandedCount = useDialogFullyExpandedCountContext() +export function Shell() { const t = useTheme() + const {geolocation} = useGeolocation() + const fullyExpandedCount = useDialogFullyExpandedCountContext() + useIntentHandler() useEffect(() => { @@ -200,9 +204,13 @@ export const Shell: React.FC = function ShellImpl() { navigationBar: t.name !== 'light' ? 'light' : 'dark', }} /> - - - + {geolocation?.isAgeBlockedGeo ? ( + + ) : ( + + + + )} ) } diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 3c2bc58ab..f942ab49e 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -5,11 +5,10 @@ import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {RemoveScrollBar} from 'react-remove-scroll-bar' -import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {useIntentHandler} from '#/lib/hooks/useIntentHandler' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type NavigationProp} from '#/lib/routes/types' -import {colors} from '#/lib/styles' +import {useGeolocation} from '#/state/geolocation' import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' import {useCloseAllActiveElements} from '#/state/util' @@ -18,6 +17,7 @@ import {ModalsContainer} from '#/view/com/modals/Modal' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, select, useTheme} from '#/alf' import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' +import {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' @@ -130,24 +130,23 @@ function ShellInner() { ) } -export const Shell: React.FC = function ShellImpl() { - const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) +export function Shell() { + const t = useTheme() + const {geolocation} = useGeolocation() return ( - - - - + + {geolocation?.isAgeBlockedGeo ? ( + + ) : ( + + + + )} ) } const styles = StyleSheet.create({ - bgLight: { - backgroundColor: colors.white, - }, - bgDark: { - backgroundColor: colors.black, // TODO - }, drawerMask: { ...a.fixed, width: '100%', -- cgit 1.4.1