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(() => { useEffect(() => {
window.location.href = window.location.href.replace( window.location.href = window.location.href.replace(
"account-handoff", "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 log from "@/next/log";
import { VerticallyCentered } from "@ente/shared/components/Container"; import { ensure } from "@/utils/ensure";
import EnteSpinner from "@ente/shared/components/EnteSpinner"; import { CenteredFlex } from "@ente/shared/components/Container";
import HTTPService from "@ente/shared/network/HTTPService"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage"; 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 { 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 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 router = useRouter();
useEffect(() => { const refreshPasskeys = async () => {
const urlParams = new URLSearchParams(window.location.search); try {
setPasskeys((await getPasskeys()) || []);
const pkg = urlParams.get("package"); } catch (e) {
if (pkg) { log.error("Failed to fetch passkeys", e);
// TODO-PK: mobile is not passing it. is that expected?
setData(LS_KEYS.CLIENT_PACKAGE, { name: pkg });
HTTPService.setHeaders({
"X-Client-Package": pkg,
});
} }
};
const token = urlParams.get("token"); useEffect(() => {
if (!token) { if (!getToken()) {
log.error("Missing accounts token");
router.push("/login"); router.push("/login");
return; return;
} }
const user = getData(LS_KEYS.USER) || {}; showNavBar(true);
user.token = token; void refreshPasskeys();
setData(LS_KEYS.USER, user);
router.push("/passkeys/setup");
}, []); }, []);
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 ( return (
<VerticallyCentered> <>
<EnteSpinner /> <CenteredFlex>
</VerticallyCentered> <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; 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. // Ente Accounts specific JWT token.
const accountsToken = await getAccountsToken(); const accountsToken = await getAccountsToken();
const pkg = clientPackageName["photos"]; const client = clientPackageName["photos"];
window.open( window.open(
`${accountsAppURL()}/passkeys?token=${accountsToken}&package=${pkg}`, `${accountsAppURL()}/passkeys/handoff?token=${accountsToken}&client=${client}`,
); );
} catch (e) { } catch (e) {
log.error("failed to redirect to accounts page", 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 ### Automatically logging into Accounts
Clients open a WebView with the URL Clients open a WebView with the URL
`https://accounts.ente.io/passkeys?token=<accountsToken>&package=<app package name>`. `https://accounts.ente.io/passkeys/handoff?client=<clientPackageName>&token=<accountsToken>`.
This page will appear like a normal loading screen to the user, but in the This page will parse the token and client package name for usage in subsequent
background, the app parses the token and package for usage in subsequent
Accounts-related API calls. Accounts-related API calls.
If valid, the user will be automatically redirected to the passkeys management 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. credentials are locked to an FQDN.
```tsx ```tsx
window.location.href = `${accountsAppURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${ window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
window.location.origin window.location.origin
}/passkeys/finish`; }/passkeys/finish`;
``` ```

View File

@ -166,7 +166,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
isTwoFactorPasskeysEnabled: true, isTwoFactorPasskeysEnabled: true,
}); });
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.ROOT); 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 window.location.origin
}/passkeys/finish`; }/passkeys/finish`;
return undefined; return undefined;

View File

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

View File

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