From e40f8a9bbdd439b4a482c35019f89158a09cff75 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:47:43 +0800 Subject: [PATCH] refactor(mobile): submit button & react-native-otp-entry (#2870) --- apps/mobile/package.json | 1 + .../src/components/common/SubmitButton.tsx | 49 ++++++++ apps/mobile/src/modules/login/email.tsx | 41 ++----- apps/mobile/src/screens/(headless)/2fa.tsx | 111 ++++++++---------- pnpm-lock.yaml | 43 +++++-- 5 files changed, 138 insertions(+), 107 deletions(-) create mode 100644 apps/mobile/src/components/common/SubmitButton.tsx 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: