Skip to content

Commit

Permalink
Extract app wrapper (#2060)
Browse files Browse the repository at this point in the history
This extracts everything related to setting up a single-page II app
(preloading some assets, setting up polyfills, compatibility checks etc)
into a helper function.

This is preparation work for adding new endpoints, so that they won't
need to import/use the main app code (more readable + smaller bundlers).

An apparently unnecessary endpoint (`#compatibilityNotice`) was also
removed.
  • Loading branch information
nmattia authored Nov 17, 2023
1 parent 1875a29 commit 1529c7f
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 116 deletions.
2 changes: 1 addition & 1 deletion src/frontend/src/flows/manage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const authFlowManage = async (connection: Connection) => {
await recoveryWizard(userNumber, authenticatedConnection);
}
// From here on, the user is authenticated to II.
void renderManage({
return renderManage({
userNumber,
connection: authenticatedConnection,
identityBackground,
Expand Down
122 changes: 12 additions & 110 deletions src/frontend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,16 @@
import loaderUrl from "$src/components/loader.webp";
import {
handleLogin,
handleLoginFlowResult,
} from "$src/components/authenticateBox";
import { addDeviceSuccess } from "$src/flows/addDevice/manage/addDeviceSuccess";
import { showWarningIfNecessary } from "./banner";
import { displayError } from "./components/displayError";
import { anyFeatures, features } from "./features";
import { nonNullish } from "@dfinity/utils";
import { registerTentativeDevice } from "./flows/addDevice/welcomeView/registerTentativeDevice";
import { authFlowAuthorize } from "./flows/authorize";
import { compatibilityNotice } from "./flows/compatibilityNotice";
import { authFlowManage, renderManageWarmup } from "./flows/manage";
import "./styles/main.css";
import { createSpa } from "./spa";
import { getAddDeviceAnchor } from "./utils/addDeviceLink";
import { checkRequiredFeatures } from "./utils/featureDetection";
import { Connection } from "./utils/iiConnection";
import { version } from "./version";

import { isNullish, nonNullish } from "@dfinity/utils";

// Polyfill Buffer globally for the browser
import {
handleLogin,
handleLoginFlowResult,
} from "$src/components/authenticateBox";
import { Buffer } from "buffer";
globalThis.Buffer = Buffer;

/** Reads the canister ID from the <script> tag.
*
* The canister injects the canister ID as a `data-canister-id` attribute on the script tag, which we then read to figure out where to make the IC calls.
*/
const readCanisterId = (): string => {
// The backend uses a known element ID so that we can pick up the value from here
const setupJs = document.querySelector(
"[data-canister-id]"
) as HTMLElement | null;
if (isNullish(setupJs) || isNullish(setupJs.dataset.canisterId)) {
void displayError({
title: "Canister ID not set",
message:
"There was a problem contacting the IC. The host serving this page did not give us a canister ID. Try reloading the page and contact support if the problem persists.",
primaryButton: "Reload",
}).then(() => {
window.location.reload();
});
throw new Error("canisterId is undefined"); // abort further execution of this script
}

return setupJs.dataset.canisterId;
};

// Show version information for the curious programmer
const printDevMessage = () => {
console.log("Welcome to Internet Identity!");
console.log(
"The code can be found here: https://github.com/dfinity/internet-identity"
);
console.log(
`https://github.com/dfinity/internet-identity/commit/${version.commit}`
);
if (nonNullish(version.release)) {
console.log(`This is version ${version.release}`);
}
if (version.dirty) {
console.warn("This version is dirty");
}

if (anyFeatures()) {
const message = `
Some features are enabled:
${Object.entries(features)
.map(([k, v]) => ` - ${k}: ${v}`)
.join("\n")}
see more at https://github.com/dfinity/internet-identity#features
`;
console.warn(message);
}
};

// Ensure the loader/spinner assets are ready when called
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload
const preloadLoaderImage = () => {
const link = document.createElement("link");
link.setAttribute("rel", "preload");
link.setAttribute("href", loaderUrl);
link.setAttribute("as", "image");
document.head.append(link);
};

const init = async () => {
try {
printDevMessage();
} catch (e) {
console.warn("Error when printing version information:", e);
}

// Preload the loader
preloadLoaderImage();

const url = new URL(document.URL);

// If the build is not "official", show a warning
// https://github.com/dfinity/internet-identity#build-features
showWarningIfNecessary();

const okOrReason = await checkRequiredFeatures(url);
if (okOrReason !== true) {
return compatibilityNotice(okOrReason);
}

// Prepare the actor/connection to talk to the canister
const connection = new Connection(readCanisterId());

void createSpa(async (connection) => {
// Figure out if user is trying to add a device. If so, use the anchor from the URL.
const addDeviceAnchor = getAddDeviceAnchor();
if (nonNullish(addDeviceAnchor)) {
Expand Down Expand Up @@ -139,14 +41,14 @@ const init = async () => {
}
}

const url = new URL(document.URL);

// Simple, #-based routing
if (url.hash === "#authorize") {
// User was brought here by a dapp for authorization
void authFlowAuthorize(connection);
return authFlowAuthorize(connection);
} else {
// The default flow
void authFlowManage(connection);
return authFlowManage(connection);
}
};

void init();
});
105 changes: 105 additions & 0 deletions src/frontend/src/spa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import loaderUrl from "$src/components/loader.webp";
import { showWarningIfNecessary } from "./banner";
import { displayError } from "./components/displayError";
import { anyFeatures, features } from "./features";
import { compatibilityNotice } from "./flows/compatibilityNotice";
import "./styles/main.css";
import { checkRequiredFeatures } from "./utils/featureDetection";
import { Connection } from "./utils/iiConnection";
import { version } from "./version";

import { isNullish, nonNullish } from "@dfinity/utils";

// Polyfill Buffer globally for the browser
import { Buffer } from "buffer";
globalThis.Buffer = Buffer;

/** Reads the canister ID from the <script> tag.
*
* The canister injects the canister ID as a `data-canister-id` attribute on the script tag, which we then read to figure out where to make the IC calls.
*/
const readCanisterId = (): string => {
// The backend uses a known element ID so that we can pick up the value from here
const setupJs = document.querySelector(
"[data-canister-id]"
) as HTMLElement | null;
if (isNullish(setupJs) || isNullish(setupJs.dataset.canisterId)) {
void displayError({
title: "Canister ID not set",
message:
"There was a problem contacting the IC. The host serving this page did not give us a canister ID. Try reloading the page and contact support if the problem persists.",
primaryButton: "Reload",
}).then(() => {
window.location.reload();
});
throw new Error("canisterId is undefined"); // abort further execution of this script
}

return setupJs.dataset.canisterId;
};

// Show version information for the curious programmer
const printDevMessage = () => {
console.log("Welcome to Internet Identity!");
console.log(
"The code can be found here: https://github.com/dfinity/internet-identity"
);
console.log(
`https://github.com/dfinity/internet-identity/commit/${version.commit}`
);
if (nonNullish(version.release)) {
console.log(`This is version ${version.release}`);
}
if (version.dirty) {
console.warn("This version is dirty");
}

if (anyFeatures()) {
const message = `
Some features are enabled:
${Object.entries(features)
.map(([k, v]) => ` - ${k}: ${v}`)
.join("\n")}
see more at https://github.com/dfinity/internet-identity#features
`;
console.warn(message);
}
};

// Ensure the loader/spinner assets are ready when called
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload
const preloadLoaderImage = () => {
const link = document.createElement("link");
link.setAttribute("rel", "preload");
link.setAttribute("href", loaderUrl);
link.setAttribute("as", "image");
document.head.append(link);
};

// Create a single page app with canister connection
export const createSpa = async (
app: (connection: Connection) => Promise<never>
) => {
try {
printDevMessage();
} catch (e) {
console.warn("Error when printing version information:", e);
}

// Preload the loader
preloadLoaderImage();

// If the build is not "official", show a warning
// https://github.com/dfinity/internet-identity#build-features
showWarningIfNecessary();

const okOrReason = await checkRequiredFeatures();
if (okOrReason !== true) {
return compatibilityNotice(okOrReason);
}

// Prepare the actor/connection to talk to the canister
const connection = new Connection(readCanisterId());

return app(connection);
};
6 changes: 1 addition & 5 deletions src/frontend/src/utils/featureDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@ import { features } from "$src/features";
import { isNullish } from "@dfinity/utils";
import { wrapError } from "./utils";

export const checkRequiredFeatures = async (
url: URL
): Promise<true | string> => {
export const checkRequiredFeatures = async (): Promise<true | string> => {
if (features.DUMMY_AUTH) {
// do not check for webauthn compatibility if DUMMY_AUTH is enabled
return true;
}
if (isNullish(window.PublicKeyCredential))
return "window.PublicKeyCredential is not defined";
if (url.hash === "#compatibilityNotice")
return "Remove #compatibilityNotice from the URL and try again.";
// For mobile devices we want to make sure we can use platform authenticators
if (!navigator.userAgent.match(/(iPhone|iPod|iPad|Android)/)) return true;
try {
Expand Down

0 comments on commit 1529c7f

Please sign in to comment.