Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at theme-changes 339 lines 8.9 kB view raw
1import { 2 createContext, 3 useCallback, 4 useContext, 5 useEffect, 6 useMemo, 7 useRef, 8 useState, 9} from 'react' 10import {Dimensions, View} from 'react-native' 11import * as Linking from 'expo-linking' 12import {msg} from '@lingui/core/macro' 13import {useLingui} from '@lingui/react' 14import {Trans} from '@lingui/react/macro' 15 16import {retry} from '#/lib/async/retry' 17import {wait} from '#/lib/async/wait' 18import {parseLinkingUrl} from '#/lib/parseLinkingUrl' 19import {useAgent, useSession} from '#/state/session' 20import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 21import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 22import {Button, ButtonText} from '#/components/Button' 23import {FullWindowOverlay} from '#/components/FullWindowOverlay' 24import {CheckThick_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check' 25import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' 26import {Loader} from '#/components/Loader' 27import {Text} from '#/components/Typography' 28import {refetchAgeAssuranceServerState} from '#/ageAssurance' 29import {useAnalytics} from '#/analytics' 30import {IS_IOS, IS_WEB} from '#/env' 31 32export type RedirectOverlayState = { 33 result: 'success' | 'unknown' 34 actorDid: string 35} 36 37/** 38 * Validate and parse the query parameters returned from the age assurance 39 * redirect. If not valid, returns `undefined` and the dialog will not open. 40 */ 41export function parseRedirectOverlayState( 42 state: { 43 result?: string 44 actorDid?: string 45 } = {}, 46): RedirectOverlayState | undefined { 47 let result: RedirectOverlayState['result'] = 'unknown' 48 const actorDid = state.actorDid 49 50 switch (state.result) { 51 case 'success': 52 result = 'success' 53 break 54 case 'unknown': 55 default: 56 result = 'unknown' 57 break 58 } 59 60 if (actorDid) { 61 return { 62 result, 63 actorDid, 64 } 65 } 66} 67 68const Context = createContext<{ 69 isOpen: boolean 70 open: (state: RedirectOverlayState) => void 71 close: () => void 72}>({ 73 isOpen: false, 74 open: () => {}, 75 close: () => {}, 76}) 77 78export function useRedirectOverlayContext() { 79 return useContext(Context) 80} 81 82export function Provider({children}: {children?: React.ReactNode}) { 83 const {currentAccount} = useSession() 84 const incomingUrl = Linking.useLinkingURL() 85 const [state, setState] = useState<RedirectOverlayState | null>(() => { 86 if (!incomingUrl) return null 87 const url = parseLinkingUrl(incomingUrl) 88 if (url.pathname !== '/intent/age-assurance') return null 89 const params = url.searchParams 90 const parsedState = parseRedirectOverlayState({ 91 result: params.get('result') ?? undefined, 92 actorDid: params.get('actorDid') ?? undefined, 93 }) 94 95 if (IS_WEB) { 96 // Clear the URL parameters so they don't re-trigger 97 history.pushState(null, '', '/') 98 } 99 100 /* 101 * If we don't have an account or the account doesn't match, do 102 * nothing. By the time the user switches to their other account, AA 103 * state should be ready for them. 104 */ 105 if ( 106 parsedState && 107 currentAccount && 108 parsedState.actorDid === currentAccount.did 109 ) { 110 return parsedState 111 } 112 113 return null 114 }) 115 const open = useCallback((state: RedirectOverlayState) => { 116 setState(state) 117 }, []) 118 const close = useCallback(() => { 119 setState(null) 120 }, []) 121 122 return ( 123 <Context.Provider 124 value={useMemo( 125 () => ({ 126 isOpen: state !== null, 127 open, 128 close, 129 }), 130 [state, open, close], 131 )}> 132 {children} 133 </Context.Provider> 134 ) 135} 136 137export function RedirectOverlay() { 138 const t = useTheme() 139 const {_} = useLingui() 140 const {isOpen} = useRedirectOverlayContext() 141 const {gtMobile} = useBreakpoints() 142 143 return isOpen ? ( 144 <FullWindowOverlay> 145 <View 146 style={[ 147 a.fixed, 148 a.inset_0, 149 // setting a zIndex when using FullWindowOverlay on iOS 150 // means the taps pass straight through to the underlying content (???) 151 // so don't set it on iOS. FullWindowOverlay already does the job. 152 !IS_IOS && {zIndex: 9999}, 153 t.atoms.bg, 154 gtMobile ? a.p_2xl : a.p_xl, 155 a.align_center, 156 // @ts-ignore 157 platform({ 158 web: { 159 paddingTop: '35vh', 160 }, 161 default: { 162 paddingTop: Dimensions.get('window').height * 0.35, 163 }, 164 }), 165 ]}> 166 <View 167 role="dialog" 168 aria-role="dialog" 169 aria-label={_(msg`Verifying your age assurance status`)}> 170 <View style={[a.pb_3xl, {width: 300}]}> 171 <Inner /> 172 </View> 173 </View> 174 </View> 175 </FullWindowOverlay> 176 ) : null 177} 178 179function Inner() { 180 const t = useTheme() 181 const ax = useAnalytics() 182 const {_} = useLingui() 183 const agent = useAgent() 184 const polling = useRef(false) 185 const unmounted = useRef(false) 186 const [error, setError] = useState(false) 187 const [success, setSuccess] = useState(false) 188 const {close} = useRedirectOverlayContext() 189 190 useEffect(() => { 191 if (polling.current) return 192 193 polling.current = true 194 195 ax.metric('ageAssurance:redirectDialogOpen', {}) 196 197 wait( 198 3e3, 199 retry( 200 5, 201 () => true, 202 async () => { 203 if (!agent.session) return 204 if (unmounted.current) return 205 206 const data = await refetchAgeAssuranceServerState({agent}) 207 208 if (data?.state.status !== 'assured') { 209 throw new Error( 210 `Polling for age assurance state did not receive assured status`, 211 ) 212 } 213 214 return data 215 }, 216 1e3, 217 ), 218 ) 219 .then(async data => { 220 if (!data) return 221 if (!agent.session) return 222 if (unmounted.current) return 223 224 setSuccess(true) 225 226 ax.metric('ageAssurance:redirectDialogSuccess', {}) 227 }) 228 .catch(() => { 229 if (unmounted.current) return 230 setError(true) 231 ax.metric('ageAssurance:redirectDialogFail', {}) 232 }) 233 234 return () => { 235 unmounted.current = true 236 } 237 }, [ax, agent]) 238 239 if (success) { 240 return ( 241 <> 242 <View style={[a.align_start, a.w_full]}> 243 <AgeAssuranceBadge /> 244 245 <View 246 style={[ 247 a.flex_row, 248 a.justify_between, 249 a.align_center, 250 a.gap_sm, 251 a.pt_lg, 252 a.pb_md, 253 ]}> 254 <SuccessIcon size="sm" fill={t.palette.positive_500} /> 255 <Text style={[a.text_3xl, a.font_bold]}> 256 <Trans>Success</Trans> 257 </Text> 258 </View> 259 260 <Text style={[a.text_md, a.leading_snug]}> 261 <Trans> 262 We've confirmed your age assurance status. You can now close this 263 dialog. 264 </Trans> 265 </Text> 266 267 <View style={[a.w_full, a.pt_lg]}> 268 <Button 269 label={_(msg`Close`)} 270 size="large" 271 variant="solid" 272 color="secondary" 273 onPress={() => close()}> 274 <ButtonText> 275 <Trans>Close</Trans> 276 </ButtonText> 277 </Button> 278 </View> 279 </View> 280 </> 281 ) 282 } 283 284 return ( 285 <> 286 <View style={[a.align_start, a.w_full]}> 287 <AgeAssuranceBadge /> 288 289 <View 290 style={[ 291 a.flex_row, 292 a.justify_between, 293 a.align_center, 294 a.gap_sm, 295 a.pt_lg, 296 a.pb_md, 297 ]}> 298 {error && <ErrorIcon size="lg" fill={t.palette.negative_500} />} 299 300 <Text style={[a.text_3xl, a.font_bold]}> 301 {error ? <Trans>Connection issue</Trans> : <Trans>Verifying</Trans>} 302 </Text> 303 304 {!error && <Loader size="lg" />} 305 </View> 306 307 <Text style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}> 308 {error ? ( 309 <Trans> 310 We were unable to receive the verification due to a connection 311 issue. It may arrive later. If it does, your account will update 312 automatically. 313 </Trans> 314 ) : ( 315 <Trans> 316 We're confirming your age assurance status with our servers. This 317 should only take a few seconds. 318 </Trans> 319 )} 320 </Text> 321 322 {error && ( 323 <View style={[a.w_full, a.pt_lg]}> 324 <Button 325 label={_(msg`Close`)} 326 size="large" 327 variant="solid" 328 color="secondary" 329 onPress={() => close()}> 330 <ButtonText> 331 <Trans>Close</Trans> 332 </ButtonText> 333 </Button> 334 </View> 335 )} 336 </View> 337 </> 338 ) 339}