paweljedrzejczyk / shopify-multistore-app-middleware

Enable custom app to be used in multiple shopify stores
10 stars 2 forks source link

Use a /apikey route instead of a list of key #3

Open egillanton opened 1 year ago

egillanton commented 1 year ago

Instead of passing a list object of API keys to the frontend as process.env.SHOPIFY_APIKEYS and expose at the same time all the store to api key value mapping, why not create a route /apiKey_ for example that will return the corresponding apikey for a given store passed by query?

In my API route I also return a cookie so it will improve the performance and prevent redundant calls to this rout.

PROS:

CONS:

// @ts-check
import React, { useEffect, useMemo, useState } from "react";
import { useNavigate, usePath, useQueryParams } from "raviger";
import { Provider } from "@shopify/app-bridge-react";
import { Banner, Layout, Page, Stack, Spinner } from "@shopify/polaris";
import Cookies from "js-cookie";

/**
 * A component to configure App Bridge.
 * @desc A thin wrapper around AppBridgeProvider that provides the following capabilities:
 *
 * 1. Ensures that navigating inside the app updates the host URL.
 * 2. Configures the App Bridge Provider, which unlocks functionality provided by the host.
 *
 * See: https://shopify.dev/apps/tools/app-bridge/getting-started/using-react
 */
export function AppBridgeProvider({ children }) {
  const [loading, setLoading] = useState(true);
  const navigate = useNavigate();
  const location = usePath();
  const [query] = useQueryParams();
  const [appBridgeConfig, setAppBridgeConfig] = useState({
    host: undefined,
    apiKey: undefined,
    forceRedirect: true,
  });

  const history = useMemo(
    () => ({
      replace: (path) => {
        navigate(path);
      },
    }),
    [location]
  );

  const routerConfig = useMemo(
    () => ({ history, location }),
    [history, location]
  );

  useEffect(() => {
    const host = query.host || window.__SHOPIFY_DEV_HOST;

    window.__SHOPIFY_DEV_HOST = host;

    // Read the apiKey from a cookie
    let apiKey = Cookies.get("apiKey"); // Replace 'apiKey' with the actual cookie name

    const shop = query.shop || Cookies.get("shopOrigin");

    console.log("AppBridgeProvider", { host, apiKey, shop });

    // In case we still don't have an apiKey, fetch it from the server by shop
    if (!apiKey && query.shop) {
      const fetchApiKey = async () => {
        try {
          const fetchUrl = `https://${appOrigin}/apikey?shop=${shop}`;
          console.log("fetchApiKey", { fetchUrl });
          const response = await fetch(fetchUrl);
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          }

          const data = await response.json();
          apiKey = data.apiKey;
          setAppBridgeConfig((prevConfig) => ({
            ...prevConfig,
            host,
            apiKey,
          }));
          setLoading(false);
        } catch (error) {
          console.error("Failed to fetch API key:", error);
          // Handle error, maybe set some error state
          setLoading(false);
        }
      };

      fetchApiKey();
    } else {
      // If the apiKey is found in the cookie, set it immediately
      setAppBridgeConfig((prevConfig) => ({
        ...prevConfig,
        host,
        apiKey,
      }));
    }
  }, []);

  if (loading) {
    return (
      <Page narrowWidth>
        <Layout>
          <Layout.Section>
            <Stack distribution="center">
              <Spinner
                accessibilityLabel="Spinner example"
                size="large"
                color="teal"
              />
            </Stack>
          </Layout.Section>
        </Layout>
      </Page>
    );
  }

  if (!appBridgeConfig.apiKey || !appBridgeConfig.host) {
    const bannerProps = !appBridgeConfig.apiKey
      ? {
          title: "Missing Shopify API Key",
          children: (
            <>
              Your app is running without the SHOPIFY_API_KEY environment
              variable. Please ensure that it is set when running or building
              your React app.
            </>
          ),
        }
      : {
          title: "Missing host query argument",
          children: (
            <>
              Your app can only load if the URL has a <b>host</b> argument.
              Please ensure that it is set, or access your app using the
              Partners Dashboard <b>Test your app</b> feature
            </>
          ),
        };

    return (
      <Page narrowWidth>
        <Layout>
          <Layout.Section>
            <div style={{ marginTop: "100px" }}>
              <Banner {...bannerProps} status="critical" />
            </div>
          </Layout.Section>
        </Layout>
      </Page>
    );
  }

  return (
    <Provider config={appBridgeConfig} router={routerConfig}>
      {children}
    </Provider>
  );
}
daniyal741 commented 4 months ago

@egillanton , @paweljedrzejczyk - Not able to ge the shop name in the App Providers file. I'm getting the host as encoded value. Why atob is not working? HOST=YWRtaW4uc2hvcGlmeS5jb20vc3RvcmUvZGV2ZWxvcGVycy1zYW5kYm94

Screenshot 2024-07-06 at 3 48 52 PM
import { PropsWithChildren, useMemo, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { Provider } from "@shopify/app-bridge-react";
import { Banner, Layout, Page } from "@shopify/polaris";
import { To } from "history";

const sanitizedShopName = (shop: string): string =>
  shop
    .replace(/^https:\/\//, "")
    .replace(".myshopify.com", "")
    .replace("/admin", "")
    .replace(/-/g, "_")
    .toUpperCase();

declare global {
  interface Window {
    __SHOPIFY_DEV_HOST: string;
  }
}

/**
 * A component to configure App Bridge.
 * @desc A thin wrapper around AppBridgeProvider that provides the following capabilities:
 *
 * 1. Ensures that navigating inside the app updates the host URL.
 * 2. Configures the App Bridge Provider, which unlocks functionality provided by the host.
 *
 * See: https://shopify.dev/apps/tools/app-bridge/getting-started/using-react
 */
export function AppBridgeProvider({ children }: PropsWithChildren) {
  const location = useLocation();
  const navigate = useNavigate();
  const history = useMemo(
    () => ({
      replace: (path: To) => {
        navigate(path, { replace: true });
      },
    }),
    [navigate]
  );

  const routerConfig = useMemo(
    () => ({ history, location }),
    [history, location]
  );

  console.log(useParams(), location, navigate, history);

  // The host may be present initially, but later removed by navigation.
  // By caching this in state, we ensure that the host is never lost.
  // During the lifecycle of an app, these values should never be updated anyway.
  // Using state in this way is preferable to useMemo.
  // See: https://stackoverflow.com/questions/60482318/version-of-usememo-for-caching-a-value-that-will-never-change
  const [appBridgeConfig] = useState(() => {
    const host =
      new URLSearchParams(location.search).get("host") ||
      window.__SHOPIFY_DEV_HOST;

    window.__SHOPIFY_DEV_HOST = host;
    console.log("HOST", host, sanitizedShopName(host));
    const customApiKey = process.env.SHOPIFY_API_KEYS?.[
      `SHOPIFY_API_KEY_${sanitizedShopName(
        host ? window.atob(host) : ""
      )}` as keyof typeof process.env.SHOPIFY_API_KEYS
    ] as string;

    return {
      host,
      apiKey: customApiKey || process.env.SHOPIFY_API_KEY || "",
      forceRedirect: true,
    };
<img width="817" alt="Screenshot 2024-07-06 at 3 47 30 PM" src="https://github.com/paweljedrzejczyk/shopify-multistore-app-middleware/assets/77852628/52b280a2-b04f-4c3d-9196-1ccce9d9a232">

  });

  if (!appBridgeConfig.apiKey || !appBridgeConfig.host) {
    const bannerProps = !appBridgeConfig.apiKey
      ? {
          title: "Missing Shopify API Key",
          children: (
            <>
              Your app is running without the SHOPIFY_API_KEY environment
              variable. Please ensure that it is set when running or building
              your React app.
            </>
          ),
        }
      : {
          title: "Missing host query argument",
          children: (
            <>
              Your app can only load if the URL has a <b>host</b> argument.
              Please ensure that it is set, or access your app using the
              Partners Dashboard <b>Test your app</b> feature
            </>
          ),
        };

    return (
      <Page narrowWidth>
        <Layout>
          <Layout.Section>
            <div style={{ marginTop: "100px" }}>
              <Banner {...bannerProps} tone="critical" />
            </div>
          </Layout.Section>
        </Layout>
      </Page>
    );
  }

  return (
    <Provider config={appBridgeConfig} router={routerConfig}>
      {children}
    </Provider>
  );
}
daniyal741 commented 4 months ago

Also what about the build because if we deploy the app on Heroku it will use Shopify API Key from Docker for the first time?