Skip to content
Last updated

Reference Web Application

EGYM provides a sample web application that might be used as a reference: https://github.com/egym/mwa-reference

The purpose of the app is to give an example of how a micro web application can be structured and what mechanics are used to interact with native mobile features.

EGYM Recommends creating your own web app from scratch but fetching some ideas from the mwa-reference app.

Basic MWA configuration

First of all, go through the Ionic documenation and get familiar with basic concepts.

Further down we describe the creation process of an MWA from the EGYM point of view and in relation to the EGYM BMA app perspective.

Ionic CLI

Ionic CLI has a set of useful tools for developing Ionic apps.

Install it with the following command:

npm install -g @ionic/cli

@capacitor-community/http

In the mwa-reference app there is a @capacitor-community/http package. It is added there only for checking the backward compatibility when updating to @capacitor/core@^4.6. In your project you should remove this http plugin so it will not pollute your console with extra warnings.

Create MWA skeleton

Once Ionic CLI is installed, it is possible now to bootstrap a new Ionic MWA from scratch. Run:

ionic start

Follow the command line prompts to finish the installation:

  • Select "React" framework
  • Type the name of the app
  • Select a starter template. We recommend choosing a blank app so there are no redundant files that are usually forgotten to remove from the project.

Linters

We highly recommend to setup eslint and prettier for every web project. It helps maintain consistency in the code style and avoid general errors and therefore eliminate bugs.

Here is how the eslint & prettier configuration looks like in EGYM for MWA:

  • Install the following packages:
  npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb eslint-config-airbnb-typescript eslint-config-prettier eslint-plugin-import eslint-plugin-jest eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks husky prettier pretty-quick
"lint": "eslint --ext js,jsx,ts,tsx src",
"lint-fix": "eslint --fix --ext js,jsx,ts,tsx src",
  • We also recommend to add the "clear npm cache" npm script, which sometimes can help with clearing eslint errors which are already resolved but still shown as errors:
"nuke:cache": "rm -rf ./node_modules/.cache"
  • Add prettier files. .prettierrc.json and .prettierignore
  • Install pre-commit hooks. Add the .husky folder with pre-commit scripts & run husky install. Pre-commit hook will run prettier checks on staged files.

Communication between mobile app and MWA

HTTP

In the MWA it is possible to make HTTP calls with the browser fetch API and it will work well until the backend restricts access to its resources by CORS policies.

By default the MWA runs on http://localhost (Android) or capacitor://localhost (iOS) and usually backends don't allow to make http calls from any origins if it's not specifically configured.

Capacitor allows to change the hostnames with the following capacitor.config.ts config:

"server": {
  "hostname": "my.testbackend.com",
  "androidScheme": "https",
  "iosScheme": "https",
},

This way the MWA can run on different origins. The backend has to also update its CORS config with the corresponding hostname (in this case https://my.testbackend.com). A downside of this approach is that you can't change the CORS configuration of the backend that you can't control.

The most convenient way to bypass CORS is to enable the Capacitor HTTP and Cookies plugins which will allow you to make calls right from the device (not from a browser).

See capacitor.config.ts:

plugins: {
  CapacitorHttp: {
    enabled: true,
  },
  CapacitorCookies: {
    enabled: true,
  },
},

When this is enabled it is possible to use CapacitorHttp and CapacitorCookies from the @capacitor/core package. There you find the unified helper method for making http calls demonstrated in an example of the mentioned plugins usage: createApiRequest.ts (Look for CapacitorHttp.request(...) and CapacitorCookies.getCookies(...))

See the mwa-reference example of how to bypass CORS restrictions and also an example of how to make http requests using CapacitorHttp, react-query and react hooks.

image.png

More reading:

  • https://ionicframework.com/docs/v5/troubleshooting/cors
  • https://capacitorjs.com/docs/apis/http
  • https://capacitorjs.com/docs/apis/cookies

Initial Context

In the mwa-reference you find an example of how to receive and store initial context passed from the native app.

See src/index.ts:

const initialContext =
  getInitialContext<PortalsContext>()?.value ||
  ({
    startingRoute: '/home',
    authToken: '',
    language: 'de_DE',
    lightPrimaryColor: '#ebfafc',
    primaryColor: '#00c4dc',
    primaryTextColor: '#ffffff',
    url: 'https://mwa-test-be.herokuapp.com',
  } as PortalsContext);

getInitialContext is the function from @ionic/portals package. When getInitialContext is invoked in the capacitor environment, it returns the initial context that the mobile app provides the web app on launch. When the MWA runs in the browser (e.g. during development), getInitialContext function will not return anything because there is no mobile app that passes initial context into MWA. That's why we have to fallback to the default initial context object.

The initialContext then is passed into <StoreProvider /> which makes the data available globally inside the app.

<StoreProvider
  initialState={{
    portalsContext: initialContext,
  }}
>
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
</StoreProvider>

In the following you can see how to get portalsContext data in the app inside any component or hook:

const [startingRoute] = useStore(getStartingRouteSelector);

See how store and selectors are implemented here - https://github.com/egym/mwa-reference/tree/main/src/store

See an example of how initial context data can be used - useTestAuth.ts

Or see an example of initialContext's authToken usage inside real production MWA application:

const useAuth = () => {
  const [authToken, set] = useStore(getAuthTokenSelector);
  const [backendUrl] = useStore(getUrlSelector);
  const [selectedLocationsIds] = useStore(getSelectedLocationsIds);

  const authQuery = useQuery(
    queryKeys.auth.session(String(authToken)),
    async () => {
      console.log('[WEB] - useAuth - cleanAllCookies');
      await CapacitorCookies.clearAllCookies();
      return authorizeUser({ payload: { egym: { token: authToken } } });
    },
    {
      onSuccess: (result) => {
        const token = result.data.identity?.token;
        const club = result.data.club;
        const user = result.data.user;

        if (token) {
          CapacitorCookies.setCookie({
            key: 'token',
            value: token,
            url: backendUrl,
          });
        }

        if (club) {
          Settings.defaultZone = club.time_zone;
        }

        if (user) {
          // set location ids only during first time fetch, ignore on refresh token
          if (!selectedLocationsIds?.length || !selectedLocationsIds.includes(user.home_location_id)) {
            console.log(
              `[WEB] - set user.home_location_id - ${user.home_location_id}, selectedDate - ${DateTime.now().toISO()}`
            );
            set({ location_id: [user.home_location_id], selectedDate: DateTime.now() });
          }
        }
      },
      enabled: Boolean(authToken),
      retryOnMount: false,
      keepPreviousData: false,
      staleTime: 0,
    }
  );

  return {
    authQuery,
  };
};

More reading:

Publish/subscribe

Thanks to Ionic Portals and Capacitor under the hood we are able to exchange messages with the mobile app.

These are the commands that EGYM Android and iOS team implemented specially for MWA communication. Let's see how to use them:

  • dismiss - Request the native app to close the MWA screen. In the MWA example it is used in the very first root page where MWA opened inside the mobile app. See &lt;CommonPageHeader /&gt; component, look for Exit MWA button and see publishDismiss click handler which publishes subscription topic with dismiss type. When the mobile app receives this message it closes the current MWA and returns to the page where it was requested to open.
  • authToken - Request the authentication token for the app user. Initially the authToken provided through the initial context on the first app launch. But this token has a ttl and when it expires it is no longer possible to use it, that is why we need a mechanism to get refreshed a token. See queryClient default options. Once BE api returns 401 error, retry function calls the native app with requestAuthToken() to refetch a new token. When the mobile app receives an authToken topic message, it fetches the new token from the EGYM backend and makes an authToken publish request to the MWA with the new token provided. To receive a new message with a new token the MWA subscribes to the authToken topic and listens to all messages with that topic from the app. Once it receives a new message it sets a new token to the store, so new data is available globally in the app. See how the pub-sub mechanism is implemented here and here.
  • exerciserInfo - Request exerciser profile data. In order to receive currently logged in user details there is mechanism same as with the authToken - Request message with exerciserInfo topic. The mobile app then returns the fetched exerciser info back into the MWA, then the MWA should catch it in the subscription. See how the pub-sub mechanism is implemented here and here.
  • openFeature - Navigate to the web app feature by tapping on a widget. See the example of this functionality inside the ClassesWidget. It is needed to jump from one MWA widget that occupies a small part of the UI inside the mobile app into another full page MWA. To support this behavior the MWA should implement startingRoute property handling from initialContext. See the src/Layout.tsx component and how startingRoute prop is used to route redirect:
<Route path="/" render={() => <Redirect to={startingRoute || '/home'} />} exact={true} />

The image below demonstrates the openFeature use case. On the image at the left there is a classe widget rendered as MWA, a click on a particular class item navigates to full page MWA. Using regular react router navigation will not work here, because of the MWA only a small peace of UI is allocated on the native view and opening the page that is intended to be used as a full screen page will be rendered in the same limited view, that's why the openFeature command is needed.

Screenshot 2022-02-28 at 12.28.43.png

Error handling

Mobile team has implemented the read-only error topic, meaning MWA can't push messages to this topic but only subsribe to it and receive possible errors from native app. The error can occur for example when the MWA sends a new authToken command, then the native app receives it but when fetching new token from the backend it receives 500 or any other error - in this case instead of sending error back to the MWA in the authToken topic, the native app uses error topic and sends error in a generic format as described here - type attribute will identify the failed command, optional code, url and message fields will be populated depending on a command and provide details of failed request.

See an example of how error subcription implemented in the reference MWA in the usePortalsSubscriptions hook:

await Portals.subscribe<string>({ topic: 'error' }, ({ topic, data }) => {
  console.log('[WEB] - subscription - error', topic, data);

  const error = parseJson(data) as PortalsError;

  presentAlert({
    header: t('errors.oops'),
    subHeader: error.type,
    message: error.code || error.message || error.url ? `${error.code} ${error.message} ${error.url}` : undefined,
    buttons: [{ text: 'Refresh', handler: () => window.location.reload() }],
    backdropDismiss: false,
    translucent: true,
    animated: true,
  });
});

Here it only shows an error alert with Refresh app button. But handling of the error can vary in different MWAs and in different cases depending on the error type, for example, you can redirect back to another page, or exit the MWA, or present the user some fancy error page, etc.

Application Structure

Notice all pages in the mwa-reference app have the same structure:

<IonPage>
  <IonHeader> // optional
    // Page header title, back btn or smth. See <CommonPageHeader /> component
  </IonHeader>
  <IonContent> // always required
    // Your content of the page/widget. May consist of ionic's framework components, or your own.
  </IonContent>
  <IonFooter> // optional
    // Content that should always be stick to the bottom, e.g. "Book class" button on the ClassDetails.tsx page
  </IonFooter>
</IonPage>

Following this structure ensures that the MWA app will display all elements inside the mobile app correctly. Using IonHeader and IonFooter with the IonToolbar applies a proper safe area indent on the top and at the bottom of the mobile app screen (if MWA is opened in full screen mode).

More reading:

  • https://ionicframework.com/docs/layout/structure

Safe area

Capacitor provides the web app with additional css variables :

--ion-safe-area-top
--ion-safe-area-right
--ion-safe-area-bottom
--ion-safe-area-left

Using these variables proper indents are set out of the box. They are also available in the custom SCSS code for the cases when, for example, you don't have an IonFooter on the page, but still want to set a proper indent to the bottom of the page, so the content is always properly visible and e.g. the iOS home button does not overlap.

See the example below: The green area on the top is a padding-top set on the IonToolbar component:

padding-top: var(--ion-safe-area-top, 0)

The padding is applied automatically, because the app is running inside the Capacitor environment:

image.png

If we disable this padding-top property for demonstration then the Exit MWA button will be shifted up and will stay under the iOS system time display:

image.png

More reading:

  • https://ionicframework.com/docs/theming/advanced#application-variables

Localisation

The native app passes a language property to the MWA on app launch. The MWA has to use it to fetch the fitting locales files and apply correct date formatting.

See src/i18n.ts implementation of the i18next for strings translations. Attention there is a custom language detector:

languageDetector.addDetector({
  name: 'portalsInitialContext',

  lookup() {
    return window.portalsContext?.language || 'en-US';
  },
});

We recommend using the Luxon library for working with dates and times in JS. It provides an easy way for date manipulation and formatting for different locales. It is also important to handle the difference in date & time formatting for different countries, e.g. 5/11/09 (May 11, 2009) (American) vs 11/5/09 (11 May 2009) (British). Luxon takes care of this and makes formatting very easy.

See how the Initial Context's language property is used to set which language to use on formatting:

src/index.ts:

initialContext.language = initialContext.language.replace('_', '-');
Settings.defaultLocale = initialContext.language;

Theming

To make the MWA reuse the same color scheme and look as similar as possible to the native, the mobile app passes the following color props inside initial context. Each brand has its own color values.

lightPrimaryColor: '#ebfafc',
primaryColor: '#00c4dc',
primaryTextColor: '#ffffff',

In order to apply it, the MWA should set the received colors props to the predefined ionic's css variables - src/index.ts (setThemeColors(window.portalsContext))

export const setThemeColors = (context: PortalsContext) => {
  const primaryColor = new Color(context.primaryColor);
  const textColor = new Color(context.primaryTextColor);

  document.body.style.setProperty('--ion-color-primary', primaryColor.hex);
  document.body.style.setProperty('--ion-color-primary-rgb', primaryColor.rgbString);
  document.body.style.setProperty('--ion-color-primary-contrast', textColor.hex);
  document.body.style.setProperty('--ion-color-primary-contrast-rgb', textColor.rgbString);
  document.body.style.setProperty('--ion-color-primary-shade', primaryColor.shade(0.12).hex);
  document.body.style.setProperty('--ion-color-primary-tint', context.lightPrimaryColor);
};

Or you can set it to any other custom css variable and reuse it when applying styles to custom components and overriding ionic's components props.

The EGYM BMA app currently supports only light mode so you don't need to care about adopting colors for dark mode.

Ionic provides variables that exist at the component level, such as --background and --color. For a list of the custom properties that a component accepts, view the CSS Custom Properties section of its API reference. For example, see the Button CSS Custom Properties.

More reading:

  • https://ionicframework.com/docs/theming/basics
  • https://ionicframework.com/docs/theming/themes
  • https://ionicframework.com/docs/theming/css-variables#ionic-variables

Cache & minimize number of loading screens

We should ensure that the MWA app behavior is as snappy as possible by minimizing the number of loading screens. Every http response should be cached so when an end user enters the page they have already visited, for the second time they will not see the loading screen again.

EGYM recommends using the react-query library which makes it easy to cache and invalidate or replace cache data. See examples of how it could be used:

To easily manage query cache keys, there is the queryKeys factory. You can reuse it for the cases when you need to revalidate or remove stored cache by a specific key.

There are also some alternatives to react-query, such as swr or rtk-query. You can see a comparison here https://react-query-v3.tanstack.com/comparison.

Building & Running & Debugging

This is how we recommend to approach the MWA development

Locally in the computer browser

As long as you don't need to perform any mobile app related checks you can run your app simply with npm start, it will be opened in a browser. Then you have to toggle the device toolbar and select the needed device in the Dimensions dropdown. This is the easiest way on how to develop an MWA without native specifications.

image.png

Capacitor app with Ionic CLI

If you need to check any native app stuff, like safe-area or HTTP CORS or Cookies handling, etc, without the EGYM BMA app context (excluding commands), you can run iOS or Android simulator locally with help of Ionic CLI.

Run the following:

ionic cap add ios

You will see the new ios folder added in the root of the project.
If you are running Mac with M1 Chip you may get ruby error which relates to Cocoapods. Try Removing Cocoapods and then installing it from Homebrew.

Once ionic cap add ios invoked successfully, run next:

npm run build && ionic cap sync
ionic cap run ios -l --external

Wait till capacitor will create and run xcode build and open a simulator:

image.png

You can do the same with android, just replace ios with android in commands above.

See the guide on how to debug and see logs from the webview opened inside simulator see these sections:

More reading:

  • https://ionicframework.com/docs/troubleshooting/debugging

Full integration in Android or iOS build

Otherwise, if you need to check the full integration with the EGYM BMA app including commands and receiving a real initial context, then you need to ask the EGYM team for creating you configured iOS or Android build. Then you can run it either on your computer in a simulator or install it on your phone.

Native builds will be connected to the specified MWA from the Appflow dashboard, so you can autonomously make changes in the MWA, commit & push changes, make web builds and deploy them to the specified channel. Afterwards Ionic Portals Live update will push the new build right onto the device and display the changes.

Run and debug Android build provided by the EGYM team on local machine

  1. Download Android studio, add virtual device and run a simulator.

  2. Get Android build (.apk file) from the EGYM team and install it on the simulator.

  3. Follow Debug MWA inside Android build section to debug MWA inside provided build.

More reading:

  • https://ionicframework.com/docs/troubleshooting/debugging

Run and debug iOS build provided by the EGYM team on local machine

Debugging iOS builds has some limitations:

  • Debugging iOS webviews requires Safari, so your dev computer must be running macOS.
  • You can only debug webviews in applications loaded onto your device through Xcode. You can't debug webviews in apps installed through the App Store or Apple Configurator.

So the only way to debug iOS builds provided by the EGYM team is to install it to any Apple device (either MacOS or iOS) and debug changes by pushing new commits and making new builds in Appflow dashboard.

You can use browser alert functions to log needed info. Logs will be rendered right on the screen. Since alert only accepts strings, to display object information you can use the following:

alert(JSON.stringify(someData, null, 2))

Debug MWA inside Android build

Once you have Android app with MWA opened inside simulator up and running on your computer, you can easily debug your MWA with chrome devtools. To do so open Google Chrome browser and enter chrome://inspect url. You will see the list of webviews opened inside the Android app simulator. Click "inspect" under the one that is not "hidden at" and use opened Chrome Dev Tools window to inspect the logs and see native Portals and Capacitor requests.

12
Screenshot 2023-01-18 at 19.28.29.pngScreenshot 2023-01-18 at 21.29.23.png

Debug MWA inside iOS build

If you have running iOS build that is generated from Xcode on your computer (Capacitor app with Ionic CLI case) you can use Safari Web Inspector to see all logs and http requests.

  1. Enable developer tools in Safari on your dev computer. Launch Safari on your dev machine and navigate to Safari > Settings in the menu bar. In the preferences pane that appears, click on the Advanced tab and then enable the Show Develop menu in menu bar option at the bottom.

  2. Open Xcode build

  3. In Safari on your dev computer, click on Develop in the menu bar and hover over the dropdown option that is your iOS device's name to show a list of MWA instances running on your Xcode build.

  4. Click the dropdown option for the webview that you wish to debug. This will open a new Safari Web Inspector window.

image.png