diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index acf7fe619..d71cdddc7 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -83,6 +83,7 @@
"react-native-ios-context-menu": "3.1.0",
"react-native-ios-utilities": "5.1.1",
"react-native-keyboard-controller": "1.16.3",
+ "react-native-otp-entry": "1.8.2",
"react-native-pager-view": "6.7.0",
"react-native-reanimated": "3.16.7",
"react-native-root-siblings": "5.0.1",
diff --git a/apps/mobile/src/components/common/SubmitButton.tsx b/apps/mobile/src/components/common/SubmitButton.tsx
new file mode 100644
index 000000000..44aa29614
--- /dev/null
+++ b/apps/mobile/src/components/common/SubmitButton.tsx
@@ -0,0 +1,49 @@
+import { cn } from "@follow/utils"
+import { useEffect } from "react"
+import type { PressableProps } from "react-native"
+import { ActivityIndicator, Text } from "react-native"
+import {
+ interpolate,
+ interpolateColor,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from "react-native-reanimated"
+import { useColor } from "react-native-uikit-colors"
+
+import { accentColor } from "@/src/theme/colors"
+
+import { ReAnimatedPressable } from "./AnimatedComponents"
+
+export function SubmitButton({
+ isLoading,
+ title,
+ ...props
+}: PressableProps & { isLoading?: boolean; title: string }) {
+ const disableColor = useColor("gray3")
+
+ const disabledValue = useSharedValue(1)
+ useEffect(() => {
+ disabledValue.value = withTiming(props.disabled ? 1 : 0)
+ }, [props.disabled])
+
+ const buttonStyle = useAnimatedStyle(() => ({
+ opacity: interpolate(disabledValue.value, [0, 1], [1, 0.5]),
+ backgroundColor: interpolateColor(disabledValue.value, [1, 0], [disableColor, accentColor]),
+ }))
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+ {title}
+ )}
+
+ )
+}
diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx
index ae89941c5..611860fa1 100644
--- a/apps/mobile/src/modules/login/email.tsx
+++ b/apps/mobile/src/modules/login/email.tsx
@@ -1,26 +1,19 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
import { router } from "expo-router"
-import { useContext, useEffect } from "react"
+import { useContext } from "react"
import type { Control } from "react-hook-form"
import { useController, useForm } from "react-hook-form"
import type { TextInputProps } from "react-native"
-import { ActivityIndicator, Text, TextInput, View } from "react-native"
+import { Text, TextInput, View } from "react-native"
import { KeyboardController } from "react-native-keyboard-controller"
-import {
- interpolate,
- interpolateColor,
- useAnimatedStyle,
- useSharedValue,
- withTiming,
-} from "react-native-reanimated"
import { z } from "zod"
-import { ReAnimatedPressable } from "@/src/components/common/AnimatedComponents"
+import { SubmitButton } from "@/src/components/common/SubmitButton"
import { LoginTermsCheckGuardContext } from "@/src/contexts/LoginTermsContext"
import { signIn } from "@/src/lib/auth"
import { toast } from "@/src/lib/toast"
-import { accentColor, useColor } from "@/src/theme/colors"
+import { accentColor } from "@/src/theme/colors"
const formSchema = z.object({
email: z.string().email(),
@@ -89,17 +82,6 @@ export function EmailLogin() {
termsCheckGuard?.(() => submitMutation.mutate(values))
})
- const disableColor = useColor("gray3")
-
- const canLogin = useSharedValue(1)
- useEffect(() => {
- canLogin.value = withTiming(submitMutation.isPending || !formState.isValid ? 1 : 0)
- }, [submitMutation.isPending, formState.isValid, canLogin])
- const buttonStyle = useAnimatedStyle(() => ({
- opacity: interpolate(canLogin.value, [0, 1], [1, 0.5]),
- backgroundColor: interpolateColor(canLogin.value, [1, 0], [disableColor, accentColor]),
- }))
-
return (
@@ -139,18 +121,13 @@ export function EmailLogin() {
/>
-
- {submitMutation.isPending ? (
-
- ) : (
- Continue
- )}
-
+ title="Continue"
+ className="mt-8"
+ />
)
}
diff --git a/apps/mobile/src/screens/(headless)/2fa.tsx b/apps/mobile/src/screens/(headless)/2fa.tsx
index 7fd74c8a7..0d0d37d0f 100644
--- a/apps/mobile/src/screens/(headless)/2fa.tsx
+++ b/apps/mobile/src/screens/(headless)/2fa.tsx
@@ -1,16 +1,16 @@
import { useMutation } from "@tanstack/react-query"
import { router } from "expo-router"
-import { useMemo, useState } from "react"
+import { useMemo, useRef } from "react"
import {
- ActivityIndicator,
Text,
- TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
useAnimatedValue,
View,
} from "react-native"
-import { KeyboardAvoidingView, KeyboardController } from "react-native-keyboard-controller"
+import { KeyboardController } from "react-native-keyboard-controller"
+import type { OtpInputRef } from "react-native-otp-entry"
+import { OtpInput } from "react-native-otp-entry"
import { useColor } from "react-native-uikit-colors"
import {
@@ -22,6 +22,7 @@ import { twoFactor } from "@/src/lib/auth"
import { queryClient } from "@/src/lib/query-client"
import { toast } from "@/src/lib/toast"
import { whoamiQueryKey } from "@/src/store/user/hooks"
+import { accentColor } from "@/src/theme/colors"
function isAuthCodeValid(authCode: string) {
return (
@@ -32,7 +33,8 @@ function isAuthCodeValid(authCode: string) {
export default function TwoFactorAuthScreen() {
const scrollY = useAnimatedValue(0)
const label = useColor("label")
- const [authCode, setAuthCode] = useState("")
+
+ const otpInputRef = useRef(null)
const submitMutation = useMutation({
mutationFn: async (value: string) => {
@@ -43,8 +45,8 @@ export default function TwoFactorAuthScreen() {
await queryClient.invalidateQueries({ queryKey: whoamiQueryKey })
},
onError(error) {
+ otpInputRef.current?.clear()
toast.error(`Failed to verify: ${error.message}`)
- setAuthCode("")
},
onSuccess() {
router.replace("/")
@@ -54,64 +56,49 @@ export default function TwoFactorAuthScreen() {
return (
({ scrollY }), [scrollY])}>
-
- {
- return (
- router.back()}>
-
-
- )
- }}
- />
- {
- KeyboardController.dismiss()
- }}
- accessible={false}
- >
-
-
-
- Verify with your authenticator app
-
-
-
-
- Enter Follow Auth Code
-
-
-
-
-
-
-
- {
- submitMutation.mutate(authCode)
- }}
- >
- {submitMutation.isPending ? (
-
- ) : (
- Submit
- )}
+ {
+ return (
+ router.back()}>
+
+ )
+ }}
+ />
+ {
+ KeyboardController.dismiss()
+ }}
+ accessible={false}
+ >
+
+
+
+ Verify with your authenticator app
+
-
-
+
+ {
+ if (isAuthCodeValid(code)) {
+ submitMutation.mutate(code)
+ }
+ }}
+ />
+
+
)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2540f6c2c..21e37b267 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -626,6 +626,9 @@ importers:
react-native-keyboard-controller:
specifier: 1.16.3
version: 1.16.3(react-native-reanimated@3.16.7(@babel/core@7.26.9)(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
+ react-native-otp-entry:
+ specifier: 1.8.2
+ version: 1.8.2(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
react-native-pager-view:
specifier: 6.7.0
version: 6.7.0(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
@@ -720,7 +723,7 @@ importers:
devDependencies:
expo-module-scripts:
specifier: 4.0.4
- version: 4.0.4(v6ktukj2nf4iuhjuuz6cntfrdu)
+ version: 4.0.4(3ecsnzozibljdasytj3d6ieu7e)
apps/mobile/web-app: {}
@@ -12507,6 +12510,12 @@ packages:
react-native: '*'
react-native-reanimated: '>=3.0.0'
+ react-native-otp-entry@1.8.2:
+ resolution: {integrity: sha512-Ergs1fNpyp69oSrQ3k/PoolBpuutXruwhQDnEmX4xT3RnaZyo9lvcRsj2FafHanbFvghn/v3WcFRGU/GIhIMqQ==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
react-native-pager-view@6.7.0:
resolution: {integrity: sha512-sutxKiMqBuQrEyt4mLaLNzy8taIC7IuYpxfcwQBXfSYBSSpAa0qE9G1FXlP/iXqTSlFgBXyK7BESsl9umOjECQ==}
peerDependencies:
@@ -24450,7 +24459,7 @@ snapshots:
expo: 52.0.35(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@expo/metro-runtime@4.0.1(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.13.2(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)(utf-8-validate@6.0.5)
react-native: 0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)
- expo-module-scripts@4.0.4(v6ktukj2nf4iuhjuuz6cntfrdu):
+ expo-module-scripts@4.0.4(3ecsnzozibljdasytj3d6ieu7e):
dependencies:
'@babel/cli': 7.26.4(@babel/core@7.26.9)
'@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.9)
@@ -24465,11 +24474,11 @@ snapshots:
commander: 12.1.0
eslint-config-universe: 14.0.0(@types/eslint@9.6.1)(eslint@9.20.1(jiti@2.4.2))(prettier@3.5.1)(typescript@5.7.3)
glob: 10.4.5
- jest-expo: 52.0.4(2en54dacn5jatfqxv77ym5375i)
+ jest-expo: 52.0.4(jneefgvk25es77gurrpifo7wzy)
jest-snapshot-prettier: prettier@2.8.8
jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)))
resolve-workspace-root: 2.0.0
- ts-jest: 29.0.5(@babel/core@7.26.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.9))(jest@29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)))(typescript@5.7.3)
+ ts-jest: 29.0.5(@babel/core@7.26.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)))(typescript@5.7.3)
typescript: 5.7.3
transitivePeerDependencies:
- '@babel/core'
@@ -26305,7 +26314,7 @@ snapshots:
jest-mock: 29.7.0
jest-util: 29.7.0
- jest-expo@52.0.4(2en54dacn5jatfqxv77ym5375i):
+ jest-expo@52.0.4(jneefgvk25es77gurrpifo7wzy):
dependencies:
'@expo/config': 10.0.10
'@expo/json-file': 9.0.2
@@ -26322,7 +26331,7 @@ snapshots:
json5: 2.2.3
lodash: 4.17.21
react-native: 0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)
- react-server-dom-webpack: 19.0.0-rc-6230622a1a-20240610(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.98.0)
+ react-server-dom-webpack: 19.0.0-rc-6230622a1a-20240610(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.98.0(esbuild@0.24.2))
react-test-renderer: 18.3.1(react@18.3.1)
server-only: 0.0.1
stacktrace-js: 2.0.2
@@ -29331,6 +29340,11 @@ snapshots:
react-native-is-edge-to-edge: 1.1.6(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
react-native-reanimated: 3.16.7(@babel/core@7.26.9)(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
+ react-native-otp-entry@1.8.2(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-native: 0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)
+
react-native-pager-view@6.7.0(react-native@0.77.0(@babel/core@7.26.9)(@babel/preset-env@7.26.9(@babel/core@7.26.9))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1):
dependencies:
react: 18.3.1
@@ -29549,13 +29563,13 @@ snapshots:
dependencies:
selecto: 1.26.3
- react-server-dom-webpack@19.0.0-rc-6230622a1a-20240610(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.98.0):
+ react-server-dom-webpack@19.0.0-rc-6230622a1a-20240610(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.98.0(esbuild@0.24.2)):
dependencies:
acorn-loose: 8.4.0
neo-async: 2.6.2
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
- webpack: 5.98.0
+ webpack: 5.98.0(esbuild@0.24.2)
react-shadow@20.6.0(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
@@ -30979,14 +30993,16 @@ snapshots:
ansi-escapes: 4.3.2
supports-hyperlinks: 2.3.0
- terser-webpack-plugin@5.3.11(webpack@5.98.0):
+ terser-webpack-plugin@5.3.11(esbuild@0.24.2)(webpack@5.98.0(esbuild@0.24.2)):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
jest-worker: 27.5.1
schema-utils: 4.3.0
serialize-javascript: 6.0.2
terser: 5.39.0
- webpack: 5.98.0
+ webpack: 5.98.0(esbuild@0.24.2)
+ optionalDependencies:
+ esbuild: 0.24.2
terser@5.39.0:
dependencies:
@@ -31143,7 +31159,7 @@ snapshots:
ts-interface-checker@0.1.13: {}
- ts-jest@29.0.5(@babel/core@7.26.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.9))(jest@29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)))(typescript@5.7.3):
+ ts-jest@29.0.5(@babel/core@7.26.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)))(typescript@5.7.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
@@ -31159,6 +31175,7 @@ snapshots:
'@babel/core': 7.26.9
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.26.9)
+ esbuild: 0.24.2
ts-morph@12.0.0:
dependencies:
@@ -31878,7 +31895,7 @@ snapshots:
webpack-virtual-modules@0.6.2: {}
- webpack@5.98.0:
+ webpack@5.98.0(esbuild@0.24.2):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.6
@@ -31900,7 +31917,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 4.3.0
tapable: 2.2.1
- terser-webpack-plugin: 5.3.11(webpack@5.98.0)
+ terser-webpack-plugin: 5.3.11(esbuild@0.24.2)(webpack@5.98.0(esbuild@0.24.2))
watchpack: 2.4.2
webpack-sources: 3.2.3
transitivePeerDependencies: