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

Configure Feed

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

at ece6dc251cdb7eaf260819a4005b3a3e3e74ac8b 335 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} 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 state = 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 (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 !IS_IOS && {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 ax = useAnalytics() 178 const {_} = useLingui() 179 const agent = useAgent() 180 const polling = useRef(false) 181 const unmounted = useRef(false) 182 const [error, setError] = useState(false) 183 const [success, setSuccess] = useState(false) 184 const {close} = useRedirectOverlayContext() 185 186 useEffect(() => { 187 if (polling.current) return 188 189 polling.current = true 190 191 ax.metric('ageAssurance:redirectDialogOpen', {}) 192 193 wait( 194 3e3, 195 retry( 196 5, 197 () => true, 198 async () => { 199 if (!agent.session) return 200 if (unmounted.current) return 201 202 const data = await refetchAgeAssuranceServerState({agent}) 203 204 if (data?.state.status !== 'assured') { 205 throw new Error( 206 `Polling for age assurance state did not receive assured status`, 207 ) 208 } 209 210 return data 211 }, 212 1e3, 213 ), 214 ) 215 .then(async data => { 216 if (!data) return 217 if (!agent.session) return 218 if (unmounted.current) return 219 220 setSuccess(true) 221 222 ax.metric('ageAssurance:redirectDialogSuccess', {}) 223 }) 224 .catch(() => { 225 if (unmounted.current) return 226 setError(true) 227 ax.metric('ageAssurance:redirectDialogFail', {}) 228 }) 229 230 return () => { 231 unmounted.current = true 232 } 233 }, [ax, agent]) 234 235 if (success) { 236 return ( 237 <> 238 <View style={[a.align_start, a.w_full]}> 239 <AgeAssuranceBadge /> 240 241 <View 242 style={[ 243 a.flex_row, 244 a.justify_between, 245 a.align_center, 246 a.gap_sm, 247 a.pt_lg, 248 a.pb_md, 249 ]}> 250 <SuccessIcon size="sm" fill={t.palette.positive_500} /> 251 <Text style={[a.text_3xl, a.font_bold]}> 252 <Trans>Success</Trans> 253 </Text> 254 </View> 255 256 <Text style={[a.text_md, a.leading_snug]}> 257 <Trans> 258 We've confirmed your age assurance status. You can now close this 259 dialog. 260 </Trans> 261 </Text> 262 263 <View style={[a.w_full, a.pt_lg]}> 264 <Button 265 label={_(msg`Close`)} 266 size="large" 267 variant="solid" 268 color="secondary" 269 onPress={() => close()}> 270 <ButtonText> 271 <Trans>Close</Trans> 272 </ButtonText> 273 </Button> 274 </View> 275 </View> 276 </> 277 ) 278 } 279 280 return ( 281 <> 282 <View style={[a.align_start, a.w_full]}> 283 <AgeAssuranceBadge /> 284 285 <View 286 style={[ 287 a.flex_row, 288 a.justify_between, 289 a.align_center, 290 a.gap_sm, 291 a.pt_lg, 292 a.pb_md, 293 ]}> 294 {error && <ErrorIcon size="lg" fill={t.palette.negative_500} />} 295 296 <Text style={[a.text_3xl, a.font_bold]}> 297 {error ? <Trans>Connection issue</Trans> : <Trans>Verifying</Trans>} 298 </Text> 299 300 {!error && <Loader size="lg" />} 301 </View> 302 303 <Text style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}> 304 {error ? ( 305 <Trans> 306 We were unable to receive the verification due to a connection 307 issue. It may arrive later. If it does, your account will update 308 automatically. 309 </Trans> 310 ) : ( 311 <Trans> 312 We're confirming your age assurance status with our servers. This 313 should only take a few seconds. 314 </Trans> 315 )} 316 </Text> 317 318 {error && ( 319 <View style={[a.w_full, a.pt_lg]}> 320 <Button 321 label={_(msg`Close`)} 322 size="large" 323 variant="solid" 324 color="secondary" 325 onPress={() => close()}> 326 <ButtonText> 327 <Trans>Close</Trans> 328 </ButtonText> 329 </Button> 330 </View> 331 )} 332 </View> 333 </> 334 ) 335}