App works on Ngrok, but not on a domain!

Solved

App works on Ngrok, but not on a domain!

keiraarts
Shopify Partner
53 7 11

This is a GraphQL mystery! When testing my embedded app on a local environment everything works perfectly.

If I push to zeit and change my app details to the aliased domain - the app just responds with Network error: Unexpected token < in JSON at position 0. Attempting to install it from the partners dashboard also just ignores the entire charges screen if it's installed without Ngrok.

I'm following the embedded app tutorial with Node and React from Shopify.

.env

SHOPIFY_API_KEY=000000
SHOPIFY_API_SECRET_KEY=000000
TUNNEL_URL='https://28a6d8fc.ngrok.io'
// switching the tunnel URL to a domain breaks the app
SERVER_URL='https://viaglamour.com'
API_VERSION=2019-04
server.js 

require("isomorphic-fetch");
const Koa = require("koa");
const WPAPI = require("wpapi");
const next = require("next");
const { default: createShopifyAuth } = require("@shopify/koa-shopify-auth");
const dotenv = require("dotenv");
const { verifyRequest } = require("@shopify/koa-shopify-auth");
const session = require("koa-session");
const axios = require("axios");

dotenv.config();
const { default: graphQLProxy } = require("@shopify/koa-shopify-graphql-proxy");
const { ApiVersion } = require("@shopify/koa-shopify-graphql-proxy");
const Router = require("koa-router");
const {
  receiveWebhook,
  registerWebhook
} = require("@shopify/koa-shopify-webhooks");
const processPayment = require("./server/router");

const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

const {
  SHOPIFY_API_SECRET_KEY,
  SHOPIFY_API_KEY,
  TUNNEL_URL,
  SERVER_URL,
  API_VERSION
} = process.env;

app.prepare().then(() => {
  const server = new Koa();
  const router = new Router();
  server.use(session(server));
  server.keys = [SHOPIFY_API_SECRET_KEY];

  router.get("/", processPayment);

  server.use(
    createShopifyAuth({
      apiKey: SHOPIFY_API_KEY,
      secret: SHOPIFY_API_SECRET_KEY,
      scopes: [
        "read_products",
        "write_products",
        "read_orders",
        "read_fulfillments",
        "write_fulfillments",
        "write_shipping",
        "write_orders",
        "write_inventory"
      ],
      async afterAuth(ctx) {
        const { shop, accessToken } = ctx.session;

        const username = shop.replace(".myshopify.com", "");

        ctx.cookies.set("shopOrigin", shop, { httpOnly: false });
        ctx.cookies.set("token", accessToken, { httpOnly: false });
        ctx.cookies.set("username", username, { httpOnly: false });

        const stringifiedBillingParams = JSON.stringify({
          recurring_application_charge: {
            name: "Cosmetic Formulations",
            price: 40,
            return_url: TUNNEL_URL,
            test: true,
            trial_days: 14,
            capped_amount: 40,
            terms:
              ""
          }
        });

        const options = {
          method: "POST",
          body: stringifiedBillingParams,
          credentials: "include",
          headers: {
            "X-Shopify-Access-Token": accessToken,
            "Content-Type": "application/json"
          }
        };

        const confirmationURL = await fetch(
          `https://${shop}/admin/api/${API_VERSION}/recurring_application_charges.json`,
          options
        )
          .then(response => response.json())
          .then(
            jsonData => jsonData.recurring_application_charge.confirmation_url
          )
          .catch(error => console.log("error", error));
        ctx.redirect(confirmationURL);
      }
    })
  );

  const webhook = receiveWebhook({ secret: SHOPIFY_API_SECRET_KEY });

  router.post("/webhooks/products/create", webhook, ctx => {
    console.log("received webhook: ", ctx.state.webhook);
    console.log("webhook: ", webhook);
  });

  server.use(graphQLProxy({ version: ApiVersion.April19 }));

  router.get("*", verifyRequest(), async ctx => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  });
  server.use(router.allowedMethods());
  server.use(router.routes());

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

import App from "next/app";
import Head from "next/head";
import { AppProvider } from "@shopify/polaris";
import "@shopify/polaris/styles.css";
import Cookies from "js-cookie";
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import { HttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import fetch from "isomorphic-fetch";
require("node-fetch");

const client = new ApolloClient({
  fetchOptions: {
    credentials: "include"
  }
}); 

// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
  global.fetch = fetch;
}

class MyApp extends App {
  state = {
    shopOrigin: Cookies.get("shopOrigin")
  };

  render() {
    console.log("shopOrigin", this.state.shopOrigin);

    const { Component, pageProps } = this.props;
    return (
      <React.Fragment>
        <Head>
          <title>viaGlamour Ltd.</title>
          <meta charSet="utf-8" />
          <script src="https://js.stripe.com/v3/" />
        </Head>

        <AppProvider
          shopOrigin={this.state.shopOrigin}
          apiKey={API_KEY}
          forceRedirect
        >
          <ApolloProvider client={client}>
            <Component {...pageProps} />
          </ApolloProvider>
        </AppProvider>
      </React.Fragment>
    );
  }
}
export default MyApp;
.component (that does graphQL)

import gql from "graphql-tag";
import { Query } from "react-apollo";
import {
  Card,
  ResourceList,
  Stack,
  Thumbnail,
  DisplayText,
  SkeletonBodyText,
  TextContainer,
  Layout,
  SkeletonPage,
  SkeletonDisplayText,
  Page,
  EmptyState,
  FooterHelp,
  Link,
  VisuallyHidden,
  Banner,
  Pagination,
  Button,
  TextStyle,
  InlineError
} from "@shopify/polaris";
import store from "store-js";
import { Redirect } from "@shopify/app-bridge/actions";
import * as PropTypes from "prop-types";

import { getPrice, getRecommendedPrice } from "../common/prices.js";
import API from "../common/api.js";

import CustomerListItem from "../components/CustomerListItem";
import BasicListItem from "../components/BasicListItem";
import IndexPagination from "../components/IndexPagination";

import { getUsername, formatProductName } from "../common/helpers";

import Cookies from "js-cookie";
import axios from "axios";

const GET_PRODUCTS = gql`
  query Dog(
    $query: String!
    $after: String
    $before: String
    $first: Int
    $last: Int
  ) {
    products(
      first: $first
      last: $last
      after: $after
      before: $before
      query: $query
    ) {
      edges {
        cursor
        node {
          id
          title
          featuredImage {
            originalSrc
          }
          metafields(first: 6, namespace: "viaglamour") {
            edges {
              node {
                key
                value
              }
            }
          }
          variants(first: 1) {
            edges {
              node {
                id
                price
                inventoryItem {
                  id
                }
                fulfillmentService {
                  handle
                }
              }
            }
          }
        }
      }
    }
  }
`;
class ResourceListWithProducts extends React.Component {
  state = {
    item: "",
    data: "",
    searchValue: "",
    credits_required: 0,
    pageIndex: 0,
    afterCursor: null,
    beforeCursor: null,
    resultsPerPage: 10,
    first: 10,
    last: null,
    query: "vendor:viaglamour",
    sortValue: "DATE_MODIFIED_DESC"
  };

  render() {
  
    return (
      <Query
        query={GET_PRODUCTS}
        variables={{
          after: this.state.afterCursor,
          before: this.state.beforeCursor,
          first: this.state.first,
          last: this.state.last,
          query: this.state.query
          //vendor:viaglamour AND product_type:lipstick
        }}
      >
        {({ data, loading, error }) => {
          if (
            loading &&
            (this.state.pageIndex > 0 ||
              this.state.query != "vendor:viaglamour")
          ) {
            <Page title="Available Products">
              <DisplayText size="small">
                Browse products from your makeup line.
              </DisplayText>{" "}
              <br />{" "}
              <Layout>
                <Layout.Section>
                  <Card sectioned>
                    <SkeletonBodyText />
                  </Card>
                  <Card sectioned>
                    <TextContainer>
                      <SkeletonDisplayText size="small" />
                      <SkeletonBodyText />
                    </TextContainer>
                  </Card>
                </Layout.Section>
              </Layout>
            </Page>;
          } else if (loading)
            return (
              <SkeletonPage primaryAction secondaryActions={2}>
                <Layout>
                  <Layout.Section>
                    <Card sectioned>
                      <SkeletonBodyText />
                    </Card>
                    <Card sectioned>
                      <TextContainer>
                        <SkeletonDisplayText size="small" />
                        <SkeletonBodyText />
                      </TextContainer>
                    </Card>
                  </Layout.Section>
                </Layout>
              </SkeletonPage>
            );
          if (error) return <div>{error.message}</div>;

          return (
            <Page title="Available Products">
              <DisplayText size="small">
                Browse products from your makeup line.
              </DisplayText>
           
            </Page>
          );
        }}
      </Query>
    );
  }
}
export default ResourceListWithProducts;


Stack Overflow suggests the error is happening because GraphQL is returning HTML because of an incorrect endpoint. But I don't understand what breaks the app just by switching the tunnel URL to a real domain.

Any hints would really help! So close to publishing!

Head of space operations on the Shopify moon-base.
Building orderediting.com, viaGlamour.com, magicsoaps.ca
Accepted Solution (1)
keiraarts
Shopify Partner
53 7 11

This is an accepted solution.

Sorry for not closing this thread! The issue got solved on Github.

For anyone else in the future - https://github.com/Shopify/shopify-demo-app-node-react/issues/32

Head of space operations on the Shopify moon-base.
Building orderediting.com, viaGlamour.com, magicsoaps.ca

View solution in original post

Replies 6 (6)

Ryan
Shopify Staff
499 42 120

I have a hunch, that may or may not be right, but is worth trying.  Can you try changing 

 

"Content-Type": "application/json"

to

"Content-Type": "application/graphql"

for post requests when you are running it on ZEIT? I believe the way you are sending your GraphQL bodies it's being interpreted as a string instead of graphql, and this can help if thats the case.  Not sure 100% why it would work with ngrok and not on ZEIT but could be config somewhere also.

 

It also could not be the problem 🙂

Cheers!
Ryan

Ryan | Shopify 
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution
 - To learn more visit the Shopify Help Center or the Shopify Blog

keiraarts
Shopify Partner
53 7 11

Thanks for replying! I tried introducing that header response - but no changes.

I noticed that when using my domain name the cookies are never set. 

The domain can't authenticate, even though it's still using the same authentication library from Shopify! https://www.npmjs.com/package/@shopify/koa-shopify-auth

The /auth URL request just doesn't get recognized when using the real domain.

GraphQL might not be working because there are no cookies to authenticate the request.

GIF demo: https://gyazo.com/78e3a16fef817e73551895670eebaa5a

Then, maybe the real problem is that the KOA server doesn't understand /auth on my domain?

Is there any possible way to have the apps team look at this? I'm so close, yet so far away haha!









On my own domain it just shows a 404 not found.


Head of space operations on the Shopify moon-base.
Building orderediting.com, viaGlamour.com, magicsoaps.ca
Josh
Shopify Staff
1134 84 235

Hey @keiraarts ,

 

This is a tricky one because our logs don't have any information available to indicate where the problem could be, this looks like a scenario where the debugging will have to be done on your end. I found a few articles that should provide some threads you can pull that'll hopefully help you uncover what is happening though : 

 

https://stackoverflow.com/questions/47950455/react-apollo-error-network-error-unexpected-token-in-js... 

 

https://github.com/graphql-boilerplates/node-graphql-server/issues/274#issuecomment-506995478

 

https://github.com/apollographql/apollo-feature-requests/issues/153

 

https://daveceddia.com/unexpected-token-in-json-at-position-0/

Josh | Shopify 
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution
 - To learn more visit the Shopify Help Center or the Shopify Blog

keiraarts
Shopify Partner
53 7 11

This is an accepted solution.

Sorry for not closing this thread! The issue got solved on Github.

For anyone else in the future - https://github.com/Shopify/shopify-demo-app-node-react/issues/32

Head of space operations on the Shopify moon-base.
Building orderediting.com, viaGlamour.com, magicsoaps.ca
playablefuture
Visitor
2 0 2

there are no issues on that repository anymore, can you please share the solution?  thanks!

tarun-gujjula
New Member
5 0 0

Would you mind sharing the solution for the same. Thank you.