Files
bsky-social-app/src/screens/Signup/StepCaptcha/index.tsx
T
Samuel Newman c3dec0bf50 fix: stop grouping all signup failures into one sentry issue
Fixes APP-S3WR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 16:24:14 +03:00

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, '')
}