forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useEffect, useMemo, useRef} from 'react'
2import {WebView, type WebViewNavigation} from 'react-native-webview'
3import {type ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes'
4
5import {type SignupState} from '#/screens/Signup/state'
6
7const ALLOWED_HOSTS = [
8 'witchsky.app',
9 'witchsky.social',
10 'bsky.social',
11 'bsky.app',
12 'staging.bsky.app',
13 'staging.bsky.dev',
14 'app.staging.bsky.dev',
15 'js.hcaptcha.com',
16 'newassets.hcaptcha.com',
17 'api2.hcaptcha.com',
18]
19
20const MIN_DELAY = 3_500
21
22export function CaptchaWebView({
23 url,
24 stateParam,
25 state,
26 onComplete,
27 onSuccess,
28 onError,
29}: {
30 url: string
31 stateParam: string
32 state?: SignupState
33 onComplete: () => void
34 onSuccess: (code: string) => void
35 onError: (error: unknown) => void
36}) {
37 const startedAt = useRef(Date.now())
38 const successTo = useRef<NodeJS.Timeout>(undefined)
39
40 useEffect(() => {
41 return () => {
42 if (successTo.current) {
43 clearTimeout(successTo.current)
44 }
45 }
46 }, [])
47
48 const redirectHost = useMemo(() => {
49 if (!state?.serviceUrl) return 'bsky.app'
50
51 return state?.serviceUrl &&
52 new URL(state?.serviceUrl).host === 'staging.bsky.dev'
53 ? 'app.staging.bsky.dev'
54 : 'witchsky.app'
55 }, [state?.serviceUrl])
56
57 const wasSuccessful = useRef(false)
58
59 const onShouldStartLoadWithRequest = (event: ShouldStartLoadRequest) => {
60 const urlp = new URL(event.url)
61 return ALLOWED_HOSTS.includes(urlp.host)
62 }
63
64 const onNavigationStateChange = (e: WebViewNavigation) => {
65 if (wasSuccessful.current) return
66
67 const urlp = new URL(e.url)
68 if (urlp.host !== redirectHost || urlp.pathname === '/gate/signup') return
69
70 const code = urlp.searchParams.get('code')
71 if (urlp.searchParams.get('state') !== stateParam || !code) {
72 onError({error: 'Invalid state or code'})
73 return
74 }
75
76 // We want to delay the completion of this screen ever so slightly so that it doesn't appear to be a glitch if it completes too fast
77 wasSuccessful.current = true
78 onComplete()
79 const now = Date.now()
80 const timeTaken = now - startedAt.current
81 if (timeTaken < MIN_DELAY) {
82 successTo.current = setTimeout(() => {
83 onSuccess(code)
84 }, MIN_DELAY - timeTaken)
85 } else {
86 onSuccess(code)
87 }
88 }
89
90 return (
91 <WebView
92 source={{uri: url}}
93 javaScriptEnabled
94 style={{
95 flex: 1,
96 backgroundColor: 'transparent',
97 borderRadius: 10,
98 }}
99 onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
100 onNavigationStateChange={onNavigationStateChange}
101 scrollEnabled={false}
102 onError={e => {
103 onError(e.nativeEvent)
104 }}
105 onHttpError={e => {
106 onError(e.nativeEvent)
107 }}
108 />
109 )
110}