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 } else { return } } export function StepCaptchaNative() { const [token, setToken] = useState() const [payload, setPayload] = useState() 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 } return } 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 ( <> {!completed ? ( setCompleted(true)} onSuccess={onSuccess} onError={onError} /> ) : ( )} {state.error && {state.error}} ) } 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, '') }