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

Configure Feed

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

at df4e888347f1935d9d98de4bb9d7a3751f3b9a2c 334 lines 8.8 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, Trans} from '@lingui/macro' 13import {useLingui} from '@lingui/react' 14 15import {retry} from '#/lib/async/retry' 16import {wait} from '#/lib/async/wait' 17import {parseLinkingUrl} from '#/lib/parseLinkingUrl' 18import {isWeb} from '#/platform/detection' 19import {isIOS} from '#/platform/detection' 20import {useAgent, useSession} from '#/state/session' 21import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 22import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 23import {Button, ButtonText} from '#/components/Button' 24import {FullWindowOverlay} from '#/components/FullWindowOverlay' 25import {CheckThick_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check' 26import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' 27import {Loader} from '#/components/Loader' 28import {Text} from '#/components/Typography' 29import {refetchAgeAssuranceServerState} from '#/ageAssurance' 30import {logger} from '#/ageAssurance' 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 state = parseRedirectOverlayState({ 91 result: params.get('result') ?? undefined, 92 actorDid: params.get('actorDid') ?? undefined, 93 }) 94 95 if (isWeb) { 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 (state && currentAccount && state.actorDid === currentAccount.did) { 106 return state 107 } 108 109 return null 110 }) 111 const open = useCallback((state: RedirectOverlayState) => { 112 setState(state) 113 }, []) 114 const close = useCallback(() => { 115 setState(null) 116 }, []) 117 118 return ( 119 <Context.Provider 120 value={useMemo( 121 () => ({ 122 isOpen: state !== null, 123 open, 124 close, 125 }), 126 [state, open, close], 127 )}> 128 {children} 129 </Context.Provider> 130 ) 131} 132 133export function RedirectOverlay() { 134 const t = useTheme() 135 const {_} = useLingui() 136 const {isOpen} = useRedirectOverlayContext() 137 const {gtMobile} = useBreakpoints() 138 139 return isOpen ? ( 140 <FullWindowOverlay> 141 <View 142 style={[ 143 a.fixed, 144 a.inset_0, 145 // setting a zIndex when using FullWindowOverlay on iOS 146 // means the taps pass straight through to the underlying content (???) 147 // so don't set it on iOS. FullWindowOverlay already does the job. 148 !isIOS && {zIndex: 9999}, 149 t.atoms.bg, 150 gtMobile ? a.p_2xl : a.p_xl, 151 a.align_center, 152 // @ts-ignore 153 platform({ 154 web: { 155 paddingTop: '35vh', 156 }, 157 default: { 158 paddingTop: Dimensions.get('window').height * 0.35, 159 }, 160 }), 161 ]}> 162 <View 163 role="dialog" 164 aria-role="dialog" 165 aria-label={_(msg`Verifying your age assurance status`)}> 166 <View style={[a.pb_3xl, {width: 300}]}> 167 <Inner /> 168 </View> 169 </View> 170 </View> 171 </FullWindowOverlay> 172 ) : null 173} 174 175function Inner() { 176 const t = useTheme() 177 const {_} = useLingui() 178 const agent = useAgent() 179 const polling = useRef(false) 180 const unmounted = useRef(false) 181 const [error, setError] = useState(false) 182 const [success, setSuccess] = useState(false) 183 const {close} = useRedirectOverlayContext() 184 185 useEffect(() => { 186 if (polling.current) return 187 188 polling.current = true 189 190 logger.metric('ageAssurance:redirectDialogOpen', {}) 191 192 wait( 193 3e3, 194 retry( 195 5, 196 () => true, 197 async () => { 198 if (!agent.session) return 199 if (unmounted.current) return 200 201 const data = await refetchAgeAssuranceServerState({agent}) 202 203 if (data?.state.status !== 'assured') { 204 throw new Error( 205 `Polling for age assurance state did not receive assured status`, 206 ) 207 } 208 209 return data 210 }, 211 1e3, 212 ), 213 ) 214 .then(async data => { 215 if (!data) return 216 if (!agent.session) return 217 if (unmounted.current) return 218 219 setSuccess(true) 220 221 logger.metric('ageAssurance:redirectDialogSuccess', {}) 222 }) 223 .catch(() => { 224 if (unmounted.current) return 225 setError(true) 226 logger.metric('ageAssurance:redirectDialogFail', {}) 227 }) 228 229 return () => { 230 unmounted.current = true 231 } 232 }, [agent]) 233 234 if (success) { 235 return ( 236 <> 237 <View style={[a.align_start, a.w_full]}> 238 <AgeAssuranceBadge /> 239 240 <View 241 style={[ 242 a.flex_row, 243 a.justify_between, 244 a.align_center, 245 a.gap_sm, 246 a.pt_lg, 247 a.pb_md, 248 ]}> 249 <SuccessIcon size="sm" fill={t.palette.positive_500} /> 250 <Text style={[a.text_3xl, a.font_bold]}> 251 <Trans>Success</Trans> 252 </Text> 253 </View> 254 255 <Text style={[a.text_md, a.leading_snug]}> 256 <Trans> 257 We've confirmed your age assurance status. You can now close this 258 dialog. 259 </Trans> 260 </Text> 261 262 <View style={[a.w_full, a.pt_lg]}> 263 <Button 264 label={_(msg`Close`)} 265 size="large" 266 variant="solid" 267 color="secondary" 268 onPress={() => close()}> 269 <ButtonText> 270 <Trans>Close</Trans> 271 </ButtonText> 272 </Button> 273 </View> 274 </View> 275 </> 276 ) 277 } 278 279 return ( 280 <> 281 <View style={[a.align_start, a.w_full]}> 282 <AgeAssuranceBadge /> 283 284 <View 285 style={[ 286 a.flex_row, 287 a.justify_between, 288 a.align_center, 289 a.gap_sm, 290 a.pt_lg, 291 a.pb_md, 292 ]}> 293 {error && <ErrorIcon size="lg" fill={t.palette.negative_500} />} 294 295 <Text style={[a.text_3xl, a.font_bold]}> 296 {error ? <Trans>Connection issue</Trans> : <Trans>Verifying</Trans>} 297 </Text> 298 299 {!error && <Loader size="lg" />} 300 </View> 301 302 <Text style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}> 303 {error ? ( 304 <Trans> 305 We were unable to receive the verification due to a connection 306 issue. It may arrive later. If it does, your account will update 307 automatically. 308 </Trans> 309 ) : ( 310 <Trans> 311 We're confirming your age assurance status with our servers. This 312 should only take a few seconds. 313 </Trans> 314 )} 315 </Text> 316 317 {error && ( 318 <View style={[a.w_full, a.pt_lg]}> 319 <Button 320 label={_(msg`Close`)} 321 size="large" 322 variant="solid" 323 color="secondary" 324 onPress={() => close()}> 325 <ButtonText> 326 <Trans>Close</Trans> 327 </ButtonText> 328 </Button> 329 </View> 330 )} 331 </View> 332 </> 333 ) 334}