Shopify / koa-shopify-auth

DEPRECATED Middleware to authenticate a Koa application with Shopify
MIT License
80 stars 63 forks source link

How to getSessionToken and use it in fetch API directly? #74

Closed ankesh7 closed 3 years ago

ankesh7 commented 3 years ago

Issue summary

I am trying to get a session token using import { getSessionToken } from @shopify/app-bridge-utils so that I can get token and add it to the Authorization header in my fetch calls. I am getting this error

AppBridgeError: APP::ERROR::INVALID_CONFIG: shopOrigin must be provided
. This happened while I tried to upgrade to the latest version of the library which handles. I went through the upgrade guide and just wasn't sure about the last part that says to replace all fetch calls in frontend to authenticatedFetch. In my case, I am not using GraphQL hence all of my calls are wired up using native fetch calls that's speak to koa routes. Based on this source I am trying to get tokens and failing to get one. All of my fetch calls are redirecting to /auth I guess because of the missing Authorization token and verify request middleware fails.

My app works well on every install as expected is functions as an embedded app on initial load but all of the fetch calls on user interaction don't work because of the above issue.

_app.js

import React from 'react';
import App from 'next/app';
import Head from 'next/head';
import { AppProvider } from '@shopify/polaris';
import '@shopify/polaris/dist/styles.css';
import translations from '@shopify/polaris/locales/en.json';
import { ThemeProvider } from 'styled-components'
import { Provider } from '@shopify/app-bridge-react';

const theme = {};
class MyApp extends App {
    render() {
        const { Component, pageProps, shopOrigin } = this.props;
        const config = { apiKey: API_KEY, shopOrigin, forceRedirect: true };
        console.log(config);

        return (
            <React.Fragment>
                <Head>
                    <title>Custom App for Shopify</title>
                    <meta charSet="utf-8" />
                </Head>
                <Provider config={config}>
                    <AppProvider i18n={translations} features={{ newDesignLanguage: true }}>
                        <ThemeProvider theme={theme}>
                            <Component {...pageProps} />
                        </ThemeProvider>
                    </AppProvider>
                </Provider>
            </React.Fragment>
        );
    }
}

MyApp.getInitialProps = async ({ ctx }) => {
    return {
        shopOrigin: ctx.query.shop,
    }
}

export default MyApp;

ComponentNeedingToken.js

import React, { useEffect } from 'react'
import createApp from "@shopify/app-bridge";
import { getSessionToken } from "@shopify/app-bridge-utils";

export default function MyComponent() {

    useEffect(async () => {
        const app = createApp({
            apiKey: "abc",
        });
        const sessionToken = await getSessionToken(app);
        console.log(sessionToken);
    }, []);

    return (
        <p>Hey</p>
    )
}

Console print for app config to ensure shopOrigin is set by the server. This is printed both serverside and client-side as I am using Next.js

{
  apiKey: 'abc',
  shopOrigin: 'mydomain.myshopify.com',
  forceRedirect: true
}

server.js

import "@babel/polyfill";
require('isomorphic-fetch');

import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
import next from 'next';
import shopifyAuth, { verifyRequest } from '@shopify/koa-shopify-auth';
import Shopify, { ApiVersion } from '@shopify/shopify-api';
import dotenv from 'dotenv';
import RedisStore from './util/redis-store';

dotenv.config();

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, HOST } = process.env;

const sessionStorage = new RedisStore();

Shopify.Context.initialize({
    API_KEY: SHOPIFY_API_KEY,
    API_SECRET_KEY: SHOPIFY_API_SECRET_KEY,
    SCOPES: ['read_products', 'write_products', 'read_orders'],
    HOST_NAME: HOST,
    API_VERSION: ApiVersion.January21,
    IS_EMBEDDED_APP: true,
    // More information at https://github.com/Shopify/shopify-node-api/blob/main/docs/issues.md#notes-on-session-handling
    SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
        sessionStorage.storeCallback,
        sessionStorage.loadCallback,
        sessionStorage.deleteCallback,
    ),
});

app.prepare().then(() => {

    const server = new Koa();
    const router = new Router();

    // Disable bodyparser for webhook routes as process handler requires http request
    server.use(async (ctx, next) => {
        if (ctx.path.includes('/webhooks/')) ctx.disableBodyParser = true;
        await next();
    });

    server.use(bodyParser());
    server.keys = [Shopify.Context.API_SECRET_KEY];

    // Storing the currently active shops in memory will force them to re-login when your server restarts. You should
    // persist this object in your app.
    const ACTIVE_SHOPIFY_SHOPS = {};

    server.use(
        shopifyAuth({
            accessMode: 'online',
            async afterAuth(ctx) {
                const { shop, scope } = ctx.state.shopify;
                ACTIVE_SHOPIFY_SHOPS[shop] = scope;
                // TODO: Handle uninstalls here
                ctx.redirect(`/?shop=${shop}`);
            }
        }));

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

    router.post("/api/import/all", verifyRequest(), async ctx => {
        console.log("This never fires because requests cannot be verified");
    });

    router.get("(/_next/static/.*)", handleRequest);

    router.get("/_next/webpack-hmr", handleRequest);

    router.get("/", async (ctx) => {
        const shop = ctx.query.shop;
        if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
            ctx.redirect(`/auth?shop=${shop}`);
        } else {
            await handleRequest(ctx);
        }
    });

    router.get("(.*)", verifyRequest(), handleRequest);

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

    server.listen(port, () => {
        console.log(`> Ready on http://localhost:${port}`);
    });
});
hrstrand commented 3 years ago

in your ComponentNeedingToken.js, try to get app by

const app = useAppBridge();

and get token from here :

const token = await getSessionToken(app);

ankesh7 commented 3 years ago

Thanks @hrstrand that surely worked. I don't have experience creating GitHub issues but I can tell your reply with the solution surely belongs to among the fastest (< 30s). Cheers dude 😄