@shopify/app-bridge-host
TypeScript icon, indicating that this package has built-in type declarations

9.0.0 • Public • Published

@shopify/app-bridge-host

App Bridge Host contains components and middleware to be consumed by the app's host, as well as the host itself. The middleware and Frame component are responsible for facilitating communication between the client and host, and used to act on actions sent from the App Bridge client. This package is used by Shopify's web admin.

License: MIT

App Bridge Host architecture

The App Bridge host uses a cross-platform, modular architecture. The host has several responsibilities. First, the host brokers communication between contexts (ie between the app and Shopify Admin). Second, the host maintains a central store representing the current state of all App Bridge features, which is exposed to the client app. Third, the host provides functionality to the client app.

Functionality is exposed to the client app via App Bridge actions. When an action is dispatched using App Bridge, the host evaluates the action against the relevant reducers, which make changes to the central store of app state. The state is then passed to UI components, which render functionality based on the state.

Features and UI components are treated separately in App Bridge. A feature consists of an action set and reducers, and the associated UI component consumes the resulting state. Most UI components have an associated feature, but this is not required.

The <HostProvider> is not responsible for rendering the client app, by Iframe or other means.

Building an App Bridge host

You can create your own App Bridge host using the <HostProvider> component.

<HostProvider> requires three types of data: app configuration, functionality to provide to the client app, and an initial state for the store.

App configuration

The <HostProvider> requires configuration information about the client app to be loaded:

const config = {
  apiKey: 'API key from Shopify Partner Dashboard',
  appId: 'app id from GraphQL',
  handle: 'my-app-handle',
  shopId: 'shop id from GraphQL',
  url: 'app url from Shopify Partner Dashboard',
  name: 'app name',
};

Note that we'll be referring to this sample config throughout the examples below.

Providing functionality

The <HostProvider> does not load any components by default. In order to provide a feature to an app, you must load the necessary component(s).

You can find pre-defined host UI components inside the @shopify/app-bridge-host/components directory. You can also write your own components.

Initial state

In App Bridge, features are gated using a permission model, which lives in the store. All feature permissions default to false. To provide a feature, you must also set the relevant permissions. If you don’t, the client app will not be permitted to use the feature, even if the component is available. Most components are associated with a single feature, but this is not a requirement.

The <HostProvider> accepts an initial state for the store. This allows a host to pre-populate the store with information the app can immediately access, such as feature permissions.

The setFeaturesAvailable utility can be used to build the initialState.features object. The following example shows a host with several components, and the corresponding feature availability set in initialState:

import {HostProvider} from '@shopify-internal/app-bridge-host';
import Loading from '@shopify/app-bridge-host/components/Loading';
import Modal from '@shopify/app-bridge-host/components/Modal';
import Navigation from '@shopify/app-bridge-host/components/Navigation';

import {Group} from '@shopify/app-bridge/actions';
import {setFeaturesAvailable} from '@shopify/app-bridge-host/store';

const initialState = {
  features: setFeaturesAvailable(Group.Loading, Group.Modal, Group.Navigation),
};

function Host() {
  return (
    <HostProvider
      config={config}
      components={[Loading, Modal, Navigation]}
      initialState={initialState}
    />
  );
}

Custom components

HostProvider can render any type of React component; it’s not limited to the components in this package. You can use either the withFeature decorator or the useFeature hook to connect a custom component to an App Bridge feature.

Your custom components can also be functional components that don't render UI, you just need to ensure it returns null:

function nonUI() {
  // do something non UI
  return null;
}

withFeature

To connect a component to the App Bridge host, wrap it using the withFeature decorator. This decorator provides the component with access to the store and actions for a specified App Bridge feature (remember to set the corresponding feature permissions in initialState).

This decorator only renders your custom component when the store for the specified feature is not undefined. This means you do not have to do an undefined check before accessing the store.

Here is an example of creating a custom component that utilizes the App Bridge Toast feature, rendering the Toast component from Polaris.

import {HostProvider, ComponentProps, withFeature} from '@shopify-internal/app-bridge-host';
import {
  feature as toastFeature,
  WithFeature,
} from '@shopify/app-bridge-host/store/reducers/embeddedApp/toast';
import {Toast} from '@shopify/polaris-internal';
import compose from '@shopify-internal/react-compose';

function CustomToastComponent(props: WithFeature) {
  const {
    actions,
    store: {content},
  } = props;

  if (!content) {
    return null;
  }

  const {duration, error, id, message, action} = content;
  return (
    <Toast
      error={error}
      duration={duration}
      onDismiss={() => actions.clear({id: id})}
      content={message}
      action={action}
    />
  );
}

const Toast = compose<ComponentProps>(withFeature(toastFeature))(CustomToastComponent);

const initialState = {
  features: setFeaturesAvailable(Group.Toast),
};

function Host() {
  return <HostProvider config={config} initialState={initialState} components={[Toast]} />;
}

useFeature

Use the useFeature hook to connect your component to the App Bridge host. This hook returns an array with the store and actions for a specified App Bridge feature (remember to set the corresponding feature permissions in initialState).

The hook is useful when you want to use one component to handle multiple related features. For example, a single component can be used to render the Menu and Title Bar features.

One thing to note is that you need to do an undefined check before accessing the store to prevent errors.

Here is an example of creating a custom component that utilizes the App Bridge TitleBar and Menu feature and uses the useRouterContext hook to access the router:

import {HostProvider, useRouterContext, useFeature} from '@shopify-internal/app-bridge-host';
import {feature as titleBarFeature} from '@shopify/app-bridge-host/store/reducers/embeddedApp/titleBar';
import {feature as menuFeature} from '@shopify/app-bridge-host/store/reducers/embeddedApp/menu';
import {Toast} from '@shopify/polaris-internal';

function TitleBarWithMenu() {
  const {appRoot} = useRouterContext();
  const [titleBarStore, titleBarActions] = useFeature(titleBarFeature);
  const [menuStore, menuActions] = useFeature(menuFeature);

  const items = menuStore?.navigationMenu?.items || [];

  return (
    <div>
      <div>{titleBarStore?.title}</div>
      <ul>
        {items.map(({label, destination: {path}}) => {
          const appUrl = `/admin/apps/${appRoot}`;
          const href = `${appUrl}/${path}`;
          return (
            <li>
              <a href={href}>{label}</a>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

const initialState = {
  features: setFeaturesAvailable(Group.TitleBar, Group.Menu),
};

function Host() {
  return (
    <HostProvider config={config} initialState={initialState} components={[TitleBarWithMenu]} />
  );
}

useFeature

Asynchronous components

You can load host components asynchronously, ie using @shopify-internal/react-async. The <HostProvider> handles adding the feature's reducer to the Redux store. Actions that are dispatched by the app before the feature is available is automatically queued and resolved once the feature's component is loaded.

import {createAsyncComponent, DeferTiming} from '@shopify-internal/react-async';
import {HostProvider} from '@shopify-internal/app-bridge-host';

const Loading = createAsyncComponent<ComponentProps>({
  load: () =>
    import(
      /* webpackChunkName: 'AppBridgeLoading' */ '@shopify/app-bridge-host/components/Loading'
    ),
  defer: DeferTiming.Idle,
  displayName: 'AppBridgeLoading',
});

function Host() {
  return <HostProvider config={config} components={[Loading]} router={router} />;
}

Rendering the client app, with navigation

Since <HostProvider> is not responsible for rendering the client app, one of the components must handle this task. The Apps section in Shopify Admin uses the MainFrame component, which additionally requires a router context to provide navigation to the client app.

If you want to provide navigation capabilities to your app, you will need to include the Navigation component and provide a router to the <HostProvider>. The router should keep the app’s current location in sync with the host page’s current location, and manage updating the location when the route changes.

The following example shows a simple router being passed into <HostProvider>, along with the MainFrame and Navigation components:

import {HostProvider} from '@shopify-internal/app-bridge-host';
import MainFrame from '@shopify/app-bridge-host/components/MainFrame';
import Navigation from '@shopify/app-bridge-host/components/Navigation';

const router = {
  location: {
    pathname: window.location.pathname,
    search: window.location.search,
  },
  history: {
    push(path: string) {
      window.history.pushState('', null, path);
    },
    replace(path: string) {
      window.history.replaceState('', null, path);
    },
  },
};

const initialState = {
  features: setFeaturesAvailable(Group.Navigation),
};

function Host() {
  return (
    <HostProvider
      config={config}
      components={[MainFrame, Navigation]}
      initialState={initialState}
      router={router}
    />
  );
}

Note that since MainFrame only renders the app itself and does not provide features to the app, there is no related initialState. Navigation, however, provides a feature to the app. To allow the app to use that feature, it is made made available in initialState.

Communicating with the loaded app

Certain App Bridge feature requires subscribing to actions dispatched by the app. For example, the Auth Code or Session Token features both respond to a request action from the app.

You can communicate with the client app by using the useHostContext hook.

The following example shows a Session Token component communicating with the client app:

import {HostProvider, useHostContext, useFeature} from '@shopify-internal/app-bridge-host';
import {SessionToken} from '@shopify/app-bridge/actions';
import {feature} from '@shopify/app-bridge-host/features/sessionToken';

function SessionTokenComponent() {
  const {app} = useHostContext();
  const [store, actions] = useFeature(feature);

  useEffect(() => {
    return app.subscribe(SessionToken.Action.REQUEST, () =>
      actions.respond({sessionToken: 'TEST-SESSION-TOKEN'}),
    );
  }, [actions, hostContext]);

  return null;
}

const initialState = {
  features: setFeaturesAvailable(Group.SessionToken),
};

function Host() {
  return (
    <HostProvider
      config={config}
      components={[SessionTokenComponent]}
      initialState={initialState}
    />
  );
}

Migrating from 2.x.x to 3.0.0

There is one breaking change in version 3.0.0.

  • React moved to peerDependencies

React moved to peerDependencies

In version 2.x.x, react was installed as a direct dependency of this package. This can result in duplicate versions of the package being installed in consuming applications.

In version 3.0.0, react has been moved to being a peer dependency. This will prevent multiple versions of the package being installed in consuming applications. If a consuming application doesn't currently have react installed as a dependency, a version compliant with the peer dependency range will need to be added (i.e. ^17.0.2).

Readme

Keywords

none

Package Sidebar

Install

npm i @shopify/app-bridge-host

Weekly Downloads

48,914

Version

9.0.0

License

MIT

Unpacked Size

532 kB

Total Files

419

Last publish

Collaborators

  • jaykay101
  • mishsmelle
  • shopify-dep
  • jaimie.rockburn
  • shopify-admin
  • maryharte
  • pmoloney89
  • netlohan