Bluesky app fork with some witchin' additions 馃挮
at readme-update 405 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 {useIsBirthdateUpdateAllowed} from '#/state/birthdate' 13import {useSessionApi} from '#/state/session' 14import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 15import {Admonition} from '#/components/Admonition' 16import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' 17import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 18import {AgeAssuranceInitDialog} from '#/components/ageAssurance/AgeAssuranceInitDialog' 19import {Button, ButtonIcon, ButtonText} from '#/components/Button' 20import {useDialogControl} from '#/components/Dialog' 21import * as Dialog from '#/components/Dialog' 22import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' 23import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog' 24import {Full as Logo} from '#/components/icons/Logo' 25import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield' 26import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' 27import {Outlet as PortalOutlet} from '#/components/Portal' 28import * as Toast from '#/components/Toast' 29import {Text} from '#/components/Typography' 30import {BottomSheetOutlet} from '#/../modules/bottom-sheet' 31import {useAgeAssurance} from '#/ageAssurance' 32import {useAgeAssuranceDataContext} from '#/ageAssurance/data' 33import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' 34import { 35 isLegacyBirthdateBug, 36 useAgeAssuranceRegionConfig, 37} from '#/ageAssurance/util' 38import {useAnalytics} from '#/analytics' 39import {IS_NATIVE, IS_WEB} from '#/env' 40import {useDeviceGeolocationApi} from '#/geolocation' 41 42const textStyles = [a.text_md, a.leading_snug] 43 44export function NoAccessScreen() { 45 const t = useTheme() 46 const {_} = useLingui() 47 const ax = useAnalytics() 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 ax.metric(`blockedGeoOverlay:shown`, {}) 67 ax.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 (IS_WEB) { 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 ax.metric('ageAssurance:noAccessScreen:openBirthdateDialog', {}) 107 birthdateControl.open() 108 })}> 109 clicking here 110 </SimpleInlineLinkText> 111 . 112 </Trans> 113 </Text> 114 115 {orgAdmonition} 116 </> 117 ) : ( 118 <Text style={[textStyles]}> 119 <Trans> 120 If you believe your birthdate is incorrect, please{' '} 121 <SimpleInlineLinkText 122 to={createSupportLink({code: SupportCode.AA_BIRTHDATE})} 123 label={_(msg`Click here to contact our support team`)} 124 style={[textStyles]}> 125 contact our support team 126 </SimpleInlineLinkText> 127 . 128 </Trans> 129 </Text> 130 ) 131 132 return ( 133 <> 134 <View style={[a.util_screen_outer, a.flex_1]}> 135 <ScrollView 136 contentContainerStyle={[ 137 a.px_2xl, 138 { 139 paddingTop: IS_WEB 140 ? a.p_5xl.padding 141 : insets.top + a.p_2xl.padding, 142 paddingBottom: 100, 143 }, 144 ]}> 145 <View 146 style={[ 147 a.mx_auto, 148 a.w_full, 149 web({ 150 maxWidth: 380, 151 paddingTop: gtPhone ? '8vh' : undefined, 152 }), 153 { 154 gap: 32, 155 }, 156 ]}> 157 <View style={[a.align_start]}> 158 <AgeAssuranceBadge /> 159 </View> 160 161 {hasDeclaredAge ? ( 162 <> 163 {isAARegion ? ( 164 <> 165 <View style={[a.gap_lg]}> 166 <Text style={[textStyles]}> 167 <Trans>Hey there!</Trans> 168 </Text> 169 <Text style={[textStyles]}> 170 <Trans> 171 You are accessing Bluesky from a region that legally 172 requires us to verify your age before allowing you to 173 access the app. 174 </Trans> 175 </Text> 176 177 {!aa.flags.isOverRegionMinAccessAge && ( 178 <Text style={[textStyles]}> 179 <Trans> 180 Unfortunately, your declared age indicates that you 181 are not old enough to access Bluesky in your region. 182 </Trans> 183 </Text> 184 )} 185 186 {!isBlocked && birthdateUpdateText} 187 </View> 188 189 {aa.flags.isOverRegionMinAccessAge && <AccessSection />} 190 </> 191 ) : ( 192 <View style={[a.gap_lg]}> 193 <Text style={[textStyles]}> 194 <Trans> 195 Unfortunately, the birthdate you have saved to your 196 profile makes you too young to access Bluesky. 197 </Trans> 198 </Text> 199 200 {birthdateUpdateText} 201 </View> 202 )} 203 </> 204 ) : ( 205 <View style={[a.gap_lg]}> 206 <Text style={[textStyles]}> 207 <Trans>Hi there!</Trans> 208 </Text> 209 <Text style={[textStyles]}> 210 <Trans> 211 In order to provide an age-appropriate experience, we need 212 to know your birthdate. This is a one-time thing, and your 213 data will be kept private. 214 </Trans> 215 </Text> 216 <Text style={[textStyles]}> 217 <Trans> 218 Set your birthdate below and we'll get you back to posting 219 and exploring in no time! 220 </Trans> 221 </Text> 222 <Button 223 color="primary" 224 size="large" 225 label={_(msg`Click here to update your birthdate`)} 226 onPress={() => birthdateControl.open()}> 227 <ButtonText> 228 <Trans>Add your birthdate</Trans> 229 </ButtonText> 230 </Button> 231 232 {orgAdmonition} 233 </View> 234 )} 235 236 <View style={[a.pt_lg, a.gap_xl]}> 237 <Logo width={120} textFill={t.atoms.text.color} /> 238 <Text style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}> 239 <Trans> 240 To log out,{' '} 241 <SimpleInlineLinkText 242 label={_(msg`Click here to log out`)} 243 {...createStaticClick(() => { 244 onPressLogout() 245 })}> 246 click here 247 </SimpleInlineLinkText> 248 . 249 </Trans> 250 </Text> 251 </View> 252 </View> 253 </ScrollView> 254 </View> 255 256 <BirthDateSettingsDialog control={birthdateControl} /> 257 258 {/* 259 * While this blocking overlay is up, other dialogs in the shell 260 * are not mounted, so it _should_ be safe to use these here 261 * without fear of other modals showing up. 262 */} 263 <BottomSheetOutlet /> 264 <PortalOutlet /> 265 </> 266 ) 267} 268 269function AccessSection() { 270 const t = useTheme() 271 const {_, i18n} = useLingui() 272 const ax = useAnalytics() 273 const control = useDialogControl() 274 const appealControl = Dialog.useDialogControl() 275 const locationControl = Dialog.useDialogControl() 276 const getTimeAgo = useGetTimeAgo() 277 const {setDeviceGeolocation} = useDeviceGeolocationApi() 278 const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess() 279 280 const aa = useAgeAssurance() 281 const {status, lastInitiatedAt} = aa.state 282 const isBlocked = status === aa.Status.Blocked 283 const hasInitiated = !!lastInitiatedAt 284 const timeAgo = lastInitiatedAt 285 ? getTimeAgo(lastInitiatedAt, new Date()) 286 : null 287 const diff = lastInitiatedAt 288 ? dateDiff(lastInitiatedAt, new Date(), 'down') 289 : null 290 291 return ( 292 <> 293 <AgeAssuranceInitDialog control={control} /> 294 <AgeAssuranceAppealDialog control={appealControl} /> 295 296 <View style={[a.gap_xl]}> 297 {isBlocked ? ( 298 <Admonition type="warning"> 299 <Trans> 300 You are currently unable to access Bluesky's Age Assurance flow. 301 Please{' '} 302 <SimpleInlineLinkText 303 label={_(msg`Contact our moderation team`)} 304 {...createStaticClick(() => { 305 appealControl.open() 306 ax.metric('ageAssurance:appealDialogOpen', {}) 307 })}> 308 contact our moderation team 309 </SimpleInlineLinkText>{' '} 310 if you believe this is an error. 311 </Trans> 312 </Admonition> 313 ) : ( 314 <> 315 <View style={[a.gap_md]}> 316 <Button 317 label={_(msg`Verify now`)} 318 size="large" 319 color={hasInitiated ? 'secondary' : 'primary'} 320 onPress={() => { 321 control.open() 322 ax.metric('ageAssurance:initDialogOpen', { 323 hasInitiatedPreviously: hasInitiated, 324 }) 325 }}> 326 <ButtonIcon icon={ShieldIcon} /> 327 <ButtonText> 328 {hasInitiated ? ( 329 <Trans>Verify again</Trans> 330 ) : ( 331 <Trans>Verify now</Trans> 332 )} 333 </ButtonText> 334 </Button> 335 336 {lastInitiatedAt && timeAgo && diff ? ( 337 <Text 338 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]} 339 title={i18n.date(lastInitiatedAt, { 340 dateStyle: 'medium', 341 timeStyle: 'medium', 342 })}> 343 {diff.value === 0 ? ( 344 <Trans>Last initiated just now</Trans> 345 ) : ( 346 <Trans>Last initiated {timeAgo} ago</Trans> 347 )} 348 </Text> 349 ) : ( 350 <Text 351 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}> 352 <Trans>Age assurance only takes a few minutes</Trans> 353 </Text> 354 )} 355 </View> 356 </> 357 )} 358 359 <View style={[a.gap_xs]}> 360 {IS_NATIVE && ( 361 <> 362 <Admonition> 363 <Trans> 364 Is your location not accurate?{' '} 365 <SimpleInlineLinkText 366 label={_(msg`Confirm your location`)} 367 {...createStaticClick(() => { 368 locationControl.open() 369 })}> 370 Tap here to confirm your location. 371 </SimpleInlineLinkText>{' '} 372 </Trans> 373 </Admonition> 374 375 <DeviceLocationRequestDialog 376 control={locationControl} 377 onLocationAcquired={props => { 378 const access = computeAgeAssuranceRegionAccess( 379 props.geolocation, 380 ) 381 if (access !== aa.Access.Full) { 382 props.disableDialogAction() 383 props.setDialogError( 384 _( 385 msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`, 386 ), 387 ) 388 } else { 389 props.closeDialog(() => { 390 // set this after close! 391 setDeviceGeolocation(props.geolocation) 392 Toast.show(_(msg`Thanks! You're all set.`), { 393 type: 'success', 394 }) 395 }) 396 } 397 }} 398 /> 399 </> 400 )} 401 </View> 402 </View> 403 </> 404 ) 405}