Skip to main content

Update App bridge to Version 4

In this guide, we will talk about how to migrate our app from version 3 to version 4, first see Shopify guide here: Migrate your app to Shopify App Bridge React 4.x.x and App bridge official guide

Step 1: Add the app-bridge.js script tag

In the embed.html and index.html, update the content to:

  <meta charset="UTF-8" />
<meta name="shopify-api-key" content="%VITE_SHOPIFY_API_KEY%" />
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YOUR APP NAME</title>
<link rel="manifest" href="/manifest.json" />

The Shopify API KEY will be inserted via vite, and the CDN link for the app bridge SDK remains the same

Step 2: Update the app bridge React to latest version

Go to the package.json in the assets folder and update the version:


"@shopify/app-bridge": "^3.7.10",
"@shopify/app-bridge-react": "^4.1.5",
"@shopify/app-bridge-utils": "^3.5.1",

Step 3: Remove the Provider setup

Do just as in the Shopify guide, this step is quite simple.

Step 4: Update components

In this steps, beside all the already-in-the-guide stuff, we will only mention some other stuff that are not there.

You cannot show NavMenu in a page with Max Modal

If you show Nav Menu in the Max Modal, you will get an error like: Cannot get onHide of.... For example, hide the NavMenu on page has Max Modal:

  <>
{isNotBlogScreen && (
<NavMenu>
{appMenu()
.filter(menu => !['/feature-request'].includes(menu?.url))
.map(menu => {
return {...menu, destination: getUrl(menu?.url)};
})
.map(menu => (
// eslint-disable-next-line react/jsx-key
<ReactRouterLink url={menu.destination} rel={menu.rel || null}>
{menu.label}
</ReactRouterLink>
))}
</NavMenu>
)}
{children}
</>
);

Using Modal within Max Modal

In a complex editor like SEOon Blog app, we need to have Modal like Media library upload modal inside the Max Modal. This will cause the the same problem as nesting the NavMenu inside Max Modal page. So you need to wrap the AppProvider like [this](https://shopify. dev/docs/api/app-bridge/using-modals-in-your-app#react-components-using-react-portals) to make it work.

No using the Contextual API inside the Max Modal

Use the Modal primary action and secondary instead of the Contextual API. Don't misuse it, or you get this error:

https://go.screenpal.com/watch/cZXq6onVZ7i

Try use src attribute of the modal

In some scenario, if your editor is a complex editor, leveraging third-party API, we need to use src attribute for your page, instead of inline your JSX inside the Max Modal content. See this guide

Just like in the SEOon blog, when we open a blog post, we open a Max Modal, open a new page, this page loads the app via route to the editor blog post. It will take a bit more time to load, but it prevent the problem of having many iframe within your Max Modal. See [this issue for reference](https://github. com/Shopify/shopify-app-bridge/issues/303) in which developers complaining about Max Modal mechanism.

Save bar inside Max Modal

If you apply for BFS with Max Modal and encounter something like this, well, you at the right place.

Correct this in your apps UI. See this screenshot for details. The Contextual Save Bar (CSB) has not been implemented correctly with the Max Modal. The proper way this should be displayed is when the form becomes dirty the Save and Discard button should appear in the TitleBar of Max Modal also removing the close button until a selection is made.

It is not stated in the Shopify App Bridge Documentation, but it can be done. Normally, using the SaveBar component will only appear on the top Shopify search bar. With the SaveBar implemented for the Max Modal, the close button of the modal will be hidden and it trigger leave prompts if you try to leave the page. All you need to do is to place the SaveBar component inside the Modal content.

import {Modal, TitleBar, useAppBridge} from '@shopify/app-bridge-react';

export function MyModal() {
const shopify = useAppBridge();

return (
<>
<button onClick={() => shopify.modal.show('my-modal')}>Open Modal</button>
<Modal id="my-modal" variant="max">
<div></div>
<TitleBar>
<button variant="primary">Primary action</button>
<button>Secondary action</button>
</TitleBar>
<SaveBar id="my-save-bar">
<button variant="primary" onClick={handleSave}></button>
<button onClick={handleDiscard}></button>
</SaveBar>
</Modal>
</>
);
}

Use direct fetch API

Take advantage of the new Direct API access feature. You can make requests to the Admin API directly from your app using the standard web fetch API. For more information about Direct API access, refer to the documentation.

So in our helpers.js file, we need to change to use direct of fetch API, instead of the authenticatedFetch from version 3. Remember that you can call direct API call since App Bridge Version 4, see [this documentation] (https://shopify.dev/docs/api/app-bridge-library#direct-api-access)

import axios from 'axios';
import createApp from '@shopify/app-bridge';
import {Redirect} from '@shopify/app-bridge/actions';
import {initializeApp} from 'firebase/app';
import {getAuth} from 'firebase/auth';
import {getStorage} from 'firebase/storage';
import {getApiPrefix} from '@functions/const/app';
import {isEmbeddedApp} from '@assets/config/app';
import {getAnalytics, initializeAnalytics, logEvent as logFbEvent} from 'firebase/analytics';
import {getFirestore} from 'firebase/firestore';

const app = initializeApp({
appId: import.meta.env.VITE_FIREBASE_APP_ID,
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET
});
export const analytics = (() => {
try {
return initializeAnalytics(app, {
config: {cookie_flags: 'max-age=7200;secure;samesite=none'}
});
} catch (e) {
// wrap in try-catch to fix when this file is reloaded by React hot reload https://i.imgur.com/tdiTZnz.png
if (e.code === 'analytics/already-initialized') {
return getAnalytics(app);
}
console.log(e);
throw new Error(e);
}
})();
export const db = getFirestore(app);
export const auth = getAuth(app);
export const storage = getStorage(app);
export const embedApp = createEmbedApp();
export const client = axios.create({timeout: 60000});
export const api = createApi();

export function getHost() {
const isProduction = import.meta.env.VITE_NODE_ENV === 'production';
if (isProduction) {
return new URLSearchParams(location.search).get('host');
}

const localStorageHost = localStorage.getItem('avada-dev-host');
const host = new URLSearchParams(location.search).get('host') || localStorageHost;
localStorage.setItem('avada-dev-host', host);

return host;
}

export function createEmbedApp() {
const host = getHost();
if (!host || !isEmbeddedApp) return;

return createApp({
apiKey: import.meta.env.VITE_SHOPIFY_API_KEY,
host,
forceRedirect: true
});
}

/**
* @return {(uri: string, options?: {headers?, body?, method?: 'GET' | 'POST' | 'PUT' | 'DELETE'}) => Promise<any>}
*/
function createApi() {
const prefix = getApiPrefix(isEmbeddedApp);

if (isEmbeddedApp) {
const fetchFunction = fetch;
return async (uri, options = {}) => {
if (options.body) {
options.body = isFormData(options.body) ? options.body : JSON.stringify(options.body);
if (!isFormData(options.body)) {
options.headers = options.headers || {};
options.headers['Content-Type'] = 'application/json';
}
}
const response = await fetchFunction(prefix + uri, options);
checkHeadersForReauthorization(response.headers, embedApp);
return await response.json();
};
}

const sendRequest = async (uri, options) => {
const idToken = await auth.currentUser.getIdToken(false);
return client
.request({
...options,
headers: {
accept: 'application/json',
...(options.headers || {}),
'x-auth-token': idToken
},
url: prefix + uri,
method: options.method,
data: options.body
})
.then(res => res.data);
};

return async (uri, options = {}) => sendRequest(uri, options);
}

/**
*
* @param headers
* @param app
*/
function checkHeadersForReauthorization(headers, app) {
if (headers.get('X-Shopify-API-Request-Failure-Reauthorize') !== '1') {
return;
}
const authUrlHeader = headers.get('X-Shopify-API-Request-Failure-Reauthorize-Url') || `/api/auth`;
const redirect = Redirect.create(app);
redirect.dispatch(
Redirect.Action.REMOTE,
authUrlHeader.startsWith('/')
? `https://${window.location.host}${authUrlHeader}`
: authUrlHeader
);
}

/**
*
* @param data
* @returns {boolean}
*/
function isFormData(data) {
return data instanceof FormData;
}

End

That is it. If you encounter any problems, contact your leader for help.