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

Configure Feed

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

at c906fea77adb2daad28a521f06e68d5bbc4bce4d 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 {useAgent, useSession} from '#/state/session' 19import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 20import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 21import {Button, ButtonText} from '#/components/Button' 22import {FullWindowOverlay} from '#/components/FullWindowOverlay' 23import {CheckThick_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check' 24import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' 25import {Loader} from '#/components/Loader' 26import {Text} from '#/components/Typography' 27import {refetchAgeAssuranceServerState} from '#/ageAssurance' 28import {useAnalytics} from '#/analytics' 29import {IS_IOS, IS_WEB} from '#/env' 30 31export type RedirectOverlayState = { 32 result: 'success' | 'unknown' 33 actorDid: string 34} 35 36/** 37 * Validate and parse the query parameters returned from the age assurance 38 * redirect. If not valid, returns `undefined` and the dialog will not open. 39 */ 40export function parseRedirectOverlayState( 41 state: { 42 result?: string 43 actorDid?: string 44 } = {}, 45): RedirectOverlayState | undefined { 46 let result: RedirectOverlayState['result'] = 'unknown' 47 const actorDid = state.actorDid 48 49 switch (state.result) { 50 case 'success': 51 result = 'success' 52 break 53 case 'unknown': 54 default: 55 result = 'unknown' 56 break 57 } 58 59 if (actorDid) { 60 return { 61 result, 62 actorDid, 63 } 64 } 65} 66 67const Context = createContext<{ 68 isOpen: boolean 69 open: (state: RedirectOverlayState) => void 70 close: () => void 71}>({ 72 isOpen: false, 73 open: () => {}, 74 close: () => {}, 75}) 76 77export function useRedirectOverlayContext() { 78 return useContext(Context) 79} 80 81export function Provider({children}: {children?: React.ReactNode}) { 82 const {currentAccount} = useSession() 83 const incomingUrl = Linking.useLinkingURL() 84 const [state, setState] = useState<RedirectOverlayState | null>(() => { 85 if (!incomingUrl) return null 86 const url = parseLinkingUrl(incomingUrl) 87 if (url.pathname !== '/intent/age-assurance') return null 88 const params = url.searchParams 89 const state = parseRedirectOverlayState({ 90 result: params.get('result') ?? undefined, 91 actorDid: params.get('actorDid') ?? undefined, 92 }) 93 94 if (IS_WEB) { 95 // Clear the URL parameters so they don't re-trigger 96 history.pushState(null, '', '/') 97 } 98 99 /* 100 * If we don't have an account or the account doesn't match, do 101 * nothing. By the time the user switches to their other account, AA 102 * state should be ready for them. 103 */ 104 if (state && currentAccount && state.actorDid === currentAccount.did) { 105 return state 106 } 107 108 return null 109 }) 110 const open = useCallback((state: RedirectOverlayState) => { 111 setState(state) 112 }, []) 113 const close = useCallback(() => { 114 setState(null) 115 }, []) 116 117 return ( 118 <Context.Provider 119 value={useMemo( 120 () => ({ 121 isOpen: state !== null, 122 open, 123 close, 124 }), 125 [state, open, close], 126 )}> 127 {children} 128 </Context.Provider> 129 ) 130} 131 132export function RedirectOverlay() { 133 const t = useTheme() 134 const {_} = useLingui() 135 const {isOpen} = useRedirectOverlayContext() 136 const {gtMobile} = useBreakpoints() 137 138 return isOpen ? ( 139 <FullWindowOverlay> 140 <View 141 style={[ 142 a.fixed, 143 a.inset_0, 144 // setting a zIndex when using FullWindowOverlay on iOS 145 // means the taps pass straight through to the underlying content (???) 146 // so don't set it on iOS. FullWindowOverlay already does the job. 147 !IS_IOS && {zIndex: 9999}, 148 t.atoms.bg, 149 gtMobile ? a.p_2xl : a.p_xl, 150 a.align_center, 151 // @ts-ignore 152 platform({ 153 web: { 154 paddingTop: '35vh', 155 }, 156 default: { 157 paddingTop: Dimensions.get('window').height * 0.35, 158 }, 159 }), 160 ]}> 161 <View 162 role="dialog" 163 aria-role="dialog" 164 aria-label={_(msg`Verifying your age assurance status`)}> 165 <View style={[a.pb_3xl, {width: 300}]}> 166 <Inner /> 167 </View> 168 </View> 169 </View> 170 </FullWindowOverlay> 171 ) : null 172} 173 174function Inner() { 175 const t = useTheme() 176 const ax = useAnalytics() 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 ax.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 ax.metric('ageAssurance:redirectDialogSuccess', {}) 222 }) 223 .catch(() => { 224 if (unmounted.current) return 225 setError(true) 226 ax.metric('ageAssurance:redirectDialogFail', {}) 227 }) 228 229 return () => { 230 unmounted.current = true 231 } 232 }, [ax, 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}