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