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