Skip to content

Commit f8ae054

Browse files
Provide geo-gated users optional GPS fallback for precise location data (#8973)
1 parent 625b4e6 commit f8ae054

31 files changed

+1075
-298
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,9 @@ EXPO_PUBLIC_SENTRY_DSN=
3333

3434
# Bitdrift API key. If undefined, Bitdrift will be disabled.
3535
EXPO_PUBLIC_BITDRIFT_API_KEY=
36+
37+
# bapp-config web worker URL
38+
BAPP_CONFIG_DEV_URL=
39+
40+
# Dev-only passthrough value for bapp-config web worker
41+
BAPP_CONFIG_DEV_BYPASS_SECRET=

app.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ module.exports = function (_config) {
360360
},
361361
],
362362
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
363+
['expo-location'],
363364
].filter(Boolean),
364365
extra: {
365366
eas: {
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
"expo-linear-gradient": "~14.1.5",
152152
"expo-linking": "~7.1.5",
153153
"expo-localization": "~16.1.5",
154+
"expo-location": "~18.1.6",
154155
"expo-media-library": "~17.1.7",
155156
"expo-notifications": "~0.31.3",
156157
"expo-screen-orientation": "~8.1.7",

src/App.native.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import {Provider as DialogStateProvider} from '#/state/dialogs'
3232
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
3333
import {listenSessionDropped} from '#/state/events'
3434
import {
35-
beginResolveGeolocation,
36-
ensureGeolocationResolved,
35+
beginResolveGeolocationConfig,
36+
ensureGeolocationConfigIsResolved,
3737
Provider as GeolocationProvider,
3838
} from '#/state/geolocation'
3939
import {GlobalGestureEventsProvider} from '#/state/global-gesture-events'
@@ -91,7 +91,7 @@ if (isAndroid) {
9191
/**
9292
* Begin geolocation ASAP
9393
*/
94-
beginResolveGeolocation()
94+
beginResolveGeolocationConfig()
9595

9696
function InnerApp() {
9797
const [isReady, setIsReady] = React.useState(false)
@@ -203,9 +203,10 @@ function App() {
203203
const [isReady, setReady] = useState(false)
204204

205205
React.useEffect(() => {
206-
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
207-
setReady(true),
208-
)
206+
Promise.all([
207+
initPersistedState(),
208+
ensureGeolocationConfigIsResolved(),
209+
]).then(() => setReady(true))
209210
}, [])
210211

211212
if (!isReady) {

src/App.web.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import {Provider as DialogStateProvider} from '#/state/dialogs'
2121
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
2222
import {listenSessionDropped} from '#/state/events'
2323
import {
24-
beginResolveGeolocation,
25-
ensureGeolocationResolved,
24+
beginResolveGeolocationConfig,
25+
ensureGeolocationConfigIsResolved,
2626
Provider as GeolocationProvider,
2727
} from '#/state/geolocation'
2828
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
@@ -69,7 +69,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom
6969
/**
7070
* Begin geolocation ASAP
7171
*/
72-
beginResolveGeolocation()
72+
beginResolveGeolocationConfig()
7373

7474
function InnerApp() {
7575
const [isReady, setIsReady] = React.useState(false)
@@ -178,9 +178,10 @@ function App() {
178178
const [isReady, setReady] = useState(false)
179179

180180
React.useEffect(() => {
181-
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
182-
setReady(true),
183-
)
181+
Promise.all([
182+
initPersistedState(),
183+
ensureGeolocationConfigIsResolved(),
184+
]).then(() => setReady(true))
184185
}, [])
185186

186187
if (!isReady) {

src/components/BlockedGeoOverlay.tsx

Lines changed: 130 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@ import {useLingui} from '@lingui/react'
66

77
import {logger} from '#/logger'
88
import {isWeb} from '#/platform/detection'
9+
import {useDeviceGeolocationApi} from '#/state/geolocation'
910
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
11+
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
12+
import * as Dialog from '#/components/Dialog'
13+
import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog'
14+
import {Divider} from '#/components/Divider'
1015
import {Full as Logo, Mark} from '#/components/icons/Logo'
16+
import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation'
1117
import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link'
18+
import {Outlet as PortalOutlet} from '#/components/Portal'
19+
import * as Toast from '#/components/Toast'
1220
import {Text} from '#/components/Typography'
21+
import {BottomSheetOutlet} from '#/../modules/bottom-sheet'
1322

1423
export function BlockedGeoOverlay() {
1524
const t = useTheme()
1625
const {_} = useLingui()
1726
const {gtPhone} = useBreakpoints()
1827
const insets = useSafeAreaInsets()
28+
const geoDialog = Dialog.useDialogControl()
29+
const {setDeviceGeolocation} = useDeviceGeolocationApi()
1930

2031
useEffect(() => {
2132
// just counting overall hits here
@@ -51,59 +62,133 @@ export function BlockedGeoOverlay() {
5162
]
5263

5364
return (
54-
<ScrollView
55-
contentContainerStyle={[
56-
a.px_2xl,
57-
{
58-
paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
59-
paddingBottom: 100,
60-
},
61-
]}>
62-
<View
63-
style={[
64-
a.mx_auto,
65-
web({
66-
maxWidth: 440,
67-
paddingTop: gtPhone ? '8vh' : undefined,
68-
}),
65+
<>
66+
<ScrollView
67+
contentContainerStyle={[
68+
a.px_2xl,
69+
{
70+
paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
71+
paddingBottom: 100,
72+
},
6973
]}>
70-
<View style={[a.align_start]}>
71-
<View
72-
style={[
73-
a.pl_md,
74-
a.pr_lg,
75-
a.py_sm,
76-
a.rounded_full,
77-
a.flex_row,
78-
a.align_center,
79-
a.gap_xs,
80-
{
81-
backgroundColor: t.palette.primary_25,
82-
},
83-
]}>
84-
<Mark fill={t.palette.primary_600} width={14} />
85-
<Text
74+
<View
75+
style={[
76+
a.mx_auto,
77+
web({
78+
maxWidth: 380,
79+
paddingTop: gtPhone ? '8vh' : undefined,
80+
}),
81+
]}>
82+
<View style={[a.align_start]}>
83+
<View
8684
style={[
87-
a.font_bold,
85+
a.pl_md,
86+
a.pr_lg,
87+
a.py_sm,
88+
a.rounded_full,
89+
a.flex_row,
90+
a.align_center,
91+
a.gap_xs,
8892
{
89-
color: t.palette.primary_600,
93+
backgroundColor: t.palette.primary_25,
9094
},
9195
]}>
92-
<Trans>Announcement</Trans>
93-
</Text>
96+
<Mark fill={t.palette.primary_600} width={14} />
97+
<Text
98+
style={[
99+
a.font_bold,
100+
{
101+
color: t.palette.primary_600,
102+
},
103+
]}>
104+
<Trans>Announcement</Trans>
105+
</Text>
106+
</View>
107+
</View>
108+
109+
<View style={[a.gap_lg, {paddingTop: 32}]}>
110+
{blocks.map((block, index) => (
111+
<Text key={index} style={[textStyles]}>
112+
{block}
113+
</Text>
114+
))}
94115
</View>
95-
</View>
96116

97-
<View style={[a.gap_lg, {paddingTop: 32, paddingBottom: 48}]}>
98-
{blocks.map((block, index) => (
99-
<Text key={index} style={[textStyles]}>
100-
{block}
101-
</Text>
102-
))}
117+
{!isWeb && (
118+
<>
119+
<View style={[a.pt_2xl]}>
120+
<Divider />
121+
</View>
122+
123+
<View style={[a.mt_xl, a.align_start]}>
124+
<Text
125+
style={[a.text_lg, a.font_heavy, a.leading_snug, a.pb_xs]}>
126+
<Trans>Not in Mississippi?</Trans>
127+
</Text>
128+
<Text
129+
style={[
130+
a.text_sm,
131+
a.leading_snug,
132+
t.atoms.text_contrast_medium,
133+
a.pb_md,
134+
]}>
135+
<Trans>
136+
Confirm your location with GPS. Your location data is not
137+
tracked and does not leave your device.
138+
</Trans>
139+
</Text>
140+
<Button
141+
label={_(msg`Confirm your location`)}
142+
onPress={() => geoDialog.open()}
143+
size="small"
144+
color="primary_subtle">
145+
<ButtonIcon icon={LocationIcon} />
146+
<ButtonText>
147+
<Trans>Confirm your location</Trans>
148+
</ButtonText>
149+
</Button>
150+
</View>
151+
152+
<DeviceLocationRequestDialog
153+
control={geoDialog}
154+
onLocationAcquired={props => {
155+
if (props.geolocationStatus.isAgeBlockedGeo) {
156+
props.disableDialogAction()
157+
props.setDialogError(
158+
_(
159+
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.`,
160+
),
161+
)
162+
} else {
163+
props.closeDialog(() => {
164+
// set this after close!
165+
setDeviceGeolocation({
166+
countryCode: props.geolocationStatus.countryCode,
167+
regionCode: props.geolocationStatus.regionCode,
168+
})
169+
Toast.show(_(msg`Thanks! You're all set.`), {
170+
type: 'success',
171+
})
172+
})
173+
}
174+
}}
175+
/>
176+
</>
177+
)}
178+
179+
<View style={[{paddingTop: 48}]}>
180+
<Logo width={120} textFill={t.atoms.text.color} />
181+
</View>
103182
</View>
183+
</ScrollView>
104184

105-
<Logo width={120} textFill={t.atoms.text.color} />
106-
</View>
107-
</ScrollView>
185+
{/*
186+
* While this blocking overlay is up, other dialogs in the shell
187+
* are not mounted, so it _should_ be safe to use these here
188+
* without fear of other modals showing up.
189+
*/}
190+
<BottomSheetOutlet />
191+
<PortalOutlet />
192+
</>
108193
)
109194
}

src/components/ageAssurance/AgeAssuranceAccountCard.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import {msg, Trans} from '@lingui/macro'
33
import {useLingui} from '@lingui/react'
44

55
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
6+
import {isNative} from '#/platform/detection'
67
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
78
import {logger} from '#/state/ageAssurance/util'
9+
import {useDeviceGeolocationApi} from '#/state/geolocation'
810
import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
911
import {Admonition} from '#/components/Admonition'
1012
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
@@ -16,8 +18,10 @@ import {
1618
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
1719
import {Button, ButtonText} from '#/components/Button'
1820
import * as Dialog from '#/components/Dialog'
21+
import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog'
1922
import {Divider} from '#/components/Divider'
2023
import {createStaticClick, InlineLinkText} from '#/components/Link'
24+
import * as Toast from '#/components/Toast'
2125
import {Text} from '#/components/Typography'
2226

2327
export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) {
@@ -35,8 +39,10 @@ function Inner({style}: ViewStyleProp & {}) {
3539
const {_, i18n} = useLingui()
3640
const control = useDialogControl()
3741
const appealControl = Dialog.useDialogControl()
42+
const locationControl = Dialog.useDialogControl()
3843
const getTimeAgo = useGetTimeAgo()
3944
const {gtPhone} = useBreakpoints()
45+
const {setDeviceGeolocation} = useDeviceGeolocationApi()
4046

4147
const copy = useAgeAssuranceCopy()
4248
const {status, lastInitiatedAt} = useAgeAssurance()
@@ -71,8 +77,50 @@ function Inner({style}: ViewStyleProp & {}) {
7177
</View>
7278
</View>
7379

74-
<View style={[a.pb_md]}>
80+
<View style={[a.pb_md, a.gap_xs]}>
7581
<Text style={[a.text_sm, a.leading_snug]}>{copy.notice}</Text>
82+
83+
{isNative && (
84+
<>
85+
<Text style={[a.text_sm, a.leading_snug]}>
86+
<Trans>
87+
Is your location not accurate?{' '}
88+
<InlineLinkText
89+
label={_(msg`Confirm your location`)}
90+
{...createStaticClick(() => {
91+
locationControl.open()
92+
})}>
93+
Click here to confirm your location.
94+
</InlineLinkText>{' '}
95+
</Trans>
96+
</Text>
97+
98+
<DeviceLocationRequestDialog
99+
control={locationControl}
100+
onLocationAcquired={props => {
101+
if (props.geolocationStatus.isAgeRestrictedGeo) {
102+
props.disableDialogAction()
103+
props.setDialogError(
104+
_(
105+
msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`,
106+
),
107+
)
108+
} else {
109+
props.closeDialog(() => {
110+
// set this after close!
111+
setDeviceGeolocation({
112+
countryCode: props.geolocationStatus.countryCode,
113+
regionCode: props.geolocationStatus.regionCode,
114+
})
115+
Toast.show(_(msg`Thanks! You're all set.`), {
116+
type: 'success',
117+
})
118+
})
119+
}
120+
}}
121+
/>
122+
</>
123+
)}
76124
</View>
77125

78126
{isBlocked ? (

0 commit comments

Comments
 (0)