Test app does not show installation prompt, causing host must be provided error

Hi,

I had created an application using shopify CLI and I am trying to test an application I have been working on a development store. The application worked fine before and I was able to install it on development stores and test it. I have been working on the application for two months now and it is almost at the end of finishing. However, I decided to go ahead and test the application’s functionality after uninstalling, and reinstalling it. This is where problem began. Once i uninstalled the application and went ahead to reinstall it, I am never shown the app installation prompt and i get redirected to the application’s UI which then gives me

Unhandled Runtime Error

AppBridgeError: APP::ERROR::INVALID_CONFIG: host must be provided

I believe the error happens because of the installation prompt not appearing. The host value is undefined from MyApp.getInitialProps() function inside _app.js file.

My server.js code is below.

import '@babel/polyfill';
import dotenv from 'dotenv';
import 'isomorphic-fetch';
import createShopifyAuth, { verifyRequest } from '@shopify/koa-shopify-auth';
import Shopify, { ApiVersion } from '@shopify/shopify-api';
import Koa from 'koa';
import mongo from 'koa-mongo';
import cors from '@koa/cors';
import next from 'next';
import Router from 'koa-router';
import serve from 'koa-static';
import conditional from 'koa-conditional-get';
import etag from 'koa-etag';
import cacheControl from 'koa-cache-control';
import staticCache from 'koa-static-cache';
import MongoClientConnection from './mongoClientConnection';
import { v4 as uuidv4 } from 'uuid';
import RedisStore from './redisStore';
import AmazonProductRouter from './routes/products';
import { dev, port } from './config';
import { getUser, insertUser, updateUser } from './queries';
import { CronJob } from './cron';

dotenv.config();
const app = next({ dev });
console.log(dev, port, 'dev and port');
const handle = app.getRequestHandler();

const sessionStorage = new RedisStore();
const CustomSessionStorage = new Shopify.Session.CustomSessionStorage(
  sessionStorage.storeCallback,
  sessionStorage.loadCallback,
  sessionStorage.deleteCallback
);
CustomSessionStorage.client = sessionStorage.client;
CustomSessionStorage.getAsync = sessionStorage.getAsync;
CustomSessionStorage.setAsync = sessionStorage.setAsync;
CustomSessionStorage.delAsync = sessionStorage.delAsync;

Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
  SCOPES: process.env.SCOPES.split(','),
  HOST_NAME: process.env.HOST.replace(/https:\/\/|\/$/g, ''),
  API_VERSION: ApiVersion.October20,
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: CustomSessionStorage,
});

app.prepare().then(async () => {
  const server = new Koa();
  const router = new Router();
  server.keys = [Shopify.Context.API_SECRET_KEY];
  server.use(cors());
  server.on('error', (err, ctx) => {
    const errorMessage = err.toString();
    if (
      errorMessage !==
        'BadRequestError: Expected a valid shop query parameter' &&
      errorMessage !== 'ClientError [BadRequestError]: Malicious Path'
    ) {
      if (errorMessage.startsWith('TimeoutError')) {
        console.log('Timeout restarting');
        process.exit(1);
      }
    }
  });

  if (dev) {
    server.use(mongo({ host: 'localhost', port: 27017, db: process.env.DB }));
  } else {
    server.use(
      mongo({
        host: 'localhost',
        port: 27017,
        user: '*****',
        pass: '******',
        authSource: process.env.DB,
        db: process.env.DB,
      })
    );
  }
  const responseUninstallWebhookHandler = async (topic, shop, body) => {
    const mongoDb = await MongoClientConnection.db();
    await mongoDb
      .collection('access_tokens')
      .updateOne({ shop }, { $set: { installStatus: 'uninstalled' } });
  };
  Shopify.Webhooks.Registry.webhookRegistry.push({
    path: '/webhooks',
    topic: 'APP_UNINSTALLED',
    webhookHandler: responseUninstallWebhookHandler,
  });

  server.use(
    createShopifyAuth({
      accessMode: 'offline',
      async afterAuth(ctx) {
        try {
          const { shop, accessToken, scope } = ctx.state.shopify;
          const host = ctx.query.host;
          const user = await getUser(ctx.db, shop);
          if (!user) {
            const result = await insertUser(ctx.db, shop, accessToken);
          } else {
            const result = await updateUser(ctx.db, shop, 'installed');
          }
          const responseUninstall = await Shopify.Webhooks.Registry.register({
            shop,
            accessToken,
            path: '/webhooks',
            topic: 'APP_UNINSTALLED',
            webhookHandler: responseUninstallWebhookHandler,
          });

          if (!responseUninstall.success) {
            console.log(
              `Failed to register APP_UNINSTALLED webhook: ${responseUninstall.result}`
            );
          }
          // Redirect to app with shop parameter upon auth
          ctx.redirect(`/?shop=${shop}&host=${host}`);
        } catch (e) {
          console.log(e, 'auth error');
        }
      },
    })
  );

  const handleRequest = async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  };

  router.post('/webhooks', async (ctx) => {
    try {
      await Shopify.Webhooks.Registry.process(ctx.req, ctx.res);
      console.log(`Webhook processed, returned status code 200`);
    } catch (error) {
      console.log(`Failed to process webhook: ${error}`);
    }
  });

  router.post(
    '/graphql',
    verifyRequest({ returnHeader: true }),
    async (ctx, next) => {
      await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
    }
  );

  router.get('(/_next/static/.*)', handleRequest); // Static content is clear
  router.get('/_next/webpack-hmr', handleRequest); // Webpack content is clear
  router.get('(.*)', async (ctx) => {
    console.log(ctx.query.host); // host is undefined here
    const shop = ctx.query.shop;
    const user = await getUser(ctx.db, shop);
    if (!user || user.installStatus !== 'installed') {
      ctx.redirect(`/auth?shop=${shop}`);
    } else {
      await handleRequest(ctx);
    }
  });

  server
    .use(AmazonProductRouter.routes())
    .use(AmazonProductRouter.allowedMethods());
  server.use(router.allowedMethods());
  server.use(router.routes());

  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
  //CronJob();
});

Below is my _app.js code.

import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';
import App from 'next/app';
import { AppProvider } from '@shopify/polaris';
import { Provider, useAppBridge } from '@shopify/app-bridge-react';
import { authenticatedFetch } from '@shopify/app-bridge-utils';
import { Redirect } from '@shopify/app-bridge/actions';
import '@shopify/polaris/dist/styles.css';
import translations from '@shopify/polaris/locales/en.json';

function userLoggedInFetch(app) {
  const fetchFunction = authenticatedFetch(app);

  return async (uri, options) => {
    const response = await fetchFunction(uri, options);

    if (
      response.headers.get('X-Shopify-API-Request-Failure-Reauthorize') === '1'
    ) {
      const authUrlHeader = response.headers.get(
        'X-Shopify-API-Request-Failure-Reauthorize-Url'
      );

      const redirect = Redirect.create(app);
      redirect.dispatch(Redirect.Action.APP, authUrlHeader || `/auth`);
      return null;
    }

    return response;
  };
}

function MyProvider(props) {
  const app = useAppBridge();

  const client = new ApolloClient({
    fetch: userLoggedInFetch(app),
    fetchOptions: {
      credentials: 'include',
    },
  });

  const Component = props.Component;

  return (
    
  );
}

class MyApp extends App {
  render() {
    const { Component, pageProps, host } = this.props;
      // host is undeifend here
    return (
      
    );
  }
}

MyApp.getInitialProps = async ({ ctx }) => {
  console.log(ctx); 
  return {
    host: ctx.query.host, // there is not host available in ctx.query
  };
};

export default MyApp;
​

export default MyApp;
I am using app-bridge-react version 2.0.2

I also have attached the screenshot of the error that appears while trying to install the app.

Any help would be great

Hi @Nabeel77 , I’m getting the same error. I’m new to shopify and just starting to learn how to build an app and I followed the instructions here: https://shopify.dev/apps/tools/cli/getting-started and when I use shopify app open and click on the link that it produces, I receive the same error. I can’t seem to find any solutions yet. Have you managed to solve it?

Hi @kandi_galaxy , Yes I have solved this issue. In my case, this solution was simple. When i deleted the app, the install status of user in my application’s database should have been changed from “installed” to “uninstall”. But, I accidentally uninstalled the test app when the server was not running so, the following function, responseUninstallWebhookHandler() function inside the server never got executed that would change the install status of the user to uninstall. So, everytime I try to reinstall the app, the server would respond as the app is already install and the code in else block would execute. When ever the app is installing, the authentication must be done and code inside if block should be executed.

router.get('(.*)', async (ctx) => {
    console.log(ctx.query.host); // host is undefined here
    const shop = ctx.query.shop;
    const user = await getUser(ctx.db, shop);
    if (!user || user.installStatus !== 'installed') {
      ctx.redirect(`/auth?shop=${shop}`);
    } else {
      await handleRequest(ctx);
    }
  });

I ended up manually changing the install status to unintsall in my database and everything worked.

For you, make sure that the code inside the if block is executing when u try to install the app. Your if condition might be different but,

ctx.redirect(`/auth?shop=${shop}`);

this line of code should execute when you try to install the app.