Shopify / theme-extension-getting-started

A theme app extension boilerplate that highlights the basic structure and features that are available to developers who want to integrate their apps with Shopify Online Stores.
https://shopify.dev/apps/online-store/theme-app-extensions/getting-started
MIT License
108 stars 45 forks source link

How to Activate Shopify Web Pixel Extension on Production Store? #34

Open KaizenTamashi opened 3 months ago

KaizenTamashi commented 3 months ago

Issue:

Hi, I'm trying to activate a Shopify web pixel extension on a production store. The web pixel extension is configured in the Shopify Remix app template. The backend is wired to Express.js.

What I've tried:

Approach_1: I was able to activate it on the dev/test store using the Shopify app dev GraphQL by following this guide https://shopify.dev/docs/apps/build/marketing-analytics/build-web-pixels#:~:text=To%20activate%20a%20web%20pixel,extension. However, I couldn't apply it on the production store since i cannot run shopify app dev on production store to open up the shopify GraphQL console.

mutation {
  # This mutation creates a web pixel, and sets the `accountID` declared in `shopify.extension.toml` to the value `123`.
  webPixelCreate(webPixel: { settings: "{\"accountID\":\"123\"}" }) {
    userErrors {
      code
      field
      message
    }
    webPixel {
      settings
      id
    }
  }
}

Approach_2: I've also tried this guide that activates the pixel using shopify remix app's loader function. Unfortunately it didn't work as well. https://community.shopify.com/c/extensions/how-do-you-actually-activate-the-web-pixel/m-p/2496617

shopify remix app template > app app._index.jsx

export const loader = async ({ request }) => { // Authenticate with Shopify const { admin } = await authenticate.admin(request); 
const mutationResponse = await admin.graphql( #graphql
        mutation webPixelCreate($webPixel: WebPixelInput!) {
          webPixelCreate(webPixel: $webPixel) {
            userErrors {
              field
              message
            }
            webPixel {
              settings
              id
            }
          }
        }
      , { variables: { webPixel: { settings: { "accountID": 'Account_ID_45466_this_is_as_per_toml'
}, }, }, } ); 
if (!mutationResponse.ok) { console.error('Request failed', mutationResponse); return; } 
const data = await mutationResponse.json(); console.log(data); 
return mutationResponse; 
}; 

const loaderDataForWebPixel = useLoaderData();

Approach_3: Oauth redirect and Token Exchange on Express.js endpoint routes>shopifyRouter.js file

import { shopify } from '../shopifyApi.js';
import { Router } from 'express';
import dotenv from 'dotenv';
import { shopify, RequestedTokenType } from '../shopifyApi.js'; 
import cookieParser from 'cookie-parser';
import axios from 'axios';

dotenv.config();

const router = Router();

// Middleware to set CSP headers for embedded apps
const setCSPHeaders = (req, res, next) => {
  const shop = req.query.shop;
  const shopDomain = shop ? `https://${shop}` : null;
  const shopifyAdminDomain = "https://admin.shopify.com";

  if (shopDomain) {
    res.setHeader("Content-Security-Policy", `frame-ancestors ${shopDomain} ${shopifyAdminDomain}`);
  } else {
    res.setHeader("Content-Security-Policy", `frame-ancestors ${shopifyAdminDomain}`);
  }
  next();
};

router.use((req, res, next) => {
  console.log(`Incoming request: ${req.method} ${req.url}`);
  next();
});

// Apply middleware
router.use(cookieParser());
router.use(setCSPHeaders);

// Route to handle the initial OAuth
router.get('/install', async (req, res) => {
  try {
    const shop = req.query.shop;

    if (!shop) {
      return res.status(400).json({ error: 'Missing "shop" query parameter' });
    }

    await shopify.auth.begin({
      shop,
      callbackPath: '/auth/callback',
      isOnline: false,
      rawRequest: req,
      rawResponse: res,
    });
  } catch (error) {
    console.error('Error during install:', error.message);
    console.error('Stack trace:', error.stack);
    res.status(500).json({ error: 'Error during install', message: error.message });
  }
});

// Route to handle the OAuth callback and activate web pixel
router.get('/auth/callback', async (req, res) => {
  try {
    const callbackResponse = await shopify.auth.callback({
      rawRequest: req,
      rawResponse: res,
    });
    const { session } = callbackResponse;
    const accessToken = session.accessToken;

    // Activate web pixel
    const graphqlUrl = `https://${session.shop}/admin/api/2023-07/graphql.json`;
    const graphqlHeaders = {
      'Content-Type': 'application/json',
      'X-Shopify-Access-Token': accessToken,
    };
    const graphqlMutation = {
      query: `
        mutation {
          webPixelCreate(webPixel: { settings: "{\\"accountID\\":\\"88888888\\"}" }) {
            userErrors {
              code
              field
              message
            }
            webPixel {
              settings
              id
            }
          }
        }
      `,
    };

    const graphqlResponse = await axios.post(graphqlUrl, graphqlMutation, { headers: graphqlHeaders });

    if (graphqlResponse.data.errors) {
      console.error('GraphQL errors:', graphqlResponse.data.errors);
      return res.status(500).json({ error: graphqlResponse.data.errors });
    }

    console.log('Web pixel activated:', graphqlResponse.data.data);
    res.json(graphqlResponse.data.data);
  } catch (error) {
    console.error('Error during OAuth callback:', error.message);
    console.error('Stack trace:', error.stack);
    res.status(500).json({ error: 'Error during OAuth callback', message: error.message });
  }
});

// Route get access token and activate web pixel
router.get('/auth', async (req, res) => {
  try {
    const shop = shopify.utils.sanitizeShop(req.query.shop, true);
    const headerSessionToken = getSessionTokenHeader(req);
    const searchParamSessionToken = getSessionTokenFromUrlParam(req);
    const sessionToken = headerSessionToken || searchParamSessionToken;

    if (!sessionToken) {
      return res.status(400).json({ error: 'Missing session token' });
    }

    const session = await shopify.auth.tokenExchange({
      sessionToken,
      shop,
      requestedTokenType: RequestedTokenType.OfflineAccessToken, // or RequestedTokenType.OnlineAccessToken
    });

    // Activate web pixel
    const accessToken = session.accessToken;
    console.log("🚀 ~ file: shopifyRouter.js:132 ~ router.get ~ accessToken:", accessToken);

    const graphqlUrl = `https://${shop}/admin/api/2023-07/graphql.json`;
    const graphqlHeaders = {
      'Content-Type': 'application/json',
      'X-Shopify-Access-Token': accessToken,
    };
    const graphqlMutation = {
      query: `
        mutation {
          webPixelCreate(webPixel: { settings: "{\\"accountID\\":\\"88888888\\"}" }) {
            userErrors {
              code
              field
              message
            }
            webPixel {
              settings
              id
            }
          }
        }
      `,
    };

    const graphqlResponse = await axios.post(graphqlUrl, graphqlMutation, { headers: graphqlHeaders });

    if (graphqlResponse.data.errors) {
      console.error('GraphQL errors:', graphqlResponse.data.errors);
      return res.status(500).json({ error: graphqlResponse.data.errors });
    }

    console.log('Web pixel activated:', graphqlResponse.data.data);
    res.json(graphqlResponse.data.data);
  } catch (error) {
    console.error('Error during token exchange:', error.message);
    console.error('Stack trace:', error.stack);
    res.status(500).json({ error: 'Error during token exchange', message: error.message });
  }
});

// Helper functions to get session token
function getSessionTokenHeader(request) {
  const authHeader = request.headers.authorization;
  if (authHeader && authHeader.startsWith('Bearer ')) {
    return authHeader.substring(7);
  }
  return null;
}

function getSessionTokenFromUrlParam(request) {
  return request.query.id_token || null;
}

export default router;

shopifyApi.js file

import '@shopify/shopify-api/adapters/node'; 
import { shopifyApi, LATEST_API_VERSION, RequestedTokenType } from '@shopify/shopify-api';

import dotenv from 'dotenv';

dotenv.config();

const myAppsLogFunction = (severity, message) => {
  console.log(`[${severity}] ${message}`);
};

let shopify;

try {
  shopify = shopifyApi({
    apiKey: process.env.SHOPIFY_API_KEY,
    apiSecretKey: process.env.SHOPIFY_API_SECRET,
    scopes: ['read_products', 'write_products', 'read_customer_events', 'write_pixels'],
    hostName: process.env.SHOPIFY_APP_HOST,    
    hostScheme: 'https',
    apiVersion: LATEST_API_VERSION,    
    isEmbeddedApp: true,
    isCustomStoreApp: false,
    userAgentPrefix: 'Custom prefix',
    logger: {
      log: (severity, message) => {
        myAppsLogFunction(severity, message);
      },
      level: 'info',
      httpRequests: true,
      timestamps: true,
    },
    future: {
      unstable_newEmbeddedAuthStrategy: true,
    }
  });

} catch (error) {
  console.log('shopifyApi.js error', error);
}

export { shopify, RequestedTokenType };

Here are the approach_3 's results