Rename again

This commit is contained in:
Manav Rathi 2024-06-06 21:32:10 +05:30
parent 0e284752d1
commit c983c43ba1
No known key found for this signature in database
9 changed files with 410 additions and 408 deletions

View File

@ -5,7 +5,7 @@ const Page = () => {
useEffect(() => {
window.location.href = window.location.href.replace(
"account-handoff",
"passkeys",
"passkeys/handoff",
);
}, []);

View File

@ -0,0 +1,50 @@
import log from "@/next/log";
import { VerticallyCentered } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
/**
* Parse credentials passed as query parameters by one of our client apps, save
* them to local storage, and then redirect to the passkeys listing.
*/
const Page: React.FC = () => {
const router = useRouter();
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const client = urlParams.get("client");
if (client) {
// TODO-PK: mobile is not passing it. is that expected?
setData(LS_KEYS.CLIENT_PACKAGE, { name: client });
HTTPService.setHeaders({
"X-Client-Package": client,
});
}
const token = urlParams.get("token");
if (!token) {
log.error("Missing accounts token");
router.push("/login");
return;
}
const user = getData(LS_KEYS.USER) || {};
user.token = token;
setData(LS_KEYS.USER, user);
router.push("/passkeys");
}, []);
return (
<VerticallyCentered>
<EnteSpinner />
</VerticallyCentered>
);
};
export default Page;

View File

@ -1,46 +1,371 @@
import log from "@/next/log";
import { VerticallyCentered } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
import { ensure } from "@/utils/ensure";
import { CenteredFlex } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import EnteButton from "@ente/shared/components/EnteButton";
import { EnteDrawer } from "@ente/shared/components/EnteDrawer";
import FormPaper from "@ente/shared/components/Form/FormPaper";
import InfoItem from "@ente/shared/components/Info/InfoItem";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import MenuItemDivider from "@ente/shared/components/Menu/MenuItemDivider";
import { MenuItemGroup } from "@ente/shared/components/Menu/MenuItemGroup";
import SingleInputForm from "@ente/shared/components/SingleInputForm";
import Titlebar from "@ente/shared/components/Titlebar";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
import { formatDateTimeFull } from "@ente/shared/time/format";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import KeyIcon from "@mui/icons-material/Key";
import { Box, Button, Stack, Typography, useMediaQuery } from "@mui/material";
import { t } from "i18next";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import { useAppContext } from "pages/_app";
import React, { useEffect, useState } from "react";
import {
deletePasskey,
registerPasskey,
renamePasskey,
} from "services/passkey";
import { getPasskeys, type Passkey } from "../../services/passkey";
const Page: React.FC = () => {
const { showNavBar } = useAppContext();
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
const [selectedPasskey, setSelectedPasskey] = useState<
Passkey | undefined
>();
const router = useRouter();
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const pkg = urlParams.get("package");
if (pkg) {
// TODO-PK: mobile is not passing it. is that expected?
setData(LS_KEYS.CLIENT_PACKAGE, { name: pkg });
HTTPService.setHeaders({
"X-Client-Package": pkg,
});
const refreshPasskeys = async () => {
try {
setPasskeys((await getPasskeys()) || []);
} catch (e) {
log.error("Failed to fetch passkeys", e);
}
};
const token = urlParams.get("token");
if (!token) {
log.error("Missing accounts token");
useEffect(() => {
if (!getToken()) {
router.push("/login");
return;
}
const user = getData(LS_KEYS.USER) || {};
user.token = token;
setData(LS_KEYS.USER, user);
router.push("/passkeys/setup");
showNavBar(true);
void refreshPasskeys();
}, []);
const handleSelectPasskey = (passkey: Passkey) => {
setSelectedPasskey(passkey);
setShowPasskeyDrawer(true);
};
const handleDrawerClose = () => {
setShowPasskeyDrawer(false);
// Don't clear the selected passkey, let the stale value be so that the
// drawer closing animation is nicer.
//
// The value will get overwritten the next time we open the drawer for a
// different passkey, so this will not have a functional impact.
};
const handleSubmit = async (
inputValue: string,
setFieldError: (errorMessage: string) => void,
resetForm: () => void,
) => {
try {
await registerPasskey(inputValue);
} catch (e) {
log.error("Failed to register a new passkey", e);
// TODO-PK: localize
setFieldError("Could not add passkey");
return;
}
await refreshPasskeys();
resetForm();
};
return (
<VerticallyCentered>
<EnteSpinner />
</VerticallyCentered>
<>
<CenteredFlex>
<Box maxWidth="20rem">
<Box marginBottom="1rem">
<Typography>{t("PASSKEYS_DESCRIPTION")}</Typography>
</Box>
<FormPaper style={{ padding: "1rem" }}>
<SingleInputForm
fieldType="text"
placeholder={t("ENTER_PASSKEY_NAME")}
buttonText={t("ADD_PASSKEY")}
initialValue={""}
callback={handleSubmit}
submitButtonProps={{ sx: { marginBottom: 1 } }}
/>
</FormPaper>
<Box marginTop="1rem">
<PasskeysList
passkeys={passkeys}
onSelectPasskey={handleSelectPasskey}
/>
</Box>
</Box>
</CenteredFlex>
<ManagePasskeyDrawer
open={showPasskeyDrawer}
onClose={handleDrawerClose}
passkey={selectedPasskey}
refreshPasskeys={() => void refreshPasskeys}
/>
</>
);
};
export default Page;
interface PasskeysListProps {
/** The list of {@link Passkey}s to show. */
passkeys: Passkey[];
/**
* Callback to invoke when an passkey in the list is clicked.
*
* It is passed the corresponding {@link Passkey}.
*/
onSelectPasskey: (passkey: Passkey) => void;
}
const PasskeysList: React.FC<PasskeysListProps> = ({
passkeys,
onSelectPasskey,
}) => {
return (
<MenuItemGroup>
{passkeys.map((passkey, i) => (
<React.Fragment key={passkey.id}>
<PasskeyListItem
passkey={passkey}
onClick={onSelectPasskey}
/>
{i < passkeys.length - 1 && <MenuItemDivider />}
</React.Fragment>
))}
</MenuItemGroup>
);
};
interface PasskeyListItemProps {
/** The passkey to show in the item. */
passkey: Passkey;
/**
* Callback to invoke when the item is clicked.
*
* It is passed the item's {@link passkey}.
*/
onClick: (passkey: Passkey) => void;
}
const PasskeyListItem: React.FC<PasskeyListItemProps> = ({
passkey,
onClick,
}) => {
return (
<EnteMenuItem
onClick={() => onClick(passkey)}
startIcon={<KeyIcon />}
endIcon={<ChevronRightIcon />}
label={passkey.friendlyName}
/>
);
};
interface ManagePasskeyDrawerProps {
/** If `true`, then the drawer is shown. */
open: boolean;
/*** Callback to invoke when the drawer wants to be closed. */
onClose: () => void;
/**
* The {@link Passkey} whose details should be shown in the drawer.
*
* The cannot be undefined logically, but the types don't currently reflect
* that reality and indicate this as undefined (this is mostly to retain the
* identity of the drawer component in a way that it animates when opening
* and closing instead of instantly getting removed from the DOM).
*/
passkey: Passkey | undefined;
refreshPasskeys: () => void;
}
const ManagePasskeyDrawer: React.FC<ManagePasskeyDrawerProps> = ({
open,
onClose,
passkey,
refreshPasskeys,
}) => {
const selectedPasskey = ensure(passkey);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showRenameDialog, setShowRenameDialog] = useState(false);
const createdAt = formatDateTimeFull(selectedPasskey.createdAt / 1000);
return (
<>
<EnteDrawer anchor="right" {...{ open, onClose }}>
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
// TODO-PK: Localize (more below too)
title="Manage Passkey"
onRootClose={onClose}
/>
<InfoItem
icon={<CalendarTodayIcon />}
title={t("CREATED_AT")}
caption={createdAt}
loading={false}
hideEditOption
/>
<MenuItemGroup>
<EnteMenuItem
onClick={() => {
setShowRenameDialog(true);
}}
startIcon={<EditIcon />}
label={"Rename Passkey"}
/>
<MenuItemDivider />
<EnteMenuItem
onClick={() => {
setShowDeleteDialog(true);
}}
startIcon={<DeleteIcon />}
label={"Delete Passkey"}
color="critical"
/>
</MenuItemGroup>
</Stack>
</EnteDrawer>
<DeletePasskeyDialog
open={showDeleteDialog}
onClose={() => {
setShowDeleteDialog(false);
refreshPasskeys();
}}
passkey={selectedPasskey}
/>
<RenamePasskeyDialog
open={showRenameDialog}
onClose={() => {
setShowRenameDialog(false);
refreshPasskeys();
}}
passkey={selectedPasskey}
/>
</>
);
};
interface DeletePasskeyDialogProps {
/** If `true`, then the dialog is shown. */
open: boolean;
/*** Callback to invoke when the dialog wants to be closed. */
onClose: () => void;
/** The {@link Passkey} to delete. */
passkey: Passkey;
}
const DeletePasskeyDialog: React.FC<DeletePasskeyDialogProps> = ({
open,
onClose,
passkey,
}) => {
const [isDeleting, setIsDeleting] = useState(false);
const fullScreen = useMediaQuery("(max-width: 428px)");
const handleConfirm = async () => {
setIsDeleting(true);
try {
await deletePasskey(passkey.id);
onClose();
} catch (e) {
log.error("Failed to delete passkey", e);
} finally {
setIsDeleting(false);
}
};
return (
<DialogBoxV2
fullWidth
{...{ open, onClose, fullScreen }}
attributes={{ title: t("DELETE_PASSKEY") }}
>
<Stack spacing={"8px"}>
<Typography>{t("DELETE_PASSKEY_CONFIRMATION")}</Typography>
<EnteButton
type="submit"
size="large"
color="critical"
loading={isDeleting}
onClick={handleConfirm}
>
{t("DELETE")}
</EnteButton>
<Button size="large" color={"secondary"} onClick={onClose}>
{t("CANCEL")}
</Button>
</Stack>
</DialogBoxV2>
);
};
interface RenamePasskeyDialogProps {
/** If `true`, then the dialog is shown. */
open: boolean;
/*** Callback to invoke when the dialog wants to be closed. */
onClose: () => void;
/** The {@link Passkey} to rename. */
passkey: Passkey;
}
const RenamePasskeyDialog: React.FC<RenamePasskeyDialogProps> = ({
open,
onClose,
passkey,
}) => {
const fullScreen = useMediaQuery("(max-width: 428px)");
const onSubmit = async (inputValue: string) => {
try {
await renamePasskey(passkey.id, inputValue);
onClose();
} catch (e) {
log.error("Failed to rename passkey", e);
return;
}
};
return (
<DialogBoxV2
fullWidth
{...{ open, onClose, fullScreen }}
attributes={{ title: t("RENAME_PASSKEY") }}
>
<SingleInputForm
initialValue={passkey?.friendlyName}
callback={onSubmit}
placeholder={t("ENTER_PASSKEY_NAME")}
buttonText={t("RENAME")}
fieldType="text"
secondaryButtonAction={onClose}
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
/>
</DialogBoxV2>
);
};

View File

@ -1,371 +0,0 @@
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
import { CenteredFlex } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import EnteButton from "@ente/shared/components/EnteButton";
import { EnteDrawer } from "@ente/shared/components/EnteDrawer";
import FormPaper from "@ente/shared/components/Form/FormPaper";
import InfoItem from "@ente/shared/components/Info/InfoItem";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import MenuItemDivider from "@ente/shared/components/Menu/MenuItemDivider";
import { MenuItemGroup } from "@ente/shared/components/Menu/MenuItemGroup";
import SingleInputForm from "@ente/shared/components/SingleInputForm";
import Titlebar from "@ente/shared/components/Titlebar";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
import { formatDateTimeFull } from "@ente/shared/time/format";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import KeyIcon from "@mui/icons-material/Key";
import { Box, Button, Stack, Typography, useMediaQuery } from "@mui/material";
import { t } from "i18next";
import { useRouter } from "next/router";
import { useAppContext } from "pages/_app";
import React, { useEffect, useState } from "react";
import {
deletePasskey,
registerPasskey,
renamePasskey,
} from "services/passkey";
import { getPasskeys, type Passkey } from "../../services/passkey";
const Page: React.FC = () => {
const { showNavBar } = useAppContext();
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
const [selectedPasskey, setSelectedPasskey] = useState<
Passkey | undefined
>();
const router = useRouter();
const refreshPasskeys = async () => {
try {
setPasskeys((await getPasskeys()) || []);
} catch (e) {
log.error("Failed to fetch passkeys", e);
}
};
useEffect(() => {
if (!getToken()) {
router.push("/login");
return;
}
showNavBar(true);
void refreshPasskeys();
}, []);
const handleSelectPasskey = (passkey: Passkey) => {
setSelectedPasskey(passkey);
setShowPasskeyDrawer(true);
};
const handleDrawerClose = () => {
setShowPasskeyDrawer(false);
// Don't clear the selected passkey, let the stale value be so that the
// drawer closing animation is nicer.
//
// The value will get overwritten the next time we open the drawer for a
// different passkey, so this will not have a functional impact.
};
const handleSubmit = async (
inputValue: string,
setFieldError: (errorMessage: string) => void,
resetForm: () => void,
) => {
try {
await registerPasskey(inputValue);
} catch (e) {
log.error("Failed to register a new passkey", e);
// TODO-PK: localize
setFieldError("Could not add passkey");
return;
}
await refreshPasskeys();
resetForm();
};
return (
<>
<CenteredFlex>
<Box maxWidth="20rem">
<Box marginBottom="1rem">
<Typography>{t("PASSKEYS_DESCRIPTION")}</Typography>
</Box>
<FormPaper style={{ padding: "1rem" }}>
<SingleInputForm
fieldType="text"
placeholder={t("ENTER_PASSKEY_NAME")}
buttonText={t("ADD_PASSKEY")}
initialValue={""}
callback={handleSubmit}
submitButtonProps={{ sx: { marginBottom: 1 } }}
/>
</FormPaper>
<Box marginTop="1rem">
<PasskeysList
passkeys={passkeys}
onSelectPasskey={handleSelectPasskey}
/>
</Box>
</Box>
</CenteredFlex>
<ManagePasskeyDrawer
open={showPasskeyDrawer}
onClose={handleDrawerClose}
passkey={selectedPasskey}
refreshPasskeys={() => void refreshPasskeys}
/>
</>
);
};
export default Page;
interface PasskeysListProps {
/** The list of {@link Passkey}s to show. */
passkeys: Passkey[];
/**
* Callback to invoke when an passkey in the list is clicked.
*
* It is passed the corresponding {@link Passkey}.
*/
onSelectPasskey: (passkey: Passkey) => void;
}
const PasskeysList: React.FC<PasskeysListProps> = ({
passkeys,
onSelectPasskey,
}) => {
return (
<MenuItemGroup>
{passkeys.map((passkey, i) => (
<React.Fragment key={passkey.id}>
<PasskeyListItem
passkey={passkey}
onClick={onSelectPasskey}
/>
{i < passkeys.length - 1 && <MenuItemDivider />}
</React.Fragment>
))}
</MenuItemGroup>
);
};
interface PasskeyListItemProps {
/** The passkey to show in the item. */
passkey: Passkey;
/**
* Callback to invoke when the item is clicked.
*
* It is passed the item's {@link passkey}.
*/
onClick: (passkey: Passkey) => void;
}
const PasskeyListItem: React.FC<PasskeyListItemProps> = ({
passkey,
onClick,
}) => {
return (
<EnteMenuItem
onClick={() => onClick(passkey)}
startIcon={<KeyIcon />}
endIcon={<ChevronRightIcon />}
label={passkey.friendlyName}
/>
);
};
interface ManagePasskeyDrawerProps {
/** If `true`, then the drawer is shown. */
open: boolean;
/*** Callback to invoke when the drawer wants to be closed. */
onClose: () => void;
/**
* The {@link Passkey} whose details should be shown in the drawer.
*
* The cannot be undefined logically, but the types don't currently reflect
* that reality and indicate this as undefined (this is mostly to retain the
* identity of the drawer component in a way that it animates when opening
* and closing instead of instantly getting removed from the DOM).
*/
passkey: Passkey | undefined;
refreshPasskeys: () => void;
}
const ManagePasskeyDrawer: React.FC<ManagePasskeyDrawerProps> = ({
open,
onClose,
passkey,
refreshPasskeys,
}) => {
const selectedPasskey = ensure(passkey);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showRenameDialog, setShowRenameDialog] = useState(false);
const createdAt = formatDateTimeFull(selectedPasskey.createdAt / 1000);
return (
<>
<EnteDrawer anchor="right" {...{ open, onClose }}>
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
// TODO-PK: Localize (more below too)
title="Manage Passkey"
onRootClose={onClose}
/>
<InfoItem
icon={<CalendarTodayIcon />}
title={t("CREATED_AT")}
caption={createdAt}
loading={false}
hideEditOption
/>
<MenuItemGroup>
<EnteMenuItem
onClick={() => {
setShowRenameDialog(true);
}}
startIcon={<EditIcon />}
label={"Rename Passkey"}
/>
<MenuItemDivider />
<EnteMenuItem
onClick={() => {
setShowDeleteDialog(true);
}}
startIcon={<DeleteIcon />}
label={"Delete Passkey"}
color="critical"
/>
</MenuItemGroup>
</Stack>
</EnteDrawer>
<DeletePasskeyDialog
open={showDeleteDialog}
onClose={() => {
setShowDeleteDialog(false);
refreshPasskeys();
}}
passkey={selectedPasskey}
/>
<RenamePasskeyDialog
open={showRenameDialog}
onClose={() => {
setShowRenameDialog(false);
refreshPasskeys();
}}
passkey={selectedPasskey}
/>
</>
);
};
interface DeletePasskeyDialogProps {
/** If `true`, then the dialog is shown. */
open: boolean;
/*** Callback to invoke when the dialog wants to be closed. */
onClose: () => void;
/** The {@link Passkey} to delete. */
passkey: Passkey;
}
const DeletePasskeyDialog: React.FC<DeletePasskeyDialogProps> = ({
open,
onClose,
passkey,
}) => {
const [isDeleting, setIsDeleting] = useState(false);
const fullScreen = useMediaQuery("(max-width: 428px)");
const handleConfirm = async () => {
setIsDeleting(true);
try {
await deletePasskey(passkey.id);
onClose();
} catch (e) {
log.error("Failed to delete passkey", e);
} finally {
setIsDeleting(false);
}
};
return (
<DialogBoxV2
fullWidth
{...{ open, onClose, fullScreen }}
attributes={{ title: t("DELETE_PASSKEY") }}
>
<Stack spacing={"8px"}>
<Typography>{t("DELETE_PASSKEY_CONFIRMATION")}</Typography>
<EnteButton
type="submit"
size="large"
color="critical"
loading={isDeleting}
onClick={handleConfirm}
>
{t("DELETE")}
</EnteButton>
<Button size="large" color={"secondary"} onClick={onClose}>
{t("CANCEL")}
</Button>
</Stack>
</DialogBoxV2>
);
};
interface RenamePasskeyDialogProps {
/** If `true`, then the dialog is shown. */
open: boolean;
/*** Callback to invoke when the dialog wants to be closed. */
onClose: () => void;
/** The {@link Passkey} to rename. */
passkey: Passkey;
}
const RenamePasskeyDialog: React.FC<RenamePasskeyDialogProps> = ({
open,
onClose,
passkey,
}) => {
const fullScreen = useMediaQuery("(max-width: 428px)");
const onSubmit = async (inputValue: string) => {
try {
await renamePasskey(passkey.id, inputValue);
onClose();
} catch (e) {
log.error("Failed to rename passkey", e);
return;
}
};
return (
<DialogBoxV2
fullWidth
{...{ open, onClose, fullScreen }}
attributes={{ title: t("RENAME_PASSKEY") }}
>
<SingleInputForm
initialValue={passkey?.friendlyName}
callback={onSubmit}
placeholder={t("ENTER_PASSKEY_NAME")}
buttonText={t("RENAME")}
fieldType="text"
secondaryButtonAction={onClose}
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
/>
</DialogBoxV2>
);
};

View File

@ -506,10 +506,10 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
// Ente Accounts specific JWT token.
const accountsToken = await getAccountsToken();
const pkg = clientPackageName["photos"];
const client = clientPackageName["photos"];
window.open(
`${accountsAppURL()}/passkeys?token=${accountsToken}&package=${pkg}`,
`${accountsAppURL()}/passkeys/handoff?token=${accountsToken}&client=${client}`,
);
} catch (e) {
log.error("failed to redirect to accounts page", e);

View File

@ -53,9 +53,8 @@ used.** This restriction is a byproduct of the enablement for automatic login.
### Automatically logging into Accounts
Clients open a WebView with the URL
`https://accounts.ente.io/passkeys?token=<accountsToken>&package=<app package name>`.
This page will appear like a normal loading screen to the user, but in the
background, the app parses the token and package for usage in subsequent
`https://accounts.ente.io/passkeys/handoff?client=<clientPackageName>&token=<accountsToken>`.
This page will parse the token and client package name for usage in subsequent
Accounts-related API calls.
If valid, the user will be automatically redirected to the passkeys management
@ -342,7 +341,7 @@ credential authentication. We use Accounts as the central WebAuthn hub because
credentials are locked to an FQDN.
```tsx
window.location.href = `${accountsAppURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
window.location.origin
}/passkeys/finish`;
```

View File

@ -166,7 +166,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
isTwoFactorPasskeysEnabled: true,
});
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.ROOT);
window.location.href = `${accountsAppURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
window.location.origin
}/passkeys/finish`;
return undefined;

View File

@ -85,7 +85,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
isTwoFactorPasskeysEnabled: true,
});
setIsFirstLogin(true);
window.location.href = `${accountsAppURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
window.location.origin
}/passkeys/finish`;
router.push(PAGES.CREDENTIALS);

View File

@ -45,6 +45,5 @@ export enum ACCOUNTS_PAGES {
VERIFY = "/verify",
ROOT = "/",
PASSKEYS = "/passkeys",
ACCOUNT_HANDOFF = "/account-handoff",
GENERATE = "/generate",
}