Skip to content

Commit

Permalink
Reverse anchor input control flow (#925)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nmattia authored Sep 29, 2022
1 parent 5b1b82e commit 771a485
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 217 deletions.
91 changes: 71 additions & 20 deletions src/frontend/src/components/anchorInput.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement> } => {
const divRef = createRef();
export const mkAnchorInput = (props: {
inputId: string;
userNumber?: bigint;
onSubmit?: (userNumber: bigint) => void;
}): {
template: TemplateResult;
userNumberInput: Ref<HTMLInputElement>;
submit: () => void;
readUserNumber: () => bigint | undefined;
} => {
const divRef: Ref<HTMLDivElement> = createRef();
const userNumberInput: Ref<HTMLInputElement> = 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` <div ${ref(divRef)} class="l-stack c-input--anchor">
<label class="c-input--anchor__wrap" aria-label="Identity Anchor">
<input
${ref(userNumberInput)}
type="text"
id="${inputId}"
id="${props.inputId}"
class="c-input c-input--vip"
placeholder="Enter anchor"
value="${userNumber !== undefined ? userNumber : ""}"
value="${props.userNumber !== undefined ? props.userNumber : ""}"
@input=${inputFilter(isDigits, onBadInput)}
@keydown=${inputFilter(isDigits, onBadInput)}
@keyup=${inputFilter(isDigits, onBadInput)}
Expand All @@ -40,16 +98,9 @@ export const mkAnchorInput = (
@keypress=${onKeyPress}
/>
</label>
<p
id="invalidAnchorMessage"
class="anchor-error-message is-hidden t-paragraph t-strong"
>
The Identity Anchor is not valid. Please try again.
</p>
</div>`;

return { template, userNumberInput };
return { template, userNumberInput, submit, readUserNumber };
};

const isDigits = (c: string) => /^\d*\.?\d*$/.test(c);
Expand Down
107 changes: 43 additions & 64 deletions src/frontend/src/flows/addDevice/welcomeView/index.ts
Original file line number Diff line number Diff line change
@@ -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`
<div class="l-container c-card c-card--highlight">
<hgroup>
<h1 class="t-title t-title--main">New Device</h1>
<p class="t-paragraph t-lead">
Please provide the Identity Anchor to which you want to add your device.
</p>
<p id="invalidAnchorMessage" class="is-hidden">
Please enter a valid Identity Anchor.
</p>
</hgroup>
${mkAnchorInput("addDeviceUserNumber", userNumber).template}
<div class="c-button-group">
<button
@click="${
window.location.reload /* TODO: L2-309: do this without reload */
}"
class="c-button c-button--secondary"
id="addDeviceUserNumberCancel"
>
Cancel
</button>
<button
class="c-button c-button--primary"
id="addDeviceUserNumberContinue"
>
Continue
</button>
const pageContent = (connection: Connection, userNumber?: bigint) => {
const anchorInput = mkAnchorInput({
inputId: "addDeviceUserNumber",
userNumber,
onSubmit: (userNumber) => registerTentativeDevice(userNumber, connection),
});

return html`
<div class="l-container c-card c-card--highlight">
<hgroup>
<h1 class="t-title t-title--main">New Device</h1>
<p class="t-paragraph t-lead">
Please provide the Identity Anchor to which you want to add your
device.
</p>
<p id="invalidAnchorMessage" class="is-hidden">
Please enter a valid Identity Anchor.
</p>
</hgroup>
${anchorInput.template}
<div class="c-button-group">
<button
@click="${
window.location.reload /* TODO: L2-309: do this without reload */
}"
class="c-button c-button--secondary"
id="addDeviceUserNumberCancel"
>
Cancel
</button>
<button
@click="${anchorInput.submit}"
class="c-button c-button--primary"
id="addDeviceUserNumberContinue"
>
Continue
</button>
</div>
</div>
</div>
`;
`;
};

/**
* 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> => {
): 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);
};
Loading

0 comments on commit 771a485

Please sign in to comment.