this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at e28f6d2f370b4e882ed6f23d08ca0f8d94dbac5f 436 lines 16 kB view raw
1import {useCallback, useEffect} from 'react' 2import {ScrollView, View} from 'react-native' 3import {useSafeAreaInsets} from 'react-native-safe-area-context' 4import {msg} from '@lingui/core/macro' 5import {useLingui} from '@lingui/react' 6import {Trans} from '@lingui/react/macro' 7 8import { 9 SupportCode, 10 useCreateSupportLink, 11} from '#/lib/hooks/useCreateSupportLink' 12import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 13import {useIsBirthdateUpdateAllowed} from '#/state/birthdate' 14import {useSessionApi} from '#/state/session' 15import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' 16import {DeleteAccountDialog} from '#/screens/Settings/components/DeleteAccountDialog' 17import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 18import {Admonition} from '#/components/Admonition' 19import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' 20import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 21import {AgeAssuranceInitDialog} from '#/components/ageAssurance/AgeAssuranceInitDialog' 22import {Button, ButtonIcon, ButtonText} from '#/components/Button' 23import {useDialogControl} from '#/components/Dialog' 24import * as Dialog from '#/components/Dialog' 25import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' 26import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog' 27import {Full as Logo} from '#/components/icons/Logo' 28import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield' 29import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' 30import {Outlet as PortalOutlet} from '#/components/Portal' 31import * as Toast from '#/components/Toast' 32import {Text} from '#/components/Typography' 33import {BottomSheetOutlet} from '#/../modules/bottom-sheet' 34import {useAgeAssurance} from '#/ageAssurance' 35import {useAgeAssuranceDataContext} from '#/ageAssurance/data' 36import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' 37import { 38 isLegacyBirthdateBug, 39 useAgeAssuranceRegionConfig, 40} from '#/ageAssurance/util' 41import {useAnalytics} from '#/analytics' 42import {IS_NATIVE, IS_WEB} from '#/env' 43import {useDeviceGeolocationApi} from '#/geolocation' 44 45const textStyles = [a.text_md, a.leading_snug] 46 47export function NoAccessScreen() { 48 const t = useTheme() 49 const {_} = useLingui() 50 const ax = useAnalytics() 51 const {gtPhone} = useBreakpoints() 52 const insets = useSafeAreaInsets() 53 const birthdateControl = useDialogControl() 54 const deactivateAccountControl = useDialogControl() 55 const deleteAccountControl = useDialogControl() 56 const {data} = useAgeAssuranceDataContext() 57 const region = useAgeAssuranceRegionConfig() 58 const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed() 59 const {logoutCurrentAccount} = useSessionApi() 60 const createSupportLink = useCreateSupportLink() 61 62 const aa = useAgeAssurance() 63 const isBlocked = aa.state.status === aa.Status.Blocked 64 const isAARegion = !!region 65 const hasDeclaredAge = data?.declaredAge !== undefined 66 const canUpdateBirthday = 67 isBirthdateUpdateAllowed || isLegacyBirthdateBug(data?.birthdate || '') 68 69 useEffect(() => { 70 // just counting overall hits here 71 ax.metric(`blockedGeoOverlay:shown`, {}) 72 ax.metric(`ageAssurance:noAccessScreen:shown`, { 73 accountCreatedAt: data?.accountCreatedAt || 'unknown', 74 isAARegion, 75 hasDeclaredAge, 76 canUpdateBirthday, 77 }) 78 // TODO This can be cleaned up with useEffectEvent once we're on 19.2 79 // eslint-disable-next-line react-hooks/exhaustive-deps 80 }, []) 81 82 const onPressLogout = useCallback(() => { 83 if (IS_WEB) { 84 // We're switching accounts, which remounts the entire app. 85 // On mobile, this gets us Home, but on the web we also need reset the URL. 86 // We can't change the URL via a navigate() call because the navigator 87 // itself is about to unmount, and it calls pushState() too late. 88 // So we change the URL ourselves. The navigator will pick it up on remount. 89 history.pushState(null, '', '/') 90 } 91 logoutCurrentAccount('AgeAssuranceNoAccessScreen') 92 }, [logoutCurrentAccount]) 93 94 const orgAdmonition = ( 95 <Admonition type="tip"> 96 <Trans> 97 For organizational accounts, use the birthdate of the person who is 98 responsible for the account. 99 </Trans> 100 </Admonition> 101 ) 102 103 const birthdateUpdateText = canUpdateBirthday ? ( 104 <> 105 <Text style={[textStyles]}> 106 <Trans> 107 If you believe your birthdate is incorrect, you can update it by{' '} 108 <SimpleInlineLinkText 109 label={_(msg`Click here to update your birthdate`)} 110 style={[textStyles]} 111 {...createStaticClick(() => { 112 ax.metric('ageAssurance:noAccessScreen:openBirthdateDialog', {}) 113 birthdateControl.open() 114 })}> 115 clicking here 116 </SimpleInlineLinkText> 117 . 118 </Trans> 119 </Text> 120 121 {orgAdmonition} 122 </> 123 ) : ( 124 <Text style={[textStyles]}> 125 <Trans> 126 If you believe your birthdate is incorrect, please{' '} 127 <SimpleInlineLinkText 128 to={createSupportLink({code: SupportCode.AA_BIRTHDATE})} 129 label={_(msg`Click here to contact our support team`)} 130 style={[textStyles]}> 131 contact our support team 132 </SimpleInlineLinkText> 133 . 134 </Trans> 135 </Text> 136 ) 137 138 return ( 139 <> 140 <View style={[a.util_screen_outer, a.flex_1]}> 141 <ScrollView 142 contentContainerStyle={[ 143 a.px_2xl, 144 { 145 paddingTop: IS_WEB 146 ? a.p_5xl.padding 147 : insets.top + a.p_2xl.padding, 148 paddingBottom: 100, 149 }, 150 ]}> 151 <View 152 style={[ 153 a.mx_auto, 154 a.w_full, 155 web({ 156 maxWidth: 380, 157 paddingTop: gtPhone ? '8vh' : undefined, 158 }), 159 { 160 gap: 32, 161 }, 162 ]}> 163 <View style={[a.align_start]}> 164 <AgeAssuranceBadge /> 165 </View> 166 167 {hasDeclaredAge ? ( 168 <> 169 {isAARegion ? ( 170 <> 171 <View style={[a.gap_lg]}> 172 <Text style={[textStyles]}> 173 <Trans>Hey there!</Trans> 174 </Text> 175 <Text style={[textStyles]}> 176 <Trans> 177 You are accessing Bluesky from a region that legally 178 requires us to verify your age before allowing you to 179 access the app. 180 </Trans> 181 </Text> 182 183 {!aa.flags.isOverRegionMinAccessAge && ( 184 <Text style={[textStyles]}> 185 <Trans> 186 Unfortunately, your declared age indicates that you 187 are not old enough to access Bluesky in your region. 188 </Trans> 189 </Text> 190 )} 191 192 {!isBlocked && birthdateUpdateText} 193 </View> 194 195 {aa.flags.isOverRegionMinAccessAge && <AccessSection />} 196 </> 197 ) : ( 198 <View style={[a.gap_lg]}> 199 <Text style={[textStyles]}> 200 <Trans> 201 Unfortunately, the birthdate you have saved to your 202 profile makes you too young to access Bluesky. 203 </Trans> 204 </Text> 205 206 {birthdateUpdateText} 207 </View> 208 )} 209 </> 210 ) : ( 211 <View style={[a.gap_lg]}> 212 <Text style={[textStyles]}> 213 <Trans>Hi there!</Trans> 214 </Text> 215 <Text style={[textStyles]}> 216 <Trans> 217 In order to provide an age-appropriate experience, we need 218 to know your birthdate. This is a one-time thing, and your 219 data will be kept private. 220 </Trans> 221 </Text> 222 <Text style={[textStyles]}> 223 <Trans> 224 Set your birthdate below and we'll get you back to posting 225 and exploring in no time! 226 </Trans> 227 </Text> 228 <Button 229 color="primary" 230 size="large" 231 label={_(msg`Click here to update your birthdate`)} 232 onPress={() => birthdateControl.open()}> 233 <ButtonText> 234 <Trans>Add your birthdate</Trans> 235 </ButtonText> 236 </Button> 237 238 {orgAdmonition} 239 </View> 240 )} 241 242 <View style={[a.pt_lg, a.gap_xl, {maxWidth: 280}]}> 243 <Logo width={120} textFill={t.atoms.text.color} /> 244 <Text 245 style={[ 246 a.text_sm, 247 a.italic, 248 a.leading_snug, 249 t.atoms.text_contrast_medium, 250 ]}> 251 <Trans> 252 To log out,{' '} 253 <SimpleInlineLinkText 254 label={_(msg`Click here to log out`)} 255 {...createStaticClick(() => { 256 onPressLogout() 257 })} 258 style={[a.italic]}> 259 click here 260 </SimpleInlineLinkText> 261 . Or if you’d prefer, you can{' '} 262 <SimpleInlineLinkText 263 label={_(msg`Click here to delete your account`)} 264 {...createStaticClick(() => { 265 ax.metric( 266 'ageAssurance:noAccessScreen:openDeleteAccountDialog', 267 {}, 268 ) 269 deleteAccountControl.open() 270 })} 271 style={[a.italic]}> 272 delete your account 273 </SimpleInlineLinkText> 274 . 275 </Trans> 276 </Text> 277 </View> 278 </View> 279 </ScrollView> 280 </View> 281 282 <BirthDateSettingsDialog control={birthdateControl} /> 283 <DeactivateAccountDialog control={deactivateAccountControl} /> 284 <DeleteAccountDialog 285 control={deleteAccountControl} 286 deactivateDialogControl={deactivateAccountControl} 287 /> 288 289 {/* 290 * While this blocking overlay is up, other dialogs in the shell 291 * are not mounted, so it _should_ be safe to use these here 292 * without fear of other modals showing up. 293 */} 294 <BottomSheetOutlet /> 295 <PortalOutlet /> 296 </> 297 ) 298} 299 300function AccessSection() { 301 const t = useTheme() 302 const {_, i18n} = useLingui() 303 const ax = useAnalytics() 304 const control = useDialogControl() 305 const appealControl = Dialog.useDialogControl() 306 const locationControl = Dialog.useDialogControl() 307 const getTimeAgo = useGetTimeAgo() 308 const {setDeviceGeolocation} = useDeviceGeolocationApi() 309 const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess() 310 311 const aa = useAgeAssurance() 312 const {status, lastInitiatedAt} = aa.state 313 const isBlocked = status === aa.Status.Blocked 314 const hasInitiated = !!lastInitiatedAt 315 const timeAgo = lastInitiatedAt 316 ? getTimeAgo(lastInitiatedAt, new Date()) 317 : null 318 const diff = lastInitiatedAt 319 ? dateDiff(lastInitiatedAt, new Date(), 'down') 320 : null 321 322 return ( 323 <> 324 <AgeAssuranceInitDialog control={control} /> 325 <AgeAssuranceAppealDialog control={appealControl} /> 326 327 <View style={[a.gap_xl]}> 328 {isBlocked ? ( 329 <Admonition type="warning"> 330 <Trans> 331 You are currently unable to access Bluesky's Age Assurance flow. 332 Please{' '} 333 <SimpleInlineLinkText 334 label={_(msg`Contact our moderation team`)} 335 {...createStaticClick(() => { 336 appealControl.open() 337 ax.metric('ageAssurance:appealDialogOpen', {}) 338 })}> 339 contact our moderation team 340 </SimpleInlineLinkText>{' '} 341 if you believe this is an error. 342 </Trans> 343 </Admonition> 344 ) : ( 345 <> 346 <View style={[a.gap_md]}> 347 <Button 348 label={_(msg`Verify now`)} 349 size="large" 350 color={hasInitiated ? 'secondary' : 'primary'} 351 onPress={() => { 352 control.open() 353 ax.metric('ageAssurance:initDialogOpen', { 354 hasInitiatedPreviously: hasInitiated, 355 }) 356 }}> 357 <ButtonIcon icon={ShieldIcon} /> 358 <ButtonText> 359 {hasInitiated ? ( 360 <Trans>Verify again</Trans> 361 ) : ( 362 <Trans>Verify now</Trans> 363 )} 364 </ButtonText> 365 </Button> 366 367 {lastInitiatedAt && timeAgo && diff ? ( 368 <Text 369 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]} 370 title={i18n.date(lastInitiatedAt, { 371 dateStyle: 'medium', 372 timeStyle: 'medium', 373 })}> 374 {diff.value === 0 ? ( 375 <Trans>Last initiated just now</Trans> 376 ) : ( 377 <Trans>Last initiated {timeAgo} ago</Trans> 378 )} 379 </Text> 380 ) : ( 381 <Text 382 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}> 383 <Trans>Age assurance only takes a few minutes</Trans> 384 </Text> 385 )} 386 </View> 387 </> 388 )} 389 390 <View style={[a.gap_xs]}> 391 {IS_NATIVE && ( 392 <> 393 <Admonition> 394 <Trans> 395 Is your location not accurate?{' '} 396 <SimpleInlineLinkText 397 label={_(msg`Confirm your location`)} 398 {...createStaticClick(() => { 399 locationControl.open() 400 })}> 401 Tap here to confirm your location. 402 </SimpleInlineLinkText>{' '} 403 </Trans> 404 </Admonition> 405 406 <DeviceLocationRequestDialog 407 control={locationControl} 408 onLocationAcquired={props => { 409 const access = computeAgeAssuranceRegionAccess( 410 props.geolocation, 411 ) 412 if (access !== aa.Access.Full) { 413 props.disableDialogAction() 414 props.setDialogError( 415 _( 416 msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`, 417 ), 418 ) 419 } else { 420 props.closeDialog(() => { 421 // set this after close! 422 setDeviceGeolocation(props.geolocation) 423 Toast.show(_(msg`Thanks! You're all set.`), { 424 type: 'success', 425 }) 426 }) 427 } 428 }} 429 /> 430 </> 431 )} 432 </View> 433 </View> 434 </> 435 ) 436}