Skip to content

Commit

Permalink
Improve add device flow (#2382)
Browse files Browse the repository at this point in the history
* Improve add device flow

This PR aligns the add device flow closer with the elements and screens
specified in [figma](https://www.figma.com/file/a8jl32LDcYODRwwzrZd5UU/Internet-Identity-Design?type=design&node-id=1013-19687&mode=design&t=Zh0ZcNOnL57MKKqp-0).

The following changes are made:
* there is now a prompt for device trust as a first step (on the new device)
* there is now a stepper for the flow
* the identity number is shown consistently on all screens
* the copy has been changed to the wording specified

There are still visual design differences. These will be solved
separately.

* Improve showcase page
  • Loading branch information
Frederik Rothenberger authored Mar 26, 2024
1 parent bd0dff0 commit 3246e09
Show file tree
Hide file tree
Showing 18 changed files with 250 additions and 25 deletions.
3 changes: 2 additions & 1 deletion src/frontend/src/flows/addDevice/addDeviceSuccess.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"en": {
"title": "You successfully added a new Passkey!",
"continue_to_home": "Continue to Internet Identity",
"explore": "Your new Passkey:"
"explore": "Your new Passkey:",
"internet_identity": "Internet Identity"
}
}
1 change: 1 addition & 0 deletions src/frontend/src/flows/addDevice/addDeviceSuccess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe("addDeviceSuccess", () => {

addDeviceSuccessPage(
{
userNumber: BigInt(123456),
i18n: new I18n(),
deviceAlias: "Test device alias",
onContinue,
Expand Down
23 changes: 17 additions & 6 deletions src/frontend/src/flows/addDevice/addDeviceSuccess.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mainWindow } from "$src/components/mainWindow";
import { I18n } from "$src/i18n";
import { renderPage } from "$src/utils/lit-html";
import { html, render } from "lit-html";
import { html, render, TemplateResult } from "lit-html";
import copyJson from "./addDeviceSuccess.json";

export type DeviceAlias = string;
Expand All @@ -11,20 +11,28 @@ export type AddDeviceSuccessTemplateProps = Parameters<
>[0];

const addDeviceSuccessTemplate = ({
userNumber,
deviceAlias,
onContinue,
stepper,
i18n,
}: {
userNumber: bigint;
deviceAlias: DeviceAlias;
onContinue: () => void;
stepper?: TemplateResult;
i18n: I18n;
}) => {
const { title, continue_to_home, explore } = i18n.i18n(copyJson);
const copy = i18n.i18n(copyJson);

const slot = html`<article>
${stepper}
<hgroup>
<h1 class="t-title t-title--main">${title}</h1>
<p class="t-lead">${explore}</p>
<div class="c-card__label">
<h2>${copy.internet_identity} ${userNumber}</h2>
</div>
<h1 class="t-title t-title--main">${copy.title}</h1>
<p class="t-lead">${copy.explore}</p>
</hgroup>
<output
class="c-input c-input--stack c-input--fullwidth c-input--readonly t-vip t-vip--small"
Expand All @@ -36,7 +44,7 @@ const addDeviceSuccessTemplate = ({
class="c-button c-button--primary"
data-action="next"
>
${continue_to_home}
${copy.continue_to_home}
</button>
</div>
</article>`;
Expand All @@ -51,7 +59,10 @@ const addDeviceSuccessTemplate = ({
export const addDeviceSuccessPage = renderPage(addDeviceSuccessTemplate);

export const addDeviceSuccess = (
props: Pick<AddDeviceSuccessTemplateProps, "deviceAlias">
props: Pick<
AddDeviceSuccessTemplateProps,
"userNumber" | "deviceAlias" | "stepper"
>
): Promise<void> =>
new Promise<void>((resolve) => {
const container = document.getElementById("pageContent") as HTMLElement;
Expand Down
7 changes: 6 additions & 1 deletion src/frontend/src/flows/addDevice/manage/addDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from "$generated/internet_identity_types";
import { displayError } from "$src/components/displayError";
import { withLoader } from "$src/components/loader";
import { tentativeDeviceStepper } from "$src/flows/addDevice/stepper";
import { AuthenticatedConnection } from "$src/utils/iiConnection";
import { isNullish } from "@dfinity/utils";
import { addDeviceSuccess } from "../addDeviceSuccess";
Expand Down Expand Up @@ -70,6 +71,10 @@ export const addDevice = async ({
});

if (result === "verified") {
await addDeviceSuccess({ deviceAlias: alias });
await addDeviceSuccess({
userNumber,
deviceAlias: alias,
stepper: tentativeDeviceStepper({ step: "success" }),
});
}
};
2 changes: 1 addition & 1 deletion src/frontend/src/flows/addDevice/manage/addFIDODevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const addFIDODevice = async (
)
);

await addDeviceSuccess({ deviceAlias: deviceName });
await addDeviceSuccess({ userNumber, deviceAlias: deviceName });
} catch (error: unknown) {
await displayFailedToAddDevice(
error instanceof Error ? error : unknownError()
Expand Down
18 changes: 14 additions & 4 deletions src/frontend/src/flows/addDevice/manage/verifyTentativeDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { displayError } from "$src/components/displayError";
import { withLoader } from "$src/components/loader";
import { mainWindow } from "$src/components/mainWindow";
import { pinInput } from "$src/components/pinInput";
import { tentativeDeviceStepper } from "$src/flows/addDevice/stepper";
import { AsyncCountdown } from "$src/utils/countdown";
import { AuthenticatedConnection } from "$src/utils/iiConnection";
import { renderPage } from "$src/utils/lit-html";
Expand All @@ -22,12 +23,14 @@ type VerifyResultOrRetry =
| { retry: true };

const verifyTentativeDeviceTemplate = <T>({
userNumber,
alias,
remaining,
verify,
doContinue,
cancel,
}: {
userNumber: bigint;
alias: string;
remaining: AsyncIterable<string>;
cancel: () => void;
Expand All @@ -51,9 +54,14 @@ const verifyTentativeDeviceTemplate = <T>({
onSubmit: doContinue,
});

const pageContentSlot = html`<h1 class="t-title t-title--main">
Do you want to create this Passkey for your Internet Identity?
</h1>
const pageContentSlot = html`<article>
${tentativeDeviceStepper({ step: "verify" })}
<hgroup>
<div class="c-card__label">
<h2>Internet Identity ${userNumber}</h2>
</div>
<h1 class="t-title t-title--main">Activate Passkey</h1>
</hgroup>
<output
class="c-input c-input--fullwidth c-input--stack c-input--readonly t-vip t-vip--small"
>${alias}</output
Expand Down Expand Up @@ -85,7 +93,8 @@ const verifyTentativeDeviceTemplate = <T>({
<button @click=${() => cancel()} class="c-button c-button--secondary">
Cancel
</button>
</div>`;
</div>
</article>`;

return mainWindow({
showLogo: false,
Expand Down Expand Up @@ -124,6 +133,7 @@ export const verifyTentativeDevice = async ({
AsyncCountdown.fromNanos(endTimestamp);

verifyTentativeDevicePage<VerifyResult>({
userNumber: connection.userNumber,
alias,
cancel: async () => {
await withLoader(() => connection.exitDeviceRegistrationMode());
Expand Down
26 changes: 26 additions & 0 deletions src/frontend/src/flows/addDevice/stepper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { checkmarkIcon } from "$src/components/icons";
import { html } from "lit-html";

export const tentativeDeviceStepper = ({
step,
}: {
step: "activate" | "verify" | "success";
}) => html`
<div class="c-progress-container">
<ol class="c-progress-stepper">
<li class="c-progress-stepper__step" aria-current=${step === "activate"}>
<span class="c-progress-stepper__label">Activate Passkey</span>
</li>
<li class="c-progress-stepper__step" aria-current=${step === "verify"}>
<span class="c-progress-stepper__label">Verify Device</span>
</li>
<li
class="c-progress-stepper__step c-progress-stepper__step--final"
aria-current=${step === "success"}
>
<i class="c-progress-stepper__icon">${checkmarkIcon}</i>
<span class="c-progress-stepper__label">Passkey Activated</span>
</li>
</ol>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"en": {
"internet_identity": "Internet Identity",
"activate_passkey": "Activate Passkey on this device",
"trust_this_device": "Do you trust this device to connect to your Internet Identity?",
"yes": "Yes",
"no": "No"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { mainWindow } from "$src/components/mainWindow";
import { tentativeDeviceStepper } from "$src/flows/addDevice/stepper";
import copyJson from "$src/flows/addDevice/welcomeView/promptDeviceTrusted.json";
import { I18n } from "$src/i18n";
import { renderPage } from "$src/utils/lit-html";
import { html } from "lit-html";

export type PromptDeviceTrustedTemplateProps = Parameters<
typeof promptDeviceTrustedTemplate
>[0];

const promptDeviceTrustedTemplate = ({
userNumber,
confirm,
cancel,
i18n,
}: {
userNumber: bigint;
confirm: () => void;
cancel: () => void;
i18n: I18n;
}) => {
const copy = i18n.i18n(copyJson);

const pageContentSlot = html` <article>
${tentativeDeviceStepper({ step: "activate" })}
<hgroup>
<div class="c-card__label">
<h2>${copy.internet_identity} ${userNumber}</h2>
</div>
<h1 class="t-title t-title--main">${copy.activate_passkey}</h1>
</hgroup>
<p class="t-paragraph">${copy.trust_this_device}</p>
<div class="l-stack">
<button
id="trustDeviceConfirm"
class="c-button"
@click=${() => confirm()}
>
${copy.yes}
</button>
<button
id="trustDeviceCancel"
class="c-button c-button--secondary"
@click=${() => cancel()}
>
${copy.no}
</button>
</div>
</article>`;

return mainWindow({
showLogo: false,
showFooter: false,
slot: pageContentSlot,
});
};

export const promptDeviceTrustedPage = renderPage(promptDeviceTrustedTemplate);

/**
* Page to prompt the user whether they trust the current device.
*/
export const promptDeviceTrusted = (
props: Pick<PromptDeviceTrustedTemplateProps, "userNumber">
): Promise<"confirmed" | "canceled"> => {
return new Promise((resolve) =>
promptDeviceTrustedPage({
...props,
confirm: () => resolve("confirmed"),
cancel: () => resolve("canceled"),
i18n: new I18n(),
})
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
import { displayError } from "$src/components/displayError";
import { withLoader } from "$src/components/loader";
import { addDeviceSuccess } from "$src/flows/addDevice/addDeviceSuccess";
import { tentativeDeviceStepper } from "$src/flows/addDevice/stepper";
import { promptDeviceTrusted } from "$src/flows/addDevice/welcomeView/promptDeviceTrusted";
import { inferPasskeyAlias, loadUAParser } from "$src/flows/register";
import { setAnchorUsed } from "$src/storage";
import { authenticatorAttachmentToKeyType } from "$src/utils/authenticatorAttachment";
Expand All @@ -23,11 +25,12 @@ import { showVerificationCode } from "./showVerificationCode";

/**
* Runs the tentative device registration flow on a _new_ device:
* 1. The user interacts with the authenticator to create a new credential.
* 2. After adding it tentatively, the user is prompted to enter the verification
* 1. The user is prompted to confirm this device is trusted.
* 2. The user interacts with the authenticator to create a new credential.
* 3. After adding it tentatively, the user is prompted to enter the verification
* code on an existing device.
* 3. This flows polls for the user to complete the verification.
* 4. Once verification is completed, a success screen is shown.
* 4. This flows polls for the user to complete the verification.
* 5. Once verification is completed, a success screen is shown.
*
* If the user cancels at any point, the flow is aborted.
*
Expand All @@ -41,6 +44,12 @@ export const registerTentativeDevice = async (
// Kick-off fetching "ua-parser-js";
const uaParser = loadUAParser();

const deviceTrusted = await promptDeviceTrusted({ userNumber });
if (deviceTrusted === "canceled") {
return { tag: "canceled" };
}
deviceTrusted satisfies "confirmed";

// Then, we create local WebAuthn credentials for the device
const result = await withLoader(() =>
createDevice({ userNumber, connection })
Expand Down Expand Up @@ -111,7 +120,11 @@ export const registerTentativeDevice = async (

verificationCodeResult satisfies "ok";

await addDeviceSuccess({ deviceAlias: alias });
await addDeviceSuccess({
userNumber,
deviceAlias: alias,
stepper: tentativeDeviceStepper({ step: "success" }),
});
return { tag: "deviceAdded" };
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { displayError } from "$src/components/displayError";
import { mainWindow } from "$src/components/mainWindow";
import { toast } from "$src/components/toast";
import { tentativeDeviceStepper } from "$src/flows/addDevice/stepper";
import { setAnchorUsed } from "$src/storage";
import { AsyncCountdown } from "$src/utils/countdown";
import { Connection } from "$src/utils/iiConnection";
Expand All @@ -20,17 +21,24 @@ type TentativeRegistrationInfo = Extract<
>["added_tentatively"];

const showVerificationCodeTemplate = ({
userNumber,
alias,
tentativeRegistrationInfo,
remaining,
cancel,
}: {
userNumber: bigint;
alias: string;
tentativeRegistrationInfo: TentativeRegistrationInfo;
remaining: AsyncIterable<string>;
cancel: () => void;
}) => {
const pageContentSlot = html` <hgroup>
const pageContentSlot = html`<article>
${tentativeDeviceStepper({ step: "verify" })}
<hgroup>
<div class="c-card__label">
<h2>Internet Identity ${userNumber}</h2>
</div>
<h1 class="t-title t-title--main">Verify New Passkey</h1>
<p class="t-paragraph">Your new Passkey:</p>
<output
Expand Down Expand Up @@ -58,7 +66,8 @@ const showVerificationCodeTemplate = ({
Cancel
</button>
</div>
</div>`;
</div>
</article>`;

return mainWindow({
showLogo: false,
Expand Down Expand Up @@ -91,6 +100,7 @@ export const showVerificationCode = async (
);

showVerificationCodePage({
userNumber,
alias,
tentativeRegistrationInfo,
remaining: countdown.remainingFormattedAsync(),
Expand Down
Loading

0 comments on commit 3246e09

Please sign in to comment.