Skip to content

Latest commit

 

History

History

frontend

Frontend of ICRC1 Payment Client Demo

This is the frontend client / web app of the ICRC1 Payment Client Demo.

It is built with Vite, React, Tailwind, React Router, Formik, eslint and others (Icônes for icons; react-zxing for scanning QR codes).

This frontend web app uses an ES module type web worker to process all communication with the backend canister (note that Firefox does not support ES module type workers yet).

This is done within a context provider CanisterProvider.jsx with its UI state managed by a reducer and is additionally composed with the hooks useInternetIdentity.jsx (which handles II authentication) and useDedicatedWorker.jsx (which handles the/a web worker). The provider's returned value is memoized and importable/obtainable via the useCanister hook anywhere in the rest of the codebase as the CanisterProvider is at the outermost scope, just below the router's provider, which can be inspected in main.jsx. In main.jsx a test is done to determine if the user's browser supports ESM type modules (Firefox for instance still does not); if passing, in App.jsx the routes are laid out to the top-level page components which are listed next.

Pages

  • Landing.jsx - the not-authenticated arguably lackluster landing page.

Screenshot of Landing Page

  • Home.jsx - the authenticated landing page, shows the AccountOverview.jsx and ICRC1CanisterMetadata.jsx components which show the stats associated with the caller's ICRC1 subaccount (currently only one per caller--the funds of which payments debits are withdrawn from and can be credited by depositing into its QR code displayed address) and the associated ICRC1 token canister metadata (including its canister id, and svg encoded logo if available).

Screenshot of Home Page

  • Payments.jsx - the very mobile responsive list of payments featuring the custom built AutoEllipsizingTextSpan.jsx which ellipsizes the address into its leading and trailing five characters or if a subaccount is present also includes the checksum and the the first three characters of the subaccount (unless the subaccount is shorter than its ellipsized form, in which case the entire subaccount is included); although AutoEllipsizingTextSpan.jsx will ellipsize any text to its full width according to the ellipsize function provided, the default value being ellipsizeICRC1Address from ellipsize-ICRC1-characters.js.
  • PaymentDetails.jsx - shows the details of a payment when a user "clicks" on one in the Payments listing, using its id via the page's useParams encoded route which is passed to the getPaymentById from useCanister and shows all the details of that payment including its current status which will include the transaction index and time if successful, or time and transfer err or inter-canister caught err if failed (unless it was to an invalid recipient address, which is handled by this client but that's not the canister's only potential caller); or the payment will be pending. Note the payment can be copied into a new send payment by clicking the copy icon in the nav bar on this page.
  • SendPayment.jsx - contains the form for sending a new payment that uses Formik for managing inputs, errors and validation. It also uses a QrCodeScanner.jsx together with the payment decoding utils of ic-js to set the input fields if a valid payment encoding is scanned.

Screenshot of Home Page

Web Worker

Note that as all the canister communication is handled by the background processing ("non-blocking") of the web worker, when the inputs are valid for a payment to be sent, and it is submitted by the form on that page, the UI immediately creates a pending payment viewmodel, using the clientPaymentId UI created UUID as its key, when the args for the backend's send_payment API method are created from the inputs by prepareSendPaymentArgs. This payment view model is passed to the CanisterProvider's reducer at the same time the args are sent as an event's data to the web worker to make the call to the backend canister's send_payment method.

Usually this happens too fast to notice, but until the canister call completes, as the route is immediately navigated back to the payment listing, the newly sent payment will appear at the top in yellow indicating it's pending. Once the call completes and its response is parsed by the web worker, the web worker calls back to the CanisterProvider's reducer to update the payment listing, the reducer using that clientPaymentId as key to replace the correct payment's view model, and it will either appear as the normal font color (black or white depending on dark mode) if completed successfully (or in red if sending the payment failed).

As the web worker uses a different thread it does have not have the same context as the browser session, however to enable the web worker to use imports the vite config's optimizeDeps esBuild define key polyfills global to globalThis. The was found the best way to handle this, although there are other solutions.

This also requires that the actor instance be created in the web worker's context which is done in the createWorkerActor.js utiltiy file, which creates the authenticated actor instance from the identity that is set by the AuthClient when the user logs into their internet identity (which happens on the UI thread, but as it uses indexdb can be accessed by the web worker).

Finally note that the web worker's data message has a type in the same form as that of a typical useReducer dispatch action: { type, key, payload/args }--payload if incoming (to the UI) and args if outgoing (to the canister). The possible values for type and key are defined by the actionType and stateKey enums found in enums.js.

Utils

Most of any logic that can be encapsulated as its own standalone pure function can be found in the utils.js. This includes the view model/presentation transforms as well as the data model transforms parsing the responses of the canister calls. It was not assumed the ICRC1 token canister metadata values would be available beforehand, so getting their values are the first call that the web worker makes (anonymously) and cached in indexdb.

If your project does know these values beforehand, it would advantageous to define them in the Vite config so they are always and immediately available so they don't have to be managed by the React state components. Alternatively they could also be cached in local storage (once they reach the UI) if their values are to be dynamic.

This project uses bigdecimal to convert base units to normal units (which is the standard in the UI, although both are often presented for sake of clarity).

Additional UI Notes

  • tailwind.config.js - includes Dfinity color branding, extends the typical screens as the typical (cellular) mobile viewport width is almost twice that of the smallest default screen and adds text shadows as a custom, but simple plugin. A fluid type plugin was added but later removed as it was easier to manage UI layout for the narrower screens with a fixed font size.
  • Header.jsx - is the navigation of this app copying the typical 'action bar' behavior although only using icons, and it is absolute positioned. To handle this for snappy scrolling in a content section, the .scrollable css class has a transprent bottom border a little taller than the height of the navbar so the bottom of a content section is always easily visible (in addition to the .pb-safe mobile device bottom offset environmental browser var on body). Setting the document's title is also handled in header (although this could be done in its own hook with an accessible announcer).
  • useTheme.jsx - is a hook wrapper around the Tailwind standard of toggling a class based dark mode.
  • useRoutePathReloader.jsx - was added to handle the side effect of the current route resetting to the base route when the browser triggered a refresh. This may not be needed if an alternate Routes setup is used (see App.jsx), but works fine including resetting route state (such as copying a payment details to send a new payment or while on a specific page's details).

Note on environmental variables

Vite uses import.meta.env to spread environmental variables on for access, unlike process.env that is used by Webpack. This project uses the Vite plugin vite-plugin-environment to automatically expose the vars of the root project's .env (that is generated by dfx when building the canisters) on import.meta.env. This also means the actor's generated declarations must be modified to use import.meta.env instead of process.env which had to be done anyways for the sake of the web worker.

Alternatively, the define key of the a vite config can be used for this purpose, with either import.meta.env or process.env being used. For an example see the testing config.

Follow-up Links