From 771a485ed167b7066f6b86af5d087adb14df41ed Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Thu, 29 Sep 2022 16:07:20 +0200 Subject: [PATCH] Reverse anchor input control flow (#925) * Use obj as props in mkAnchorInput * Extend anchorInput to avoid global IDs * Use proper mkAnchorInput in unknown login * Use proper z-inder for anchor tooltip * Remove unused dependency * Select anchorInput in unknown login --- src/frontend/src/components/anchorInput.ts | 91 +++++++++++---- .../src/flows/addDevice/welcomeView/index.ts | 107 +++++++----------- src/frontend/src/flows/authenticate/index.ts | 103 ++++++++--------- src/frontend/src/flows/login/unknownAnchor.ts | 77 +++++-------- src/frontend/src/flows/promptUserNumber.ts | 34 ++---- src/frontend/src/showcase.ts | 1 + src/frontend/src/styles/main.css | 3 +- 7 files changed, 199 insertions(+), 217 deletions(-) diff --git a/src/frontend/src/components/anchorInput.ts b/src/frontend/src/components/anchorInput.ts index a7b7881e12..7f6b9c67f6 100644 --- a/src/frontend/src/components/anchorInput.ts +++ b/src/frontend/src/components/anchorInput.ts @@ -1,33 +1,91 @@ import { html, TemplateResult } from "lit-html"; +import { withRef } from "../utils/utils"; import { createRef, ref, Ref } from "lit-html/directives/ref.js"; +import { parseUserNumber } from "../utils/userNumber"; /** A component for inputting an anchor number */ -export const mkAnchorInput = ( - inputId: string, - userNumber?: bigint, - onKeyPress?: (e: KeyboardEvent) => void -): { template: TemplateResult; userNumberInput: Ref } => { - const divRef = createRef(); +export const mkAnchorInput = (props: { + inputId: string; + userNumber?: bigint; + onSubmit?: (userNumber: bigint) => void; +}): { + template: TemplateResult; + userNumberInput: Ref; + submit: () => void; + readUserNumber: () => bigint | undefined; +} => { + const divRef: Ref = createRef(); const userNumberInput: Ref = createRef(); + const showHint = (message: string) => { + withRef(divRef, (div) => { + if (!div.classList.contains("flash-error")) { + div.setAttribute("data-hint", message); + div.classList.add("flash-error"); + setTimeout(() => div.classList.remove("flash-error"), 2000); + } + }); + }; + + // When "submitting" (either .submit() is called or enter is typed) + // parse the value and call onSubmit + const submit = () => { + const result = readAndParseValue(); + if (result === "invalid") { + return showHint("Invalid Anchor"); + } + if (result === undefined) { + return showHint("Please enter an Anchor"); + } + props.onSubmit?.(result); + }; + // How we react on unexpected (i.e. non-digit) input const onBadInput = () => { - const div = divRef.value; - if (div !== undefined && !div.classList.contains("flash-error")) { - div.classList.add("flash-error"); - setTimeout(() => div.classList.remove("flash-error"), 2000); + showHint("Anchors only consist of digits"); + }; + + // When enter is pressed, submit + const onKeyPress = (e: KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + submit(); } }; + // Helper for reading the anchor/user number from the input + const readAndParseValue = (): undefined | "invalid" | bigint => { + return withRef(userNumberInput, (userNumberInput) => { + const value = userNumberInput.value; + if (value === "") { + return undefined; + } + const parsed = parseUserNumber(value); + if (parsed === null) { + return "invalid"; + } + return parsed; + }); + }; + + // Read user number if submit() shouldn't be called for some reason + const readUserNumber = () => { + const result = readAndParseValue(); + if (result === "invalid") { + return undefined; + } + return result; + }; + const template = html`
- -
`; - return { template, userNumberInput }; + return { template, userNumberInput, submit, readUserNumber }; }; const isDigits = (c: string) => /^\d*\.?\d*$/.test(c); diff --git a/src/frontend/src/flows/addDevice/welcomeView/index.ts b/src/frontend/src/flows/addDevice/welcomeView/index.ts index 46b862a525..ab71dedf9c 100644 --- a/src/frontend/src/flows/addDevice/welcomeView/index.ts +++ b/src/frontend/src/flows/addDevice/welcomeView/index.ts @@ -1,79 +1,58 @@ import { html, render } from "lit-html"; -import { parseUserNumber } from "../../../utils/userNumber"; import { registerTentativeDevice } from "./registerTentativeDevice"; -import { toggleErrorMessage } from "../../../utils/errorHelper"; import { mkAnchorInput } from "../../../components/anchorInput"; import { Connection } from "../../../utils/iiConnection"; -const pageContent = (userNumber?: bigint) => html` -
-
-

New Device

-

- Please provide the Identity Anchor to which you want to add your device. -

- -
- ${mkAnchorInput("addDeviceUserNumber", userNumber).template} -
- - +const pageContent = (connection: Connection, userNumber?: bigint) => { + const anchorInput = mkAnchorInput({ + inputId: "addDeviceUserNumber", + userNumber, + onSubmit: (userNumber) => registerTentativeDevice(userNumber, connection), + }); + + return html` +
+
+

New Device

+

+ Please provide the Identity Anchor to which you want to add your + device. +

+ +
+ ${anchorInput.template} +
+ + +
-
-`; + `; +}; /** * Entry point for the flow of adding a new authenticator when starting from the welcome view (by clicking 'Already have an anchor but using a new device?'). * This shows a prompt to enter the identity anchor to add this new device to. */ -export const addRemoteDevice = async ( +export const addRemoteDevice = ( connection: Connection, userNumber?: bigint -): Promise => { +): void => { const container = document.getElementById("pageContent") as HTMLElement; - render(pageContent(userNumber), container); - return init(connection); -}; - -const init = (connection: Connection) => { - const continueButton = document.getElementById( - "addDeviceUserNumberContinue" - ) as HTMLButtonElement; - const userNumberInput = document.getElementById( - "addDeviceUserNumber" - ) as HTMLInputElement; - - userNumberInput.onkeypress = (e) => { - // submit if user hits enter - if (e.key === "Enter") { - e.preventDefault(); - continueButton.click(); - } - }; - - continueButton.onclick = async () => { - const userNumber = parseUserNumber(userNumberInput.value); - if (userNumber !== null) { - toggleErrorMessage("addDeviceUserNumber", "invalidAnchorMessage", false); - await registerTentativeDevice(userNumber, connection); - } else { - toggleErrorMessage("addDeviceUserNumber", "invalidAnchorMessage", true); - userNumberInput.placeholder = "Please enter your Identity Anchor first"; - } - }; + render(pageContent(connection, userNumber), container); }; diff --git a/src/frontend/src/flows/authenticate/index.ts b/src/frontend/src/flows/authenticate/index.ts index adee54f3e2..05c24eec11 100644 --- a/src/frontend/src/flows/authenticate/index.ts +++ b/src/frontend/src/flows/authenticate/index.ts @@ -1,11 +1,7 @@ import { html, render, TemplateResult } from "lit-html"; import { icLogo, attentionIcon } from "../../components/icons"; import { footer } from "../../components/footer"; -import { - getUserNumber, - parseUserNumber, - setUserNumber, -} from "../../utils/userNumber"; +import { getUserNumber, setUserNumber } from "../../utils/userNumber"; import { withLoader } from "../../components/loader"; import { mkAnchorInput } from "../../components/anchorInput"; import { AuthenticatedConnection, Connection } from "../../utils/iiConnection"; @@ -17,7 +13,6 @@ import { import { displayError } from "../../components/displayError"; import { useRecovery } from "../recovery/useRecovery"; import waitForAuthRequest, { AuthContext } from "./postMessageInterface"; -import { toggleErrorMessage } from "../../utils/errorHelper"; import { fetchDelegation } from "./fetchDelegation"; import { registerIfAllowed } from "../../utils/registerAllowedCheck"; import { @@ -35,6 +30,7 @@ type PageElements = { const pageContent = ( connection: Connection, hostName: string, + onContinue: (arg: bigint) => void, userNumber?: bigint, derivationOrigin?: string ): PageElements & { template: TemplateResult } => { @@ -43,18 +39,17 @@ const pageContent = ( window.location.reload(); }; - const onRecoverClick = () => useRecovery(connection, readUserNumber()); - const authorizeButton: Ref = createRef(); const registerButton: Ref = createRef(); - const anchorInput = mkAnchorInput("userNumberInput", userNumber, (e) => { - if (e.key === "Enter") { - // authenticate if user hits enter - e.preventDefault(); - withRef(authorizeButton, (authorizeButton) => authorizeButton.click()); - } + const anchorInput = mkAnchorInput({ + inputId: "userNumberInput", + userNumber, + onSubmit: onContinue, }); + const onRecoverClick = () => + useRecovery(connection, anchorInput.readUserNumber()); + const template = html`

Internet Identity

@@ -77,7 +72,12 @@ const pageContent = ( ${anchorInput.template} -
@@ -191,21 +191,29 @@ const init = ( authContext: AuthContext, userNumber?: bigint ): Promise => { - const { authorizeButton, userNumberInput, registerButton } = displayPage( - connection, - authContext.requestOrigin, - userNumber, - authContext.authRequest.derivationOrigin - ); + return new Promise((resolve) => { + const { authorizeButton, userNumberInput, registerButton } = displayPage( + connection, + authContext.requestOrigin, + async (userNumber) => { + const authSuccess = await authenticateUser( + connection, + authContext, + userNumber + ); + resolve(authSuccess); + }, + userNumber, + authContext.authRequest.derivationOrigin + ); - // only focus on the button if the anchor is set and was previously used successfully (i.e. is in local storage) - if (userNumber !== undefined && userNumber === getUserNumber()) { - withRef(authorizeButton, (authorizeButton) => authorizeButton.focus()); - } else { - withRef(userNumberInput, (userNumberInput) => userNumberInput.select()); - } + // only focus on the button if the anchor is set and was previously used successfully (i.e. is in local storage) + if (userNumber !== undefined && userNumber === getUserNumber()) { + withRef(authorizeButton, (authorizeButton) => authorizeButton.focus()); + } else { + withRef(userNumberInput, (userNumberInput) => userNumberInput.select()); + } - return new Promise((resolve) => { // Resolve either on successful authentication or after registration withRef(registerButton, (registerButton) => initRegistration( @@ -215,15 +223,6 @@ const init = ( userNumber ).then(resolve) ); - withRef(authorizeButton, (authorizeButton) => { - authorizeButton.onclick = () => { - authenticateUser(connection, authContext).then((authSuccess) => { - if (authSuccess !== null) { - resolve(authSuccess); - } - }); - }; - }); }); }; @@ -259,14 +258,10 @@ const initRegistration = async ( const authenticateUser = async ( connection: Connection, - authContext: AuthContext -): Promise => { - const userNumber = readUserNumber(); + authContext: AuthContext, + userNumber: bigint +): Promise => { try { - if (userNumber === undefined) { - toggleErrorMessage("userNumberInput", "invalidAnchorMessage", true); - return null; - } const result = await withLoader(() => connection.login(userNumber)); const loginResult = apiResultToLoginFlowResult(result); if (loginResult.tag === "ok") { @@ -294,11 +289,18 @@ const authenticateUser = async ( export const displayPage = ( connection: Connection, origin: string, + onContinue: (arg: bigint) => void, userNumber?: bigint, derivationOrigin?: string ): PageElements => { const container = document.getElementById("pageContent") as HTMLElement; - const ret = pageContent(connection, origin, userNumber, derivationOrigin); + const ret = pageContent( + connection, + origin, + onContinue, + userNumber, + derivationOrigin + ); render(ret.template, container); return ret; @@ -325,14 +327,3 @@ async function handleAuthSuccess( }), }; } - -/** - * Read and parse the user number from the input field. - */ -const readUserNumber = () => { - const parsedUserNumber = parseUserNumber( - (document.getElementById("userNumberInput") as HTMLInputElement).value - ); - // get rid of null, we use undefined for 'not set' - return parsedUserNumber === null ? undefined : parsedUserNumber; -}; diff --git a/src/frontend/src/flows/login/unknownAnchor.ts b/src/frontend/src/flows/login/unknownAnchor.ts index 66089e7715..0344cd29bc 100644 --- a/src/frontend/src/flows/login/unknownAnchor.ts +++ b/src/frontend/src/flows/login/unknownAnchor.ts @@ -13,11 +13,16 @@ import { addRemoteDevice } from "../addDevice/welcomeView"; import { registerIfAllowed } from "../../utils/registerAllowedCheck"; import { withRef } from "../../utils/utils"; -const pageContent = (): { +const pageContent = (props: { + onContinue: (res: bigint) => void; +}): { template: TemplateResult; userNumberInput: Ref; } => { - const anchorInput = mkAnchorInput("registerUserNumber"); + const anchorInput = mkAnchorInput({ + inputId: "registerUserNumber", + onSubmit: props.onContinue, + }); const template = html`
@@ -28,7 +33,7 @@ const pageContent = (): {
${anchorInput.template} -
@@ -63,21 +68,21 @@ const pageContent = (): { export const loginUnknownAnchor = async ( connection: Connection -): Promise => { - const container = document.getElementById("pageContent") as HTMLElement; - const content = pageContent(); - render(content.template, container); - return new Promise((resolve, reject) => { - initLogin( - connection, - { userNumberInput: content.userNumberInput }, - resolve +): Promise => + new Promise((resolve, reject) => { + const container = document.getElementById("pageContent") as HTMLElement; + const content = pageContent({ + onContinue: (userNumber) => resolve(doLogin(userNumber, connection)), + }); + render(content.template, container); + // always select the input + withRef(content.userNumberInput, (userNumberInput) => + userNumberInput.select() ); initLinkDevice(connection); initRegister(connection, resolve, reject); initRecovery(connection); }); -}; const initRegister = ( connection: Connection, @@ -107,43 +112,15 @@ const initRecovery = (connection: Connection) => { recoverButton.onclick = () => useRecovery(connection); }; -const initLogin = ( - connection: Connection, - { userNumberInput }: { userNumberInput: Ref }, - resolve: (res: LoginFlowResult) => void -) => { - const loginButton = document.getElementById( - "loginButton" - ) as HTMLButtonElement; - - withRef(userNumberInput, (userNumberInput) => { - userNumberInput.onkeypress = (e) => { - // submit if user hits enter - if (e.key === "Enter") { - e.preventDefault(); - loginButton.click(); - } - }; - - // always select the input - userNumberInput.select(); - - loginButton.onclick = async () => { - const userNumber = parseUserNumber(userNumberInput.value); - if (userNumber === null) { - return resolve({ - tag: "err", - title: "Please enter a valid Identity Anchor", - message: `${userNumber} doesn't parse as a number`, - }); - } - const result = await withLoader(() => connection.login(userNumber)); - if (result.kind === "loginSuccess") { - setUserNumber(userNumber); - } - resolve(apiResultToLoginFlowResult(result)); - }; - }); +const doLogin = async ( + userNumber: bigint, + connection: Connection +): Promise => { + const result = await withLoader(() => connection.login(userNumber)); + if (result.kind === "loginSuccess") { + setUserNumber(userNumber); + } + return apiResultToLoginFlowResult(result); }; const initLinkDevice = (connection: Connection) => { diff --git a/src/frontend/src/flows/promptUserNumber.ts b/src/frontend/src/flows/promptUserNumber.ts index 59e1ea5ce9..79b9b90e02 100644 --- a/src/frontend/src/flows/promptUserNumber.ts +++ b/src/frontend/src/flows/promptUserNumber.ts @@ -1,6 +1,5 @@ import { html, render, TemplateResult } from "lit-html"; import { Ref, ref, createRef } from "lit-html/directives/ref.js"; -import { parseUserNumber } from "../utils/userNumber"; import { withRef } from "../utils/utils"; import { mkAnchorInput } from "../components/anchorInput"; @@ -10,30 +9,13 @@ const pageContent = ( callbacks: { onContinue: (ret: bigint) => void; onCancel: () => void } ): { template: TemplateResult; userNumberInput: Ref } => { const userNumberContinue: Ref = createRef(); - const anchorInput = mkAnchorInput( - "userNumberInput", - userNumber ?? undefined, - (e) => { - // submit if user hits enter - if (e.key === "Enter") { - e.preventDefault(); - withRef(userNumberContinue, (userNumberContinue) => - userNumberContinue.click() - ); - } - } - ); - - const onContinue = () => - withRef(anchorInput.userNumberInput, (userNumberInput) => { - const userNumber = parseUserNumber(userNumberInput.value); - if (userNumber !== null) { - callbacks.onContinue(userNumber); - } else { - userNumberInput.classList.toggle("has-error", true); - userNumberInput.placeholder = "Please enter an Identity Anchor first"; - } - }); + const anchorInput = mkAnchorInput({ + inputId: "userNumberInput", + userNumber: userNumber ?? undefined, + onSubmit: (userNumber: bigint) => { + callbacks.onContinue(userNumber); + }, + }); const template = html`
@@ -52,7 +34,7 @@ const pageContent = (