about summary refs log tree commit diff
path: root/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx
diff options
context:
space:
mode:
authorhailey <me@haileyok.com>2025-08-07 12:33:38 -0700
committerGitHub <noreply@github.com>2025-08-07 12:33:38 -0700
commitc0593e49792af987b0c7accd6301f235d132030f (patch)
treeafc1faccd7686646d09567165b1f0ebe7849f7b7 /src/screens/Signup/StepCaptcha/CaptchaWebView.tsx
parent39e775a3768007df05ab91fe3ead39e36355b19a (diff)
downloadvoidsky-c0593e49792af987b0c7accd6301f235d132030f.tar.zst
Add device attestation to signup flow (#8757)
Diffstat (limited to 'src/screens/Signup/StepCaptcha/CaptchaWebView.tsx')
-rw-r--r--src/screens/Signup/StepCaptcha/CaptchaWebView.tsx88
1 files changed, 50 insertions, 38 deletions
diff --git a/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx b/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx
index caa0aa28a..27951d4cc 100644
--- a/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx
+++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx
@@ -1,20 +1,22 @@
-import React from 'react'
-import {StyleSheet} from 'react-native'
-import {WebView, WebViewNavigation} from 'react-native-webview'
-import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes'
+import {useEffect, useMemo, useRef} from 'react'
+import {WebView, type WebViewNavigation} from 'react-native-webview'
+import {type ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes'
 
-import {SignupState} from '#/screens/Signup/state'
+import {type SignupState} from '#/screens/Signup/state'
 
 const ALLOWED_HOSTS = [
   'bsky.social',
   'bsky.app',
   'staging.bsky.app',
   'staging.bsky.dev',
+  'app.staging.bsky.dev',
   'js.hcaptcha.com',
   'newassets.hcaptcha.com',
   'api2.hcaptcha.com',
 ]
 
+const MIN_DELAY = 3_500
+
 export function CaptchaWebView({
   url,
   stateParam,
@@ -28,49 +30,67 @@ export function CaptchaWebView({
   onSuccess: (code: string) => void
   onError: (error: unknown) => void
 }) {
-  const redirectHost = React.useMemo(() => {
+  const startedAt = useRef(Date.now())
+  const successTo = useRef<NodeJS.Timeout>()
+
+  useEffect(() => {
+    return () => {
+      if (successTo.current) {
+        clearTimeout(successTo.current)
+      }
+    }
+  }, [])
+
+  const redirectHost = useMemo(() => {
     if (!state?.serviceUrl) return 'bsky.app'
 
     return state?.serviceUrl &&
       new URL(state?.serviceUrl).host === 'staging.bsky.dev'
-      ? 'staging.bsky.app'
+      ? 'app.staging.bsky.dev'
       : 'bsky.app'
   }, [state?.serviceUrl])
 
-  const wasSuccessful = React.useRef(false)
+  const wasSuccessful = useRef(false)
 
-  const onShouldStartLoadWithRequest = React.useCallback(
-    (event: ShouldStartLoadRequest) => {
-      const urlp = new URL(event.url)
-      return ALLOWED_HOSTS.includes(urlp.host)
-    },
-    [],
-  )
+  const onShouldStartLoadWithRequest = (event: ShouldStartLoadRequest) => {
+    const urlp = new URL(event.url)
+    return ALLOWED_HOSTS.includes(urlp.host)
+  }
 
-  const onNavigationStateChange = React.useCallback(
-    (e: WebViewNavigation) => {
-      if (wasSuccessful.current) return
+  const onNavigationStateChange = (e: WebViewNavigation) => {
+    if (wasSuccessful.current) return
 
-      const urlp = new URL(e.url)
-      if (urlp.host !== redirectHost) return
+    const urlp = new URL(e.url)
+    if (urlp.host !== redirectHost || urlp.pathname === '/gate/signup') return
 
-      const code = urlp.searchParams.get('code')
-      if (urlp.searchParams.get('state') !== stateParam || !code) {
-        onError({error: 'Invalid state or code'})
-        return
-      }
+    const code = urlp.searchParams.get('code')
+    if (urlp.searchParams.get('state') !== stateParam || !code) {
+      onError({error: 'Invalid state or code'})
+      return
+    }
 
-      wasSuccessful.current = true
+    // 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
+    wasSuccessful.current = true
+    const now = Date.now()
+    const timeTaken = now - startedAt.current
+    if (timeTaken < MIN_DELAY) {
+      successTo.current = setTimeout(() => {
+        onSuccess(code)
+      }, MIN_DELAY - timeTaken)
+    } else {
       onSuccess(code)
-    },
-    [redirectHost, stateParam, onSuccess, onError],
-  )
+    }
+  }
 
   return (
     <WebView
       source={{uri: url}}
       javaScriptEnabled
-      style={styles.webview}
+      style={{
+        flex: 1,
+        backgroundColor: 'transparent',
+        borderRadius: 10,
+      }}
       onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
       onNavigationStateChange={onNavigationStateChange}
       scrollEnabled={false}
@@ -83,11 +103,3 @@ export function CaptchaWebView({
     />
   )
 }
-
-const styles = StyleSheet.create({
-  webview: {
-    flex: 1,
-    backgroundColor: 'transparent',
-    borderRadius: 10,
-  },
-})