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!