How’d they code the NNS front end to do this?

So, i recently noticed that if you save the NNS front end address to your Home Screen as a bookmark, the app no longer shows the address bar at the bottom of the screen. That’s a cool feature that i feel like gives the NNS the feel of a react-native app.( See the screenshots below for reference.)

My question is, how did they code that feature? I’d like to include it in my app? Is it an extension or is there a JS dependency that i could use to achieve this same effect? If someone could @ someone from the NNS front end team, that’d be great.

Screenshot of view of nns front end from safari browser:

Screenshot of view of NNS front end from bookmark that was added to Home Screen:

Before answering your question, a note about the difference in the screenshots:

  • the first screenshot is the “old” login page in Flutter / Dart
  • the second screenshot is the “new” login page in Svelte

Currently both cohabits until we finish rewriting the frontend in Svelte. The “new” login page is the default and the Flutter app redirect the user to that page. However I also noticed in my browser that time to time it uses the cache and don’t redirect me, that’s why I also land sometimes on the “old” login page. No biggy, both login are working just fine.

Now about your question: when you install a web app to the home screen, you can define a set of options that tell the OS how your app should behave or be presented. This can be done by providing a Web App manifest file. Commonly the file is often called the manifest.json or manifest.webmanifest.

Within this json file you can define such options as the “theme” color but also what is the display behavior. In nns-dapp we use standalone that makes the application look and feel like a standalone application.

A couple of references:

Hope this answer your question. Let me know if you have more questions.

5 Likes

Thank you kindly :pray:t5: :pray:t5:

2 Likes

I think this type of app is called a Progressive Web App (PWA).

yeah. I’m reading about it now. Never knew this stuff existed, but now I gotta incorporate it into my app lol.

The main issue with PWAs is that Apple handicaps it on iOS… still no push notifications and access to other native functionality.

Yeah. Apple really put their “dev suppression” caps on with this one. Anything to funnel devs through the iOS store.

1 Like

Which file did you define the service worker in? and where do you register the service worker? I’m getting this error in the console:

GET http://localhost:3000/manifest.json 404 (Not Found)
Manifest: Line: 1, column: 1, Syntax error.

I wanna look at how you all set up the service worker to see what I’m doing wrong.

We do not register a custom service worker.

Long story short, at the moment if your application is served through the .ic0.app domain you cannot register a custom service worker without a workaround.

The reason behind is the fact that the IC answer to http queries with an html answer that automatically register a service worker which takes care of certifying all resources that will be downloaded by the browser for security reason. In other words: even if you don’t register a service worker there is actually already one that is registered for you.

As a workaround, you can register a custom worker from the raw domain. For example: /_/raw/service-worker.js. However if do so, you might not be able to use all features of a service worker anyway.

There is no such limitation if you serve your app directly through the .raw.ic0.app domain but it’s generally not the best practice as you loose the certification explained above.

Side note: as a web developer I dislike this limitation or issue (depends the point of view). It’s one of my top two features which I bring to the table on a weekly basis. Hopefully it will be solved in the future.

2 Likes

ok. I asked about the service worker because I’m getting this error:

Manifest: Line: 1, column: 1, Syntax error.

and stack exchange led me to service workers. But this error must have a different solution since you too didn’t use a service worker. Would you happen to have an idea as to why this error is occurring? and also, do this mean I wouldn’t be able to do any caching without performing the workaround? or is there some magical engineering behind the scenes takes care of caching for me? I noticed the load times are much better on the PWA version of the nns app. I’m wondering how you all did that too?

heres the code from my manifest file:

{
  "short_name": "DTC",
  "name": "The Digital Time Capsule",
  "icons": [
    {
      "src": "https://cqjyx-qqaaa-aaaap-qaakq-cai.raw.ic0.app/src/dtc_assets/src/assets/dtc-logo-white.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "background_color": "#3367D6",
  "display": "standalone",
  "scope": "/",
  "theme_color": "#3367D6",
  "description": "Document you legacy"
}

and the from my Index.html file:

    <link rel="manifest" href="/manifest.json" crossorigin="anonymous" />

Oh an error linked to the validation of the manifest not a JavaScript error. I had a look at your canister answer and I think the error is due to the fact that the response header provided for your manifest is not the correct mime type. It provides the json file with content-type: text/html but it should be content-type: application/json. (see screenshots)

To be honest with you I am not sure about what’s possible or not with the workaround. I’m sure the workaround is a must if you want to be able to do at least something. I think in Openchat for example they used it to register the web push notifications. I personally was never able to really dig into the subject because in my personal apps I had no luck with workaround.

Being said, about cache, there is the sw cache but there are also other ways to provide longer caching policy. For example by setting cache-control in the http headers.

As far as I am aware of, we do not apply any particular policy to better cache on mobile devices.


Ok. Thank you for investigating :pray:t5:. Would changing the mime type be as simple as assigning the type Attribute of the <link> tag to application/json In the Index.html File?

Well depends what you mean with “simple” but it ain’t something you can set on the frontend side, it’s an information that is set in the headers of the http answer provided by the backend.

Are you using a custom assets canisters or anything particular in your deployment?

I have tested adding a manifest.json in a fresh new project served locally and an app served on mainnet - both using the default assets canister. Both provided the manifest file with the correct mime type.


not to my knowldge. I used this template from @kpeacock 's repo to start my react js project

and of course I’ve done plenty of frontend and backend coding for the project. the backend is written in motoko. one possibility could be that I have the manifest.json file in the wrong directory. I currently have it at the same level as the index.html file where I place the <link> tag. is this the proper place for it?

Kyle, do you know of any reason the request header for the manifest.json file of a project initiated with this template would be returning with a content-type: text/html instead of content-type: application/json?

here are my configurations if that helps at all.

webpack.config.js:

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");

let localCanisters, prodCanisters, canisters;

function initCanisterIds() {
  try {
    localCanisters = require(path.resolve(".dfx", "local", "canister_ids.json"));
  } catch (error) {
    console.log("No local canister_ids.json found. Continuing production");
  }
  try {
    prodCanisters = require(path.resolve("canister_ids.json"));
  } catch (error) {
    console.log("No production canister_ids.json found. Continuing with local");
  }

  const network =
    process.env.DFX_NETWORK ||
    (process.env.NODE_ENV === "production" ? "ic" : "local");

  canisters = network === "local" ? localCanisters : prodCanisters;

  for (const canister in canisters) {
    process.env[canister.toUpperCase() + "_CANISTER_ID"] =
      canisters[canister][network];
  }
}
initCanisterIds();

const isDevelopment = process.env.NODE_ENV !== "production";
const asset_entry = path.join(
  "src",
  "dtc_assets",
  "src",
  "index.html"
);

module.exports = {
  target: "web",
  mode: isDevelopment ? "development" : "production",
  entry: {
    // The frontend.entrypoint points to the HTML file for this build, so we need
    // to replace the extension to `.js`.
    index: path.join(__dirname, asset_entry).replace(/\.html$/, ".jsx"),
  },
  devtool: isDevelopment ? "source-map" : false,
  optimization: {
    minimize: !isDevelopment,
    minimizer: [new TerserPlugin()],
  },
  resolve: {
    extensions: [".js", ".ts", ".jsx", ".tsx"],
    fallback: {
      assert: require.resolve("assert/"),
      buffer: require.resolve("buffer/"),
      events: require.resolve("events/"),
      stream: require.resolve("stream-browserify/"),
      util: require.resolve("util/"),
    },
  },
  output: {
    filename: "index.js",
    path: path.join(__dirname, "dist", "dtc_assets"),
  },

  // Depending in the language or framework you are using for
  // front-end development, add module loaders to the default
  // webpack configuration. For example, if you are using React
  // modules and CSS as described in the "Adding a stylesheet"
  // tutorial, uncomment the following lines:
  module: {
   rules: [
     { test: /\.(ts|tsx|jsx)$/, loader: "ts-loader" },
     {
      test: /\.scss$/,
      use: ['style-loader', 'css-loader', 'sass-loader']
    }
   ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, asset_entry),
      cache: false
    }),
    new CopyPlugin({
      patterns: [
        {
          from: path.join(__dirname, "src", "dtc_assets", "assets"),
          to: path.join(__dirname, "dist", "dtc_assets"),
        },
      ],
    }),
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'development',
      DTC_CANISTER_ID: canisters["dtc"],
      II_URL : isDevelopment ?
      "http://localhost:8000?canisterId=rwlgt-iiaaa-aaaaa-aaaaa-cai#authorize" :
      "https://identity.ic0.app/#authorize",
    }),
    new webpack.ProvidePlugin({
      Buffer: [require.resolve("buffer/"), "Buffer"],
      process: require.resolve("process/browser"),
    }),
  ],
  // proxy /api to port 8000 during development
  devServer: {
    proxy: {
      "/api": {
        target: "http://localhost:8000",
        changeOrigin: true,
        pathRewrite: {
          "^/api": "/api",
        },
      },
    },
    hot: true,
    static: {
      directory: path.join(__dirname, "./src/dtc_assets"),
      watch: true
    },
    port: 3000
  },
};

tsconfig.json:

{
    "compilerOptions": {
      "allowJs": true,
      "esModuleInterop": true,
      "forceConsistentCasingInFileNames": true,
      "jsx": "react",
      "lib": [
        "ES2020",
        "DOM"
      ],
      "module": "commonjs",
      "strict": true,
      "target": "es2020"
    }
  }

package.json:

{
  "name": "hello_assets",
  "version": "0.1.0",
  "description": "Internet Computer starter application",
  "keywords": [
    "Internet Computer",
    "Motoko",
    "JavaScript",
    "Canister"
  ],
  "scripts": {
    "devStart": "nodemon server.jsx",
    "build": "webpack",
    "prebuild": "npm run copy:types",
    "start": "webpack serve --mode development --env development",
    "prestart": "npm run copy:types",
    "copy:types": "rsync -avr .dfx/$(echo ${DFX_NETWORK:-'**'})/canisters/** --exclude='assets/' --exclude='idl/' --exclude='*.wasm' --delete src/declarations"
  },
  "devDependencies": {
    "@dfinity/agent": "^0.10.1",
    "@dfinity/auth-client": "^0.10.1",
    "@dfinity/authentication": "^0.10.1",
    "@dfinity/candid": "^0.10.1",
    "@dfinity/identity": "^0.10.1",
    "@dfinity/principal": "^0.10.1",
    "assert": "2.0.0",
    "buffer": "6.0.3",
    "copy-webpack-plugin": "^9.0.1",
    "css-loader": "^6.4.0",
    "events": "3.3.0",
    "html-webpack-plugin": "5.3.1",
    "mini-css-extract-plugin": "^2.4.3",
    "node-sass": "^7.0.1",
    "nodemon": "^2.0.15",
    "process": "0.11.10",
    "redux-devtools-extension": "^2.13.9",
    "sass": "^1.43.3",
    "sass-loader": "^12.2.0",
    "stream-browserify": "3.0.0",
    "style-loader": "^3.3.1",
    "terser-webpack-plugin": "5.1.1",
    "ts-loader": "^9.2.5",
    "typescript": "^4.3.5",
    "util": "0.12.3",
    "webpack": "^5.24.4",
    "webpack-cli": "4.9.0",
    "webpack-dev-server": "^4.7.2"
  },
  "browserslist": [
    "last 2 chrome version",
    "last 2 firefox version",
    "last 2 safari version",
    "last 2 edge version"
  ],
  "dependencies": {
    "@stripe/react-stripe-js": "^1.7.0",
    "@stripe/stripe-js": "^1.22.0",
    "@types/react": "^17.0.34",
    "@types/react-dom": "^17.0.11",
    "axios": "^0.24.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-es2021": "^1.0.0",
    "dotenv": "^10.0.0",
    "express": "^4.17.1",
    "get-youtube-id": "^1.0.1",
    "jsonwebtoken": "^8.5.1",
    "nvm": "^0.0.4",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-redux": "^7.2.6",
    "react-router-dom": "^6.2.1",
    "react-youtube": "^7.13.1",
    "redux": "^4.1.2",
    "redux-thunk": "^2.4.0"
  }
}

dfx.json:

{
  "canisters": {
    "dtc": {
      "main": "src/dtc/main.mo",
      "type": "motoko"
    },
    
    "dtc_assets": {
      "dependencies": [
        "dtc"
      ],
      "frontend": {
        "entrypoint": "src/dtc_assets/src/index.html"
      },
      "source": [
        "src/dtc_assets/assets",
        "dist/dtc_assets/"
      ],
      "type": "assets"
    }
  },
  "defaults": {
    "build": {
      "args": "",
      "packtool": ""
    }
  },
  "dfx": "0.9.3",
  "networks": {
    "local": {
      "bind": "127.0.0.1:8000",
      "type": "ephemeral"
    }
  },
  "version": 1
}

I think the issue may have something to do with the fact that I’m using a <HashRouter> for navigating between tabs. I’m seeing in stack exchange that this introduced the same issue for other ppl as well.

Indeed it can probably be an issue with your routing or where the file is located - i.e. the url https://cqjyx-qqaaa-aaaap-qaakq-cai.ic0.app/manifest.json leads to no file.

you can compare the expected result with one of my site, for example https://iey7l-kaaaa-aaaah-qadoa-cai.raw.ic0.app/manifest.json

in my previous screenshot the browser display text/html because it does not find the file and redirect to the root index.html that is a text/html, got it

if you fix https://cqjyx-qqaaa-aaaap-qaakq-cai.ic0.app/manifest.json location you should be good

Ok. I’ll keep at it. Would you mind giving some screenshots showing me the file structure you used and showing me how you defined the file paths that you called in the link tag and in the manifest.json file

The thing is that I don’t use React which you seem to use and I also my apps have quite an opinionated files structure therefore not sure it would be that helpful.

Maybe I can share the sample I app I quickly built to test the manifest on local net? Give me five…

There you go :point_right: GitHub - peterpeterparker/manifest: https://forum.dfinity.org/t/how-d-they-code-the-nns-front-end-to-do-this/11967