From b9b3d9bb69690116f311ab6701f47296eeee71c5 Mon Sep 17 00:00:00 2001 From: Marc Kelechava Date: Thu, 4 Dec 2025 13:26:51 -0800 Subject: [PATCH] Autofill totp_identifier for user/email based on creds (#4200) --- skyvern-frontend/src/api/types.ts | 1 + .../routes/credentials/CredentialsModal.tsx | 3 + .../credentials/PasswordCredentialContent.tsx | 177 +++++++++++------- 3 files changed, 113 insertions(+), 68 deletions(-) diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index b82c5e9d..c4c839a9 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -528,6 +528,7 @@ export type PasswordCredential = { password: string; totp: string | null; totp_type: "authenticator" | "email" | "text" | "none"; + totp_identifier?: string | null; }; export type CreditCardCredential = { diff --git a/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx b/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx index f23cec81..e75c14ed 100644 --- a/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx +++ b/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx @@ -28,6 +28,7 @@ const PASSWORD_CREDENTIAL_INITIAL_VALUES = { password: "", totp: "", totp_type: "none" as "none" | "authenticator" | "email" | "text", + totp_identifier: "", }; const CREDIT_CARD_CREDENTIAL_INITIAL_VALUES = { @@ -141,6 +142,7 @@ function CredentialsModal({ onCredentialCreated }: Props) { const username = passwordCredentialValues.username.trim(); const password = passwordCredentialValues.password.trim(); const totp = passwordCredentialValues.totp.trim(); + const totpIdentifier = passwordCredentialValues.totp_identifier.trim(); if (username === "" || password === "") { toast({ @@ -158,6 +160,7 @@ function CredentialsModal({ onCredentialCreated }: Props) { password, totp: totp === "" ? null : totp, totp_type: passwordCredentialValues.totp_type, + totp_identifier: totpIdentifier === "" ? null : totpIdentifier, }, }); } else if (type === CredentialModalTypes.CREDIT_CARD) { diff --git a/skyvern-frontend/src/routes/credentials/PasswordCredentialContent.tsx b/skyvern-frontend/src/routes/credentials/PasswordCredentialContent.tsx index e591ce10..2f785cbb 100644 --- a/skyvern-frontend/src/routes/credentials/PasswordCredentialContent.tsx +++ b/skyvern-frontend/src/routes/credentials/PasswordCredentialContent.tsx @@ -15,7 +15,7 @@ import { EyeOpenIcon, MobileIcon, } from "@radix-ui/react-icons"; -import { useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; type Props = { @@ -25,6 +25,7 @@ type Props = { password: string; totp: string; totp_type: "authenticator" | "email" | "text" | "none"; + totp_identifier: string; }; onChange: (values: { name: string; @@ -32,27 +33,80 @@ type Props = { password: string; totp: string; totp_type: "authenticator" | "email" | "text" | "none"; + totp_identifier: string; }) => void; }; -function PasswordCredentialContent({ - values: { name, username, password, totp, totp_type }, - onChange, -}: Props) { +function PasswordCredentialContent({ values, onChange }: Props) { + const { name, username, password, totp, totp_type, totp_identifier } = values; const [totpMethod, setTotpMethod] = useState< "authenticator" | "email" | "text" - >("authenticator"); + >( + totp_type === "email" || totp_type === "text" ? totp_type : "authenticator", + ); const [showPassword, setShowPassword] = useState(false); + const prevUsernameRef = useRef(username); + const prevTotpMethodRef = useRef(totpMethod); + const totpIdentifierLabel = + totpMethod === "text" + ? "TOTP Identifier (Phone)" + : "TOTP Identifier (Username or Email)"; + const totpIdentifierHelper = + totpMethod === "text" + ? "Phone number used to receive 2FA codes." + : "Email address used to receive 2FA codes."; + + const updateValues = useCallback( + (updates: Partial): void => { + onChange({ + name, + username, + password, + totp, + totp_type, + totp_identifier, + ...updates, + }); + }, + [name, onChange, password, totp, totp_identifier, totp_type, username], + ); + + useEffect(() => { + const prevUsername = prevUsernameRef.current; + const prevMethod = prevTotpMethodRef.current; + + if (totpMethod === "email") { + const usernameChanged = username !== prevUsername; + const identifierBlank = totp_identifier.trim() === ""; + const identifierMatchedPrevUsername = totp_identifier === prevUsername; + const methodChanged = prevMethod !== "email"; + + if ( + identifierBlank || + methodChanged || + (usernameChanged && identifierMatchedPrevUsername) + ) { + updateValues({ totp_identifier: username }); + } + } + + if (totpMethod === "text" && prevMethod !== "text") { + const wasAutoFilled = totp_identifier === prevUsername; + if (wasAutoFilled || totp_identifier.trim() === "") { + updateValues({ totp_identifier: "" }); + } + } + + prevUsernameRef.current = username; + prevTotpMethodRef.current = totpMethod; + }, [totpMethod, totp_identifier, updateValues, username]); // Update totp_type when totpMethod changes const handleTotpMethodChange = ( method: "authenticator" | "email" | "text", ) => { setTotpMethod(method); - onChange({ - name, - username, - password, + updateValues({ totp: method === "authenticator" ? totp : "", totp_type: method, }); @@ -69,33 +123,17 @@ function PasswordCredentialContent({ - onChange({ - name: e.target.value, - username, - password, - totp, - totp_type, - }) - } + onChange={(e) => updateValues({ name: e.target.value })} />
- +
- onChange({ - name, - username: e.target.value, - password, - totp, - totp_type, - }) - } + onChange={(e) => updateValues({ username: e.target.value })} />
@@ -107,15 +145,7 @@ function PasswordCredentialContent({ className="pr-9" type={showPassword ? "text" : "password"} value={password} - onChange={(e) => - onChange({ - name, - username, - totp, - password: e.target.value, - totp_type, - }) - } + onChange={(e) => updateValues({ password: e.target.value })} />
{(totpMethod === "text" || totpMethod === "email") && ( -

- - Contact us to set up two-factor authentication in workflows - {" "} - or{" "} - - see our documentation on how to set up two-factor - authentication in workflows - {" "} - to get started. -

+ <> +
+
+
+ +
+ + updateValues({ totp_identifier: e.target.value }) + } + /> +
+

+ {totpIdentifierHelper} +

+
+

+ + Contact us to set up two-factor authentication in + workflows + {" "} + or{" "} + + see our documentation on how to set up two-factor + authentication in workflows + {" "} + to get started. +

+ )} {totpMethod === "authenticator" && (
@@ -215,15 +264,7 @@ function PasswordCredentialContent({
- onChange({ - name, - username, - password, - totp: e.target.value, - totp_type, - }) - } + onChange={(e) => updateValues({ totp: e.target.value })} />