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 {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}