Bluesky app fork with some witchin' additions 馃挮
at linkat-integration 190 lines 5.2 kB view raw
1import React, {useEffect, useState} from 'react' 2import {ActivityIndicator, Platform, View} from 'react-native' 3import ReactNativeDeviceAttest from 'react-native-device-attest' 4import {msg} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6import {nanoid} from 'nanoid/non-secure' 7 8import {createFullHandle} from '#/lib/strings/handles' 9import {logger} from '#/logger' 10import {useSignupContext} from '#/screens/Signup/state' 11import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView' 12import {atoms as a, useTheme} from '#/alf' 13import {FormError} from '#/components/forms/FormError' 14import {useAnalytics} from '#/analytics' 15import {GCP_PROJECT_ID, IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env' 16import {BackNextButtons} from '../BackNextButtons' 17 18const CAPTCHA_PATH = 19 IS_WEB || GCP_PROJECT_ID === 0 20 ? '/gate/signup' 21 : '/gate/signup/attempt-attest' 22 23export function StepCaptcha() { 24 if (IS_WEB) { 25 return <StepCaptchaInner /> 26 } else { 27 return <StepCaptchaNative /> 28 } 29} 30 31export function StepCaptchaNative() { 32 const [token, setToken] = useState<string>() 33 const [payload, setPayload] = useState<string>() 34 const [ready, setReady] = useState(false) 35 36 useEffect(() => { 37 ;(async () => { 38 logger.debug('trying to generate attestation token...') 39 try { 40 if (IS_IOS) { 41 logger.debug('starting to generate devicecheck token...') 42 const token = await ReactNativeDeviceAttest.getDeviceCheckToken() 43 setToken(token) 44 logger.debug(`generated devicecheck token: ${token}`) 45 } else { 46 const {token, payload} = 47 await ReactNativeDeviceAttest.getIntegrityToken('signup') 48 setToken(token) 49 setPayload(base64UrlEncode(payload)) 50 } 51 } catch (e: any) { 52 logger.error(e) 53 } finally { 54 setReady(true) 55 } 56 })() 57 }, []) 58 59 if (!ready) { 60 return <View /> 61 } 62 63 return <StepCaptchaInner token={token} payload={payload} /> 64} 65 66function StepCaptchaInner({ 67 token, 68 payload, 69}: { 70 token?: string 71 payload?: string 72}) { 73 const t = useTheme() 74 const {_} = useLingui() 75 const ax = useAnalytics() 76 const theme = useTheme() 77 const {state, dispatch} = useSignupContext() 78 79 const [completed, setCompleted] = React.useState(false) 80 81 const stateParam = React.useMemo(() => nanoid(15), []) 82 const url = React.useMemo(() => { 83 const newUrl = new URL(state.serviceUrl) 84 newUrl.pathname = CAPTCHA_PATH 85 newUrl.searchParams.set( 86 'handle', 87 createFullHandle(state.handle, state.userDomain), 88 ) 89 newUrl.searchParams.set('state', stateParam) 90 newUrl.searchParams.set('colorScheme', theme.name) 91 92 if (IS_NATIVE && token) { 93 newUrl.searchParams.set('platform', Platform.OS) 94 newUrl.searchParams.set('token', token) 95 if (IS_ANDROID && payload) { 96 newUrl.searchParams.set('payload', payload) 97 } 98 } 99 100 return newUrl.href 101 }, [ 102 state.serviceUrl, 103 state.handle, 104 state.userDomain, 105 stateParam, 106 theme.name, 107 token, 108 payload, 109 ]) 110 111 const onSuccess = React.useCallback( 112 (code: string) => { 113 setCompleted(true) 114 ax.metric('signup:captchaSuccess', {}) 115 dispatch({ 116 type: 'submit', 117 task: {verificationCode: code, mutableProcessed: false}, 118 }) 119 }, 120 [ax, dispatch], 121 ) 122 123 const onError = React.useCallback( 124 (error?: unknown) => { 125 dispatch({ 126 type: 'setError', 127 value: _(msg`Error receiving captcha response.`), 128 }) 129 ax.metric('signup:captchaFailure', {}) 130 logger.error('Signup Flow Error', { 131 registrationHandle: state.handle, 132 error, 133 }) 134 }, 135 [_, ax, dispatch, state.handle], 136 ) 137 138 const onBackPress = React.useCallback(() => { 139 logger.error('Signup Flow Error', { 140 errorMessage: 141 'User went back from captcha step. Possibly encountered an error.', 142 registrationHandle: state.handle, 143 }) 144 145 dispatch({type: 'prev'}) 146 }, [dispatch, state.handle]) 147 148 return ( 149 <> 150 <View style={[a.gap_lg, a.pt_lg]}> 151 <View 152 style={[ 153 a.w_full, 154 a.overflow_hidden, 155 {minHeight: 510}, 156 completed && [a.align_center, a.justify_center], 157 ]}> 158 {!completed ? ( 159 <CaptchaWebView 160 url={url} 161 stateParam={stateParam} 162 state={state} 163 onComplete={() => setCompleted(true)} 164 onSuccess={onSuccess} 165 onError={onError} 166 /> 167 ) : ( 168 <ActivityIndicator size="large" color={t.palette.primary_500} /> 169 )} 170 </View> 171 <FormError error={state.error} /> 172 </View> 173 <BackNextButtons 174 hideNext 175 isLoading={state.isLoading} 176 onBackPress={onBackPress} 177 /> 178 </> 179 ) 180} 181 182function base64UrlEncode(data: string): string { 183 const encoder = new TextEncoder() 184 const bytes = encoder.encode(data) 185 186 const binaryString = String.fromCharCode(...bytes) 187 const base64 = btoa(binaryString) 188 189 return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]/g, '') 190}