c3dec0bf50
Fixes APP-S3WR Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
185 lines
4.9 KiB
TypeScript
185 lines
4.9 KiB
TypeScript
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
import {ActivityIndicator, Platform, View} from 'react-native'
|
|
import ReactNativeDeviceAttest from 'react-native-device-attest'
|
|
import {msg} from '@lingui/core/macro'
|
|
import {useLingui} from '@lingui/react'
|
|
import {nanoid} from 'nanoid/non-secure'
|
|
|
|
import {createFullHandle} from '#/lib/strings/handles'
|
|
import {logger} from '#/logger'
|
|
import {useSignupContext} from '#/screens/Signup/state'
|
|
import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
|
|
import {atoms as a, useTheme} from '#/alf'
|
|
import {Admonition} from '#/components/Admonition'
|
|
import {useAnalytics} from '#/analytics'
|
|
import {GCP_PROJECT_ID, IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
|
|
import {BackNextButtons} from '../BackNextButtons'
|
|
|
|
const CAPTCHA_PATH =
|
|
IS_WEB || GCP_PROJECT_ID === 0
|
|
? '/gate/signup'
|
|
: '/gate/signup/attempt-attest'
|
|
|
|
export function StepCaptcha() {
|
|
if (IS_WEB) {
|
|
return <StepCaptchaInner />
|
|
} else {
|
|
return <StepCaptchaNative />
|
|
}
|
|
}
|
|
|
|
export function StepCaptchaNative() {
|
|
const [token, setToken] = useState<string>()
|
|
const [payload, setPayload] = useState<string>()
|
|
const [ready, setReady] = useState(false)
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
logger.debug('trying to generate attestation token...')
|
|
try {
|
|
if (IS_IOS) {
|
|
logger.debug('starting to generate devicecheck token...')
|
|
const token = await ReactNativeDeviceAttest.getDeviceCheckToken()
|
|
setToken(token)
|
|
logger.debug(`generated devicecheck token: ${token}`)
|
|
} else {
|
|
const {token, payload} =
|
|
await ReactNativeDeviceAttest.getIntegrityToken('signup')
|
|
setToken(token)
|
|
setPayload(base64UrlEncode(payload))
|
|
}
|
|
} catch (err) {
|
|
const e = err as Error
|
|
logger.error(e)
|
|
} finally {
|
|
setReady(true)
|
|
}
|
|
})()
|
|
}, [])
|
|
|
|
if (!ready) {
|
|
return <View />
|
|
}
|
|
|
|
return <StepCaptchaInner token={token} payload={payload} />
|
|
}
|
|
|
|
function StepCaptchaInner({
|
|
token,
|
|
payload,
|
|
}: {
|
|
token?: string
|
|
payload?: string
|
|
}) {
|
|
const {_} = useLingui()
|
|
const ax = useAnalytics()
|
|
const theme = useTheme()
|
|
const {state, dispatch} = useSignupContext()
|
|
|
|
const [completed, setCompleted] = useState(false)
|
|
|
|
const stateParam = useMemo(() => nanoid(15), [])
|
|
const url = useMemo(() => {
|
|
const newUrl = new URL(state.serviceUrl)
|
|
newUrl.pathname = CAPTCHA_PATH
|
|
newUrl.searchParams.set(
|
|
'handle',
|
|
createFullHandle(state.handle, state.userDomain),
|
|
)
|
|
newUrl.searchParams.set('state', stateParam)
|
|
newUrl.searchParams.set('colorScheme', theme.name)
|
|
|
|
if (IS_NATIVE && token) {
|
|
newUrl.searchParams.set('platform', Platform.OS)
|
|
newUrl.searchParams.set('token', token)
|
|
if (IS_ANDROID && payload) {
|
|
newUrl.searchParams.set('payload', payload)
|
|
}
|
|
}
|
|
|
|
return newUrl.href
|
|
}, [
|
|
state.serviceUrl,
|
|
state.handle,
|
|
state.userDomain,
|
|
stateParam,
|
|
theme.name,
|
|
token,
|
|
payload,
|
|
])
|
|
|
|
const onSuccess = useCallback(
|
|
(code: string) => {
|
|
setCompleted(true)
|
|
ax.metric('signup:captchaSuccess', {})
|
|
dispatch({
|
|
type: 'submit',
|
|
task: {verificationCode: code, mutableProcessed: false},
|
|
})
|
|
},
|
|
[ax, dispatch],
|
|
)
|
|
|
|
const onError = useCallback(
|
|
(error?: unknown) => {
|
|
dispatch({
|
|
type: 'setError',
|
|
value: _(msg`Error receiving captcha response.`),
|
|
})
|
|
ax.metric('signup:captchaFailure', {})
|
|
logger.error('Signup: captcha response error', {
|
|
safeMessage: error,
|
|
})
|
|
},
|
|
[_, ax, dispatch],
|
|
)
|
|
|
|
const onBackPress = useCallback(() => {
|
|
ax.metric('signup:captchaBackPress', {})
|
|
dispatch({type: 'prev'})
|
|
}, [ax, dispatch])
|
|
|
|
return (
|
|
<>
|
|
<View style={[a.gap_lg, a.pt_lg]}>
|
|
<View
|
|
style={[
|
|
a.w_full,
|
|
a.overflow_hidden,
|
|
{minHeight: 510},
|
|
completed && [a.align_center, a.justify_center],
|
|
]}>
|
|
{!completed ? (
|
|
<CaptchaWebView
|
|
url={url}
|
|
stateParam={stateParam}
|
|
state={state}
|
|
onComplete={() => setCompleted(true)}
|
|
onSuccess={onSuccess}
|
|
onError={onError}
|
|
/>
|
|
) : (
|
|
<ActivityIndicator size="large" />
|
|
)}
|
|
</View>
|
|
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
|
</View>
|
|
<BackNextButtons
|
|
hideNext
|
|
isLoading={state.isLoading}
|
|
onBackPress={onBackPress}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function base64UrlEncode(data: string): string {
|
|
const encoder = new TextEncoder()
|
|
const bytes = encoder.encode(data)
|
|
|
|
const binaryString = String.fromCharCode(...bytes)
|
|
const base64 = btoa(binaryString)
|
|
|
|
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]/g, '')
|
|
}
|