diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts
index 8b02f77a..3804d9b1 100644
--- a/skyvern-frontend/src/api/types.ts
+++ b/skyvern-frontend/src/api/types.ts
@@ -317,3 +317,36 @@ export type CredentialApiResponse = {
credential_type: "password" | "credit_card";
name: string;
};
+
+export function isPasswordCredential(
+ credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse,
+): credential is PasswordCredentialApiResponse {
+ return "username" in credential;
+}
+
+export function isCreditCardCredential(
+ credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse,
+): credential is CreditCardCredentialApiResponse {
+ return "last_four" in credential;
+}
+
+export type CreateCredentialRequest = {
+ name: string;
+ credential_type: "password" | "credit_card";
+ credential: PasswordCredential | CreditCardCredential;
+};
+
+export type PasswordCredential = {
+ username: string;
+ password: string;
+ totp: string | null;
+};
+
+export type CreditCardCredential = {
+ card_number: string;
+ card_cvv: string;
+ card_exp_month: string;
+ card_exp_year: string;
+ card_brand: string;
+ card_holder_name: string;
+};
diff --git a/skyvern-frontend/src/routes/credentials/CredentialItem.tsx b/skyvern-frontend/src/routes/credentials/CredentialItem.tsx
new file mode 100644
index 00000000..122b2e3c
--- /dev/null
+++ b/skyvern-frontend/src/routes/credentials/CredentialItem.tsx
@@ -0,0 +1,56 @@
+import { isPasswordCredential } from "@/api/types";
+import { DeleteCredentialButton } from "./DeleteCredentialButton";
+import { CredentialApiResponse } from "@/api/types";
+
+type Props = {
+ credential: CredentialApiResponse;
+};
+
+function CredentialItem({ credential }: Props) {
+ return (
+
+
+
+
Name
+
+ The name of the credential
+
+
+
onChange({ ...values, name: e.target.value })}
+ />
+
+
+
+ Cardholder Name
+
+ onChange({ ...values, cardHolderName: e.target.value })
+ }
+ />
+
+
+ Number
+ {
+ onChange({
+ ...values,
+ cardNumber: formatCardNumber(event.target.value),
+ });
+ }}
+ pattern="[0-9]{13,19}"
+ placeholder="XXXX XXXX XXXX XXXX"
+ maxLength={19}
+ autoComplete="cc-number"
+ />
+
+
+ Brand
+ ({
+ label: brand,
+ value: brand,
+ }))}
+ value={values.cardBrand}
+ onChange={(value) => onChange({ ...values, cardBrand: value })}
+ placeholder="Select Brand"
+ />
+
+
+
+ );
+}
+
+export { CreditCardCredentialContent };
diff --git a/skyvern-frontend/src/routes/credentials/DeleteCredentialButton.tsx b/skyvern-frontend/src/routes/credentials/DeleteCredentialButton.tsx
new file mode 100644
index 00000000..19b90d0a
--- /dev/null
+++ b/skyvern-frontend/src/routes/credentials/DeleteCredentialButton.tsx
@@ -0,0 +1,100 @@
+import { DialogClose } from "@/components/ui/dialog";
+import {
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { getClient } from "@/api/AxiosClient";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { toast } from "@/components/ui/use-toast";
+import { useCredentialGetter } from "@/hooks/useCredentialGetter";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { AxiosError } from "axios";
+import { ReloadIcon, TrashIcon } from "@radix-ui/react-icons";
+import { CredentialApiResponse } from "@/api/types";
+type Props = {
+ credential: CredentialApiResponse;
+};
+
+function DeleteCredentialButton({ credential }: Props) {
+ const credentialGetter = useCredentialGetter();
+ const queryClient = useQueryClient();
+
+ const deleteCredentialMutation = useMutation({
+ mutationFn: async (id: string) => {
+ const client = await getClient(credentialGetter);
+ return client.delete(`/credentials/${id}`);
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: ["credentials"],
+ });
+ toast({
+ title: "Credential deleted",
+ variant: "success",
+ description: "The credential has been deleted.",
+ });
+ },
+ onError: (error: AxiosError) => {
+ toast({
+ variant: "destructive",
+ title: "Failed to delete credential",
+ description: error.message,
+ });
+ },
+ });
+
+ return (
+
+
+
+
Name
+
+ The name of the credential
+
+
+
+ onChange({
+ name: e.target.value,
+ username,
+ password,
+ totp,
+ })
+ }
+ />
+
+
+
+
+ Username or email
+
+
+ onChange({
+ name,
+ username: e.target.value,
+ password,
+ totp,
+ })
+ }
+ />
+
+
+
+ Password
+
+
+
+ onChange({
+ name,
+ username,
+ totp,
+ password: e.target.value,
+ })
+ }
+ />
+
{
+ setShowPassword((value) => !value);
+ }}
+ aria-label="Toggle password visibility"
+ >
+ {showPassword ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
Two-Factor Authentication
+
+ Set up Skyvern to automatically retrieve two-factor authentication
+ codes.
+
+
+
+
setTotpMethod("authenticator")}
+ >
+
+ Authenticator App
+
+
setTotpMethod("email")}
+ >
+
+ Email
+
+
setTotpMethod("text")}
+ >
+
+ Text Message
+
+
+ {(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.
+
+ )}
+ {totpMethod === "authenticator" && (
+
+
+
+ Authenticator Key
+
+
+ onChange({
+ name,
+ username,
+ password,
+ totp: e.target.value,
+ })
+ }
+ />
+
+
+ You need to find the authenticator secret from the website where
+ you are using the credential. Here are some guides from popular
+ authenticator apps:{" "}
+
+ Bitwarden
+
+ {", "}
+
+ 1Password
+
+ {", and "}
+
+ LastPass
+
+ {"."}
+
+
+ )}
+
+
+ );
+}
+
+export { PasswordCredentialContent };
diff --git a/skyvern-frontend/src/routes/credentials/useCredentialModalState.ts b/skyvern-frontend/src/routes/credentials/useCredentialModalState.ts
new file mode 100644
index 00000000..ab5eb4f3
--- /dev/null
+++ b/skyvern-frontend/src/routes/credentials/useCredentialModalState.ts
@@ -0,0 +1,59 @@
+import { useSearchParams } from "react-router-dom";
+
+const modalParam = "modal";
+const typeParam = "type";
+
+export const CredentialModalTypes = {
+ PASSWORD: "password",
+ CREDIT_CARD: "credit-card",
+} as const;
+
+export type CredentialModalType =
+ (typeof CredentialModalTypes)[keyof typeof CredentialModalTypes];
+
+type ReturnType = {
+ isOpen: boolean;
+ type: CredentialModalType;
+ setIsOpen: (isOpen: boolean) => void;
+ setType: (type: CredentialModalType) => void;
+};
+
+function getCredentialModalType(type: string): CredentialModalType {
+ if (
+ Object.values(CredentialModalTypes).includes(type as CredentialModalType)
+ ) {
+ return type as CredentialModalType;
+ }
+ return CredentialModalTypes.PASSWORD;
+}
+
+function useCredentialModalState(): ReturnType {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const modal = searchParams.get(modalParam);
+ const isOpen = modal === "true";
+ const type = getCredentialModalType(searchParams.get(typeParam) ?? "");
+
+ const setIsOpen = (isOpen: boolean) => {
+ setSearchParams((prev) => {
+ prev.set(modalParam, isOpen.toString());
+ return prev;
+ });
+ };
+
+ const setType = (type: CredentialModalType) => {
+ setSearchParams((prev) => {
+ prev.set(typeParam, type);
+ return prev;
+ });
+ };
+
+ return {
+ isOpen,
+ type,
+ setIsOpen,
+ setType,
+ };
+}
+
+export { useCredentialModalState };
diff --git a/skyvern-frontend/src/routes/workflows/components/CredentialParameterSourceSelector.tsx b/skyvern-frontend/src/routes/workflows/components/CredentialParameterSourceSelector.tsx
index dcb5eb65..0d05b2d6 100644
--- a/skyvern-frontend/src/routes/workflows/components/CredentialParameterSourceSelector.tsx
+++ b/skyvern-frontend/src/routes/workflows/components/CredentialParameterSourceSelector.tsx
@@ -9,6 +9,12 @@ import { Skeleton } from "@/components/ui/skeleton";
import { useCredentialsQuery } from "../hooks/useCredentialsQuery";
import { useWorkflowParametersState } from "../editor/useWorkflowParametersState";
import { WorkflowParameterValueType } from "../types/workflowTypes";
+import { PlusIcon } from "@radix-ui/react-icons";
+import {
+ CredentialModalTypes,
+ useCredentialModalState,
+} from "@/routes/credentials/useCredentialModalState";
+import { CredentialsModal } from "@/routes/credentials/CredentialsModal";
type Props = {
value: string;
@@ -17,6 +23,7 @@ type Props = {
function CredentialParameterSourceSelector({ value, onChange }: Props) {
const { data: credentials, isFetching } = useCredentialsQuery();
+ const { setIsOpen, setType } = useCredentialModalState();
const [workflowParameters] = useWorkflowParametersState();
const workflowParametersOfTypeCredentialId = workflowParameters.filter(
(parameter) =>
@@ -51,18 +58,42 @@ function CredentialParameterSourceSelector({ value, onChange }: Props) {
];
return (
-