From 1cb2cffb111a6c25e782a7a5b5c3acd12c1d45a6 Mon Sep 17 00:00:00 2001 From: ravdeep kainth Date: Tue, 3 Nov 2026 11:01:11 +1100 Subject: [PATCH 1/8] fix(mobile): Android secret requests no longer offer password manager logins The secret request field is a React Native TextInput, which can only set importantForAutofill=no. Newer Android still starts credential autofill for it, so Proton Pass offered the account login above the keyboard while typing a new admin password. Android now uses a small native EditText for this field that reports AUTOFILL_TYPE_NONE, the same opt-out the terminal input uses. iOS keeps the React Native TextInput. Co-Authored-By: Claude Sonnet 5.5 --- .../t3nativecontrols/T3SecretInputModule.kt | 120 ++++++++++++++++++ .../expo-module.config.json | 3 +- .../components/ui/SecretTextInput.android.tsx | 55 ++++++++ .../src/components/ui/SecretTextInput.tsx | 24 ++++ .../components/ui/SecretTextInput.types.ts | 8 ++ .../features/threads/SecretRequestCard.tsx | 13 +- vite.config.ts | 1 + 7 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt create mode 100644 apps/mobile/src/components/ui/SecretTextInput.android.tsx create mode 100644 apps/mobile/src/components/ui/SecretTextInput.tsx create mode 100644 apps/mobile/src/components/ui/SecretTextInput.types.ts diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt new file mode 100644 index 0000000000000..be112f42cf8e5 --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt @@ -0,0 +1,120 @@ +package expo.modules.t3nativecontrols + +import android.content.Context +import android.graphics.Color +import android.graphics.Typeface +import android.os.Build +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition +import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView + +class T3SecretInputModule : Module() { + override fun definition() = ModuleDefinition { + Name("T3SecretInput") + + View(T3SecretInputView::class) { + Prop("value") { view: T3SecretInputView, value: String -> view.setValue(value) } + Prop("placeholder") { view: T3SecretInputView, value: String -> + view.input.hint = value + } + Prop("textColor") { view: T3SecretInputView, value: Int -> view.input.setTextColor(value) } + Prop("placeholderColor") { view: T3SecretInputView, value: Int -> + view.input.setHintTextColor(value) + } + Prop("cursorColor") { view: T3SecretInputView, value: Int -> view.setCursorColor(value) } + Prop("fontSize") { view: T3SecretInputView, value: Float -> + view.input.setTextSize(TypedValue.COMPLEX_UNIT_DIP, value) + } + Prop("editable") { view: T3SecretInputView, value: Boolean -> view.input.isEnabled = value } + Events("onChangeText", "onSubmit") + } + } +} + +/** + * Masked single-line input for secret requests. React Native's own TextInput cannot opt out of + * credential autofill, so password managers offer logins above the keyboard; this view owns the + * EditText so it can. + */ +class T3SecretInputView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { + // Yoga sizes this view; the EditText inside is laid out by Android. + override val shouldUseAndroidLayout = true + + // Newer Android can request autofill even with importantForAutofill=no. + // Reporting no autofill type keeps this input out of credential autofill. + val input = object : EditText(context) { + override fun getAutofillType(): Int = View.AUTOFILL_TYPE_NONE + } + private val onChangeText by EventDispatcher() + private val onSubmit by EventDispatcher() + private var applyingValue = false + + init { + input.background = null + input.setPadding(0, 0, 0, 0) + input.gravity = Gravity.CENTER_VERTICAL or Gravity.START + input.importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO + input.imeOptions = EditorInfo.IME_ACTION_DONE + // Not setSingleLine(): that swaps out the password transformation. The password variation is + // already single line, and the keyboard keeps typed text out of suggestions. + input.inputType = + InputType.TYPE_CLASS_TEXT or + InputType.TYPE_TEXT_VARIATION_PASSWORD or + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + // The password variation switches the typeface to monospace. + input.typeface = Typeface.DEFAULT + input.setTextColor(Color.BLACK) + input.setHintTextColor(Color.GRAY) + input.addTextChangedListener( + object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + override fun afterTextChanged(s: Editable?) { + if (!applyingValue) onChangeText(mapOf("value" to (s?.toString() ?: ""))) + } + } + ) + input.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + onSubmit(emptyMap()) + true + } else { + false + } + } + addView( + input, + LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + ) + } + + /** Applies JS state without echoing it back, and without moving the caret on equal values. */ + fun setValue(value: String) { + if (input.text.toString() == value) return + applyingValue = true + input.setText(value) + input.setSelection(input.text.length) + applyingValue = false + } + + fun setCursorColor(color: Int) { + input.highlightColor = Color.argb(0x52, Color.red(color), Color.green(color), Color.blue(color)) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return + + input.textCursorDrawable = input.textCursorDrawable?.mutate()?.apply { setTint(color) } + input.textSelectHandle?.mutate()?.setTint(color) + input.textSelectHandleLeft?.mutate()?.setTint(color) + input.textSelectHandleRight?.mutate()?.setTint(color) + } +} diff --git a/apps/mobile/modules/t3-native-controls/expo-module.config.json b/apps/mobile/modules/t3-native-controls/expo-module.config.json index 5ff3009ef4d20..d405bf41cc335 100644 --- a/apps/mobile/modules/t3-native-controls/expo-module.config.json +++ b/apps/mobile/modules/t3-native-controls/expo-module.config.json @@ -13,7 +13,8 @@ "expo.modules.t3nativecontrols.T3NativeControlsModule", "expo.modules.t3nativecontrols.T3WorkspaceNavigationModule", "expo.modules.t3nativecontrols.T3KeyboardCommandsModule", - "expo.modules.t3nativecontrols.T3FoldPostureModule" + "expo.modules.t3nativecontrols.T3FoldPostureModule", + "expo.modules.t3nativecontrols.T3SecretInputModule" ] } } diff --git a/apps/mobile/src/components/ui/SecretTextInput.android.tsx b/apps/mobile/src/components/ui/SecretTextInput.android.tsx new file mode 100644 index 0000000000000..88b88602b0f18 --- /dev/null +++ b/apps/mobile/src/components/ui/SecretTextInput.android.tsx @@ -0,0 +1,55 @@ +import { requireNativeView } from "expo"; +import { processColor, View, type NativeSyntheticEvent, type ViewProps } from "react-native"; + +import { flattenThemeColor } from "../../lib/mobileTheme"; +import { useUniwindTheme } from "../../lib/useUniwindTheme"; +import type { SecretTextInputProps } from "./SecretTextInput.types"; + +interface NativeSecretInputProps extends ViewProps { + readonly value: string; + readonly placeholder: string; + readonly textColor: number; + readonly placeholderColor: number; + readonly cursorColor: number; + readonly fontSize: number; + readonly editable: boolean; + readonly onChangeText: (event: NativeSyntheticEvent<{ readonly value: string }>) => void; + readonly onSubmit: () => void; +} + +const NativeSecretInput = requireNativeView("T3SecretInput"); + +const FONT_SIZE = 16; +const LINE_HEIGHT = 24; + +function nativeColor(value: string) { + const color = processColor(value); + return typeof color === "number" ? color : 0; +} + +/** + * Android's React Native TextInput cannot opt out of credential autofill, so password managers + * offer logins above the keyboard. This field is a native EditText that reports no autofill type. + * The frame matches AppTextInput's. + */ +export function SecretTextInput(props: SecretTextInputProps) { + const theme = useUniwindTheme(); + const surface = theme["--color-input"]; + return ( + + props.onChangeText(event.nativeEvent.value)} + onSubmit={props.onSubmitEditing} + style={{ height: LINE_HEIGHT }} + /> + + ); +} diff --git a/apps/mobile/src/components/ui/SecretTextInput.tsx b/apps/mobile/src/components/ui/SecretTextInput.tsx new file mode 100644 index 0000000000000..01cbc1099834e --- /dev/null +++ b/apps/mobile/src/components/ui/SecretTextInput.tsx @@ -0,0 +1,24 @@ +import { AppTextInput } from "./AppText"; +import type { SecretTextInputProps } from "./SecretTextInput.types"; + +/** Masked field for a value that must stay out of password managers and suggestions. */ +export function SecretTextInput(props: SecretTextInputProps) { + return ( + + ); +} diff --git a/apps/mobile/src/components/ui/SecretTextInput.types.ts b/apps/mobile/src/components/ui/SecretTextInput.types.ts new file mode 100644 index 0000000000000..5c86fda2ce75d --- /dev/null +++ b/apps/mobile/src/components/ui/SecretTextInput.types.ts @@ -0,0 +1,8 @@ +export interface SecretTextInputProps { + readonly accessibilityLabel: string; + readonly placeholder: string; + readonly value: string; + readonly onChangeText: (value: string) => void; + readonly onSubmitEditing: () => void; + readonly editable: boolean; +} diff --git a/apps/mobile/src/features/threads/SecretRequestCard.tsx b/apps/mobile/src/features/threads/SecretRequestCard.tsx index bc1272d6847df..a1a7afc485ecc 100644 --- a/apps/mobile/src/features/threads/SecretRequestCard.tsx +++ b/apps/mobile/src/features/threads/SecretRequestCard.tsx @@ -24,7 +24,8 @@ import { useRef, useState } from "react"; import { View, type ColorValue } from "react-native"; import { SymbolView, type AppSymbolName } from "../../components/ui/AppSymbol"; -import { AppText as Text, AppTextInput as TextInput } from "../../components/ui/AppText"; +import { AppText as Text } from "../../components/ui/AppText"; +import { SecretTextInput } from "../../components/ui/SecretTextInput"; import { serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; import { Button } from "../../components/ui/Button"; @@ -157,20 +158,12 @@ function PendingSecretRequestForm(props: { ) : null} {source === null ? ( - ) : ( diff --git a/vite.config.ts b/vite.config.ts index d5ee6c9d0dff2..bddcc1c68c82b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -640,6 +640,7 @@ export default defineConfig({ "apps/mobile/src/features/threads/ThreadQueueControl.tsx", "apps/mobile/src/features/review/ReviewCommentCard.tsx", "apps/mobile/src/features/threads/thread-list-v2-items.tsx", + "apps/mobile/src/components/ui/SecretTextInput.android.tsx", "apps/mobile/src/lib/useMobileNavigationTheme.ts", "apps/mobile/src/native/T3ComposerEditor.ios.tsx", "apps/mobile/src/native/T3ComposerEditor.native.tsx", From 87f17ecaba8b748f05490b22b24c10381ee8e6b8 Mon Sep 17 00:00:00 2001 From: ravdeep kainth Date: Tue, 3 Nov 2026 12:22:17 +1100 Subject: [PATCH 2/8] fix(mobile): secret input no longer drops fast keystrokes or submits twice The native secret input applied every JS echo of an older value, which overwrote characters typed in the meantime, and hardware Enter reported both key down and key up, so it submitted twice. Track values sent to JS until they are echoed, and submit only on key down. Co-Authored-By: Claude Sonnet 5.5 --- .../t3nativecontrols/T3SecretInputModule.kt | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt index be112f42cf8e5..963399156f205 100644 --- a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt @@ -9,6 +9,7 @@ import android.text.InputType import android.text.TextWatcher import android.util.TypedValue import android.view.Gravity +import android.view.KeyEvent import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo @@ -60,6 +61,10 @@ class T3SecretInputView(context: Context, appContext: AppContext) : ExpoView(con private val onSubmit by EventDispatcher() private var applyingValue = false + // Values sent to JS that its state has not echoed back yet. An echo of an older value would + // otherwise overwrite characters typed in the meantime. + private val unechoed = ArrayDeque() + init { input.background = null input.setPadding(0, 0, 0, 0) @@ -81,13 +86,17 @@ class T3SecretInputView(context: Context, appContext: AppContext) : ExpoView(con override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit override fun afterTextChanged(s: Editable?) { - if (!applyingValue) onChangeText(mapOf("value" to (s?.toString() ?: ""))) + if (applyingValue) return + val value = s?.toString() ?: "" + unechoed.addLast(value) + onChangeText(mapOf("value" to value)) } } ) - input.setOnEditorActionListener { _, actionId, _ -> + input.setOnEditorActionListener { _, actionId, event -> if (actionId == EditorInfo.IME_ACTION_DONE) { - onSubmit(emptyMap()) + // A hardware Enter reports both key down and key up. + if (event == null || event.action == KeyEvent.ACTION_DOWN) onSubmit(emptyMap()) true } else { false @@ -101,6 +110,13 @@ class T3SecretInputView(context: Context, appContext: AppContext) : ExpoView(con /** Applies JS state without echoing it back, and without moving the caret on equal values. */ fun setValue(value: String) { + val echoed = unechoed.indexOf(value) + if (echoed >= 0) { + repeat(echoed + 1) { unechoed.removeFirst() } + return + } + // A value JS chose itself, such as clearing after a save. + unechoed.clear() if (input.text.toString() == value) return applyingValue = true input.setText(value) From 1937b7fc971f3ec606bb48371be71d09f320c308 Mon Sep 17 00:00:00 2001 From: ravdeep kainth Date: Tue, 3 Nov 2026 12:34:54 +1100 Subject: [PATCH 3/8] fix(mobile): secret input submits on hardware Enter on Android 13 and earlier Co-Authored-By: Claude Sonnet 5.5 --- .../java/expo/modules/t3nativecontrols/T3SecretInputModule.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt index 963399156f205..aa6df6fc6d206 100644 --- a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt @@ -94,8 +94,8 @@ class T3SecretInputView(context: Context, appContext: AppContext) : ExpoView(con } ) input.setOnEditorActionListener { _, actionId, event -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - // A hardware Enter reports both key down and key up. + if (actionId == EditorInfo.IME_ACTION_DONE || event?.keyCode == KeyEvent.KEYCODE_ENTER) { + // Hardware Enter reports IME_NULL on Android 13 and earlier, and both key down and key up. if (event == null || event.action == KeyEvent.ACTION_DOWN) onSubmit(emptyMap()) true } else { From 5dd1ba423fbc7659552ee1e12e53c38eb5e53349 Mon Sep 17 00:00:00 2001 From: ravdeep kainth Date: Tue, 3 Nov 2026 12:39:33 +1100 Subject: [PATCH 4/8] fix(mobile): secret input label reaches the Android EditText for TalkBack Co-Authored-By: Claude Sonnet 5.5 --- .../java/expo/modules/t3nativecontrols/T3SecretInputModule.kt | 4 ++++ apps/mobile/src/components/ui/SecretTextInput.android.tsx | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt index aa6df6fc6d206..cc93c588efd58 100644 --- a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt @@ -29,6 +29,10 @@ class T3SecretInputModule : Module() { Prop("placeholder") { view: T3SecretInputView, value: String -> view.input.hint = value } + // Not "accessibilityLabel": React Native applies that to the outer view, not the EditText. + Prop("inputLabel") { view: T3SecretInputView, value: String -> + view.input.contentDescription = value + } Prop("textColor") { view: T3SecretInputView, value: Int -> view.input.setTextColor(value) } Prop("placeholderColor") { view: T3SecretInputView, value: Int -> view.input.setHintTextColor(value) diff --git a/apps/mobile/src/components/ui/SecretTextInput.android.tsx b/apps/mobile/src/components/ui/SecretTextInput.android.tsx index 88b88602b0f18..4cd1c40b9aae4 100644 --- a/apps/mobile/src/components/ui/SecretTextInput.android.tsx +++ b/apps/mobile/src/components/ui/SecretTextInput.android.tsx @@ -6,6 +6,7 @@ import { useUniwindTheme } from "../../lib/useUniwindTheme"; import type { SecretTextInputProps } from "./SecretTextInput.types"; interface NativeSecretInputProps extends ViewProps { + readonly inputLabel: string; readonly value: string; readonly placeholder: string; readonly textColor: number; @@ -38,7 +39,7 @@ export function SecretTextInput(props: SecretTextInputProps) { return ( Date: Tue, 3 Nov 2026 13:05:31 +1100 Subject: [PATCH 5/8] fix(mobile): secret input only sets importantForAutofill on API 26 and later Co-Authored-By: Claude Sonnet 5.5 --- .../expo/modules/t3nativecontrols/T3SecretInputModule.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt index cc93c588efd58..d955bd29a9c6b 100644 --- a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt @@ -73,7 +73,10 @@ class T3SecretInputView(context: Context, appContext: AppContext) : ExpoView(con input.background = null input.setPadding(0, 0, 0, 0) input.gravity = Gravity.CENTER_VERTICAL or Gravity.START - input.importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO + // The autofill APIs arrive with API 26; minSdk is 24. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + input.importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO + } input.imeOptions = EditorInfo.IME_ACTION_DONE // Not setSingleLine(): that swaps out the password transformation. The password variation is // already single line, and the keyboard keeps typed text out of suggestions. From 18e52d67bd7786334ed3944ef20c3218296bd29c Mon Sep 17 00:00:00 2001 From: ravdeep kainth Date: Fri, 6 Nov 2026 10:34:26 +1100 Subject: [PATCH 6/8] fix(mobile): keep the reference input import after merging secret sources --- apps/mobile/src/features/threads/SecretRequestCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/SecretRequestCard.tsx b/apps/mobile/src/features/threads/SecretRequestCard.tsx index a1a7afc485ecc..01e1dab11657e 100644 --- a/apps/mobile/src/features/threads/SecretRequestCard.tsx +++ b/apps/mobile/src/features/threads/SecretRequestCard.tsx @@ -24,7 +24,7 @@ import { useRef, useState } from "react"; import { View, type ColorValue } from "react-native"; import { SymbolView, type AppSymbolName } from "../../components/ui/AppSymbol"; -import { AppText as Text } from "../../components/ui/AppText"; +import { AppText as Text, AppTextInput as TextInput } from "../../components/ui/AppText"; import { SecretTextInput } from "../../components/ui/SecretTextInput"; import { serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; From 31f5843518cade0cc2ea5f167a4eb69e1e197731 Mon Sep 17 00:00:00 2001 From: ravdeep kainth Date: Fri, 6 Nov 2026 10:46:38 +1100 Subject: [PATCH 7/8] fix(mobile): secret input submits the field's current text The submit event now carries the native text, so Done right after a final keystroke cannot save a stale render of the secret. --- .../java/expo/modules/t3nativecontrols/T3SecretInputModule.kt | 2 +- apps/mobile/src/components/ui/SecretTextInput.android.tsx | 4 ++-- apps/mobile/src/components/ui/SecretTextInput.tsx | 2 +- apps/mobile/src/components/ui/SecretTextInput.types.ts | 3 ++- apps/mobile/src/features/threads/SecretRequestCard.tsx | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt index d955bd29a9c6b..9136342292f91 100644 --- a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt @@ -103,7 +103,7 @@ class T3SecretInputView(context: Context, appContext: AppContext) : ExpoView(con input.setOnEditorActionListener { _, actionId, event -> if (actionId == EditorInfo.IME_ACTION_DONE || event?.keyCode == KeyEvent.KEYCODE_ENTER) { // Hardware Enter reports IME_NULL on Android 13 and earlier, and both key down and key up. - if (event == null || event.action == KeyEvent.ACTION_DOWN) onSubmit(emptyMap()) + if (event == null || event.action == KeyEvent.ACTION_DOWN) onSubmit(mapOf("value" to input.text.toString())) true } else { false diff --git a/apps/mobile/src/components/ui/SecretTextInput.android.tsx b/apps/mobile/src/components/ui/SecretTextInput.android.tsx index 4cd1c40b9aae4..f93b091cf9b58 100644 --- a/apps/mobile/src/components/ui/SecretTextInput.android.tsx +++ b/apps/mobile/src/components/ui/SecretTextInput.android.tsx @@ -15,7 +15,7 @@ interface NativeSecretInputProps extends ViewProps { readonly fontSize: number; readonly editable: boolean; readonly onChangeText: (event: NativeSyntheticEvent<{ readonly value: string }>) => void; - readonly onSubmit: () => void; + readonly onSubmit: (event: NativeSyntheticEvent<{ readonly value: string }>) => void; } const NativeSecretInput = requireNativeView("T3SecretInput"); @@ -48,7 +48,7 @@ export function SecretTextInput(props: SecretTextInputProps) { fontSize={FONT_SIZE} editable={props.editable} onChangeText={(event) => props.onChangeText(event.nativeEvent.value)} - onSubmit={props.onSubmitEditing} + onSubmit={(event) => props.onSubmitEditing(event.nativeEvent.value)} style={{ height: LINE_HEIGHT }} /> diff --git a/apps/mobile/src/components/ui/SecretTextInput.tsx b/apps/mobile/src/components/ui/SecretTextInput.tsx index 01cbc1099834e..ed8bba58daaa7 100644 --- a/apps/mobile/src/components/ui/SecretTextInput.tsx +++ b/apps/mobile/src/components/ui/SecretTextInput.tsx @@ -18,7 +18,7 @@ export function SecretTextInput(props: SecretTextInputProps) { importantForAutofill="no" spellCheck={false} returnKeyType="done" - onSubmitEditing={props.onSubmitEditing} + onSubmitEditing={(event) => props.onSubmitEditing(event.nativeEvent.text)} /> ); } diff --git a/apps/mobile/src/components/ui/SecretTextInput.types.ts b/apps/mobile/src/components/ui/SecretTextInput.types.ts index 5c86fda2ce75d..37db8e458d28d 100644 --- a/apps/mobile/src/components/ui/SecretTextInput.types.ts +++ b/apps/mobile/src/components/ui/SecretTextInput.types.ts @@ -3,6 +3,7 @@ export interface SecretTextInputProps { readonly placeholder: string; readonly value: string; readonly onChangeText: (value: string) => void; - readonly onSubmitEditing: () => void; + /** Receives the field's current text, so a submit never uses a stale render of it. */ + readonly onSubmitEditing: (value: string) => void; readonly editable: boolean; } diff --git a/apps/mobile/src/features/threads/SecretRequestCard.tsx b/apps/mobile/src/features/threads/SecretRequestCard.tsx index 01e1dab11657e..4d4234075a596 100644 --- a/apps/mobile/src/features/threads/SecretRequestCard.tsx +++ b/apps/mobile/src/features/threads/SecretRequestCard.tsx @@ -164,7 +164,7 @@ function PendingSecretRequestForm(props: { value={secret} onChangeText={setSecret} editable={!submitting} - onSubmitEditing={save} + onSubmitEditing={(value) => void send({ type: "save", secret: value })} /> ) : ( Date: Fri, 6 Nov 2026 10:51:18 +1100 Subject: [PATCH 8/8] style(mobile): wrap the secret input submit call for ktlint --- .../java/expo/modules/t3nativecontrols/T3SecretInputModule.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt index 9136342292f91..1df8c646d4832 100644 --- a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3SecretInputModule.kt @@ -103,7 +103,9 @@ class T3SecretInputView(context: Context, appContext: AppContext) : ExpoView(con input.setOnEditorActionListener { _, actionId, event -> if (actionId == EditorInfo.IME_ACTION_DONE || event?.keyCode == KeyEvent.KEYCODE_ENTER) { // Hardware Enter reports IME_NULL on Android 13 and earlier, and both key down and key up. - if (event == null || event.action == KeyEvent.ACTION_DOWN) onSubmit(mapOf("value" to input.text.toString())) + if (event == null || event.action == KeyEvent.ACTION_DOWN) { + onSubmit(mapOf("value" to input.text.toString())) + } true } else { false