Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,14 @@ export default function App(props: AppProps) {
navigate("/settings");
}

// Abort controller for the in-flight conditional mediation request, if any.
let passkeyAutofillAbortController: AbortController | null = null;

function abortPasskeyAutofill() {
passkeyAutofillAbortController?.abort();
passkeyAutofillAbortController = null;
}

// Try zero-click passkey authentication
async function tryPasskeyAuth(): Promise<boolean> {
const startedAt = logPasskeyStart("autofill");
Expand Down Expand Up @@ -895,6 +903,7 @@ export default function App(props: AppProps) {
});

// Try to get credential with conditional mediation
passkeyAutofillAbortController = new AbortController();
const credential = (await navigator.credentials.get({
publicKey: {
challenge: base64UrlDecode(options.challenge).buffer as ArrayBuffer,
Expand All @@ -904,7 +913,9 @@ export default function App(props: AppProps) {
allowCredentials: [],
},
mediation: "conditional",
signal: passkeyAutofillAbortController.signal,
})) as PublicKeyCredential | null;
passkeyAutofillAbortController = null;

if (!credential) {
logPasskeyEvent("autofill", "credential.null");
Expand Down Expand Up @@ -948,6 +959,9 @@ export default function App(props: AppProps) {
}
return true;
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
return false;
}
logPasskeyError("autofill", startedAt, err);
return false;
}
Expand Down Expand Up @@ -1749,6 +1763,7 @@ export default function App(props: AppProps) {
}
await tryPasskeyAuth();
}}
onAbortAutofill={abortPasskeyAutofill}
onAuthenticated={handleNativeAuthComplete}
onChooseUsername={() => setAuthStep("choose_username")}
onEmailLogin={() => setAuthStep("email_login")}
Expand Down
4 changes: 4 additions & 0 deletions src/components/LoginScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import "./EmailLoginScreen.css";

type LoginScreenProps = {
onTryPasskey: () => Promise<void>;
onAbortAutofill: () => void;
onAuthenticated: (user: User, needsPasskeySetup: boolean) => Promise<void>;
onChooseUsername: () => void;
onEmailLogin: () => void;
Expand Down Expand Up @@ -270,6 +271,9 @@ export function LoginScreen(props: LoginScreenProps) {
}

async function handlePasskeyLogin() {
// Abort any in-flight conditional mediation request before starting an explicit one.
// The browser allows only one pending WebAuthn request at a time.
props.onAbortAutofill();
const startedAt = logPasskeyStart("login");
if (!Capacitor.isNativePlatform() && !window.PublicKeyCredential) {
setPasskeyError("Passkeys are not supported on this device");
Expand Down
Loading