ParcelPanel Headless React Components Integration Guide

Complete guide for integrating ParcelPanel shipping tracking components into Shopify Hydrogen Storefront projects.

Hydrogen Storefront Integration (SSR)

Pure Client-Side Rendering Projects (CSR)

Multi-language Support

Custom Styling

Checklist

Important Reminder

Technical Support

Version Updates


Component Overview

NPM Package​: @cw-parcelpanel/headless-react

ParcelPanel provides two core components for Shopify Headless stores:

  • TrackingWidget- Order Tracking component (for tracking pages)
  • ParcelPanelEDD- Estimated Delivery Date component (for product pages)

Component Features

  • Shopify Native - Built specifically for the Shopify ecosystem
  • Headless Ready - Perfect fit for custom storefronts and mobile apps
  • Enterprise Grade - Trusted by tens of thousands of Shopify merchants
  • Developer Friendly - Clear API with full TypeScript support
  • Responsive Design - Automatically adapts to different screen sizes
  • Multi-language Support - Built-in internationalization
  • Performance Optimized - Lightweight and efficient rendering

Installation

npm install @cw-parcelpanel/headless-react

⚠️ Important: SSR Compatibility Notice

If your project uses Server-Side Rendering (SSR) (like Hydrogen, Next.js App Router, Remix, etc.), you need special handling because ParcelPanel components depend on the browser's document object.

If your project is purely Client-Side Rendering (CSR) (like Create React App, Vite SPA, etc.), you can use the components directly without special handling.

Hydrogen Storefront Integration (SSR)

1. Create SSR-Safe Components

Create Client-Only Component File

Create app/components/ParcelPanelComponents.tsx:

import { useState, useEffect } from 'react'


interface TrackingWidgetProps {
  shopDomain: string
  locale: string
  className?: string
  onLoaded?: () => void
  onError?: (error: any) => void
}

interface ParcelPanelEDDProps {
  shopDomain: string
  productId: string
  locale: string
  customer: {
    country_code: string
    province_code: string
  }
  onLoaded?: (sdk: any) => void
  onError?: (error: any) => void
}

/**
 * TrackingWidget Component - Client-side only
 * Usage:
 * <TrackingWidget shopDomain="..." locale="..." />
 */
export function TrackingWidget(props: TrackingWidgetProps) {
  const mounted = useClientOnly()
  const [Component, setComponent] = useState<any>(null)

  useEffect(() => {
    if (!mounted) return

    // Dynamic import of ParcelPanel component
    import('@cw-parcelpanel/headless-react')
      .then((module) => {
        setComponent(() => module.TrackingWidget)
      })
      .catch((err) => {
        console.error('Failed to load TrackingWidget:', err)
        props.onError?.(err)
      })
  }, [mounted, props])

  // Return null during SSR
  if (!mounted) {
    return null
  }

  // Loading state
  if (!Component) {
    return (
      <div className="text-gray-500 text-sm">
        Loading order tracking...
      </div>
    )
  }

  // Render component
  return <Component {...props} />
}

/**
 * Extract numeric ID from Shopify GID
 * Example: gid://shopify/Product/123456 -> 123456
 */
function extractShopifyId(gid: string): string {
  if (!gid) return ''
  
  // If already numeric, return as-is
  if (/^\d+$/.test(gid)) {
    return gid
  }
  
  // Extract ID from gid://shopify/Product/123456 format
  const match = gid.match(/gid:\/\/shopify\/Product\/(\d+)/)
  return match ? match[1] : gid
}

/**
 * Simple client-only check hook
 */
function useClientOnly() {
  const [mounted, setMounted] = useState(false)
  
  useEffect(() => {
    setMounted(true)
  }, [])
  
  return mounted
}

/**
 * ParcelPanel EDD Component - Client-side only
 * Usage:
 * <ParcelPanelEDD shopDomain="..." productId="..." locale="..." customer={{...}} />
 */
export function ParcelPanelEDD(props: ParcelPanelEDDProps) {
  const mounted = useClientOnly()
  const [Component, setComponent] = useState<any>(null)

  useEffect(() => {
    if (!mounted) return

    // Dynamic import of ParcelPanel component
    import('@cw-parcelpanel/headless-react')
      .then((module) => {
        setComponent(() => module.ParcelPanelEDD)
      })
      .catch((err) => {
        console.error('Failed to load ParcelPanelEDD:', err)
        props.onError?.(err)
      })
  }, [mounted, props])

  // Return null during SSR
  if (!mounted) {
    return null
  }

  // Loading state
  if (!Component) {
    return (
      <div className="text-gray-500 text-sm">
        Loading delivery information...
      </div>
    )
  }

  // Render component
  const numericProductId = extractShopifyId(props.productId)
  
  return <Component {...props} productId={numericProductId} />
}

2. Configure Content Security Policy (CSP)

⚠️ Important: Hydrogen Storefront CSP Restrictions

Hydrogen Storefront Special Requirements:

Shopify Hydrogen framework enables strict Content Security Policy (CSP) by default for:

  • Security Protection - Prevent XSS attacks and malicious script injection
  • Performance Optimization - Control resource loading for better performance
  • Mobile Optimization - Ensure security and performance on mobile devices

Default CSP Restrictions:

default-src 'self' https://cdn.shopify.com https://shopify.com http://localhost:*

This means by default, Hydrogen ​only allows​:

  • ✅ Same-origin resources ('self')
  • ✅ Official Shopify CDN
  • ✅ Local development server

ParcelPanel Component Resource Requirements:

ParcelPanel components need access to the following external resources at runtime:

  • Load external scripts - Get tracking logic from pp-proxy.parcelpanel.com
  • Display image resources - Carrier logos, ParcelPanel logos, etc.
  • Load web fonts - Ensure component UI status icons display correctly
  • Establish network connections - Real-time delivery information and tracking data
  • Embed iframes - Tracking features may need to load external pages (Google Maps)

Consequences of Not Updating CSP:

If you don't update CSP configuration, Hydrogen's strict security policy will block these resource loads, causing browser console errors:

❌ Refused to load the script 'https://pp-proxy.parcelpanel.com/...' 
   because it violates the following Content Security Policy directive

❌ Refused to load the image 'https://cdn.parcelpanel.com/...' 
   because it violates the following Content Security Policy directive

❌ Refused to connect to 'https://pp-proxy.parcelpanel.com/api/...' 
   because it violates the following Content Security Policy directive

Result:

  • ❌ Components cannot display properly
  • ❌ Delivery information retrieval fails
  • ❌ Console shows numerous CSP errors
  • ❌ Users see blank or broken component interface

⚠️ This is a REQUIRED step for Hydrogen projects, not optional!

CSP Configuration Steps

Update CSP configuration in app/entry.server.tsx:

import type {AppLoadContext} from '@shopify/remix-oxygen';
import {ServerRouter} from 'react-router';
import {isbot} from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {createContentSecurityPolicy} from '@shopify/hydrogen';
import type {EntryContext} from 'react-router';

export default async function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  reactRouterContext: EntryContext,
  context: AppLoadContext,
) {
  const {nonce, header, NonceProvider} = createContentSecurityPolicy({
    shop: {
      checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
      storeDomain: context.env.PUBLIC_STORE_DOMAIN,
    },
    // Allow network connections - API calls and delivery data
    connectSrc: [
      "'self'",
      'https://pp-proxy.parcelpanel.com',    // ParcelPanel main API
      'https://cdn.parcelpanel.com',         // ParcelPanel CDN
      'https://*.shopify.com',               // Shopify services
    ],
    // Allow script loading - ParcelPanel core functionality
    scriptSrc: [
      "'self'",
      "'unsafe-inline'",                     // Inline script support
      'https://pp-proxy.parcelpanel.com',    // ParcelPanel scripts
      'https://cdn.parcelpanel.com',         // ParcelPanel CDN scripts
      'https://*.shopify.com',               // Shopify scripts
    ],
    // Allow image loading - Carrier icons, status images, etc.
    imgSrc: [
      "'self'",
      'data:',                               // Base64 image support
      'https:',                              // All HTTPS images
      'https://pp-proxy.parcelpanel.com',    // ParcelPanel images
      'https://cdn.parcelpanel.com',         // ParcelPanel CDN images
      'https://*.shopifycdn.net',            // Shopify CDN images
    ],
    // Allow font loading - Component interface fonts
    fontSrc: [
      "'self'",
      'data:',                               // Base64 font support
      'https://pp-proxy.parcelpanel.com',    // ParcelPanel fonts
      'https://cdn.parcelpanel.com',         // ParcelPanel CDN fonts
      'https://*.shopifycdn.net',            // Shopify fonts
    ],
    // Allow iframe embedding - Certain tracking features
    frameSrc: [
      "'self'",
      'https://*.google.com',                // Google Maps and services
      'https://pp-proxy.parcelpanel.com',    // ParcelPanel iframes
      'https://cdn.parcelpanel.com',         // ParcelPanel CDN
    ],
  });

  // ... rest of the code remains unchanged
  const body = await renderToReadableStream(
    <NonceProvider>
      <ServerRouter
        context={reactRouterContext}
        url={request.url}
        nonce={nonce}
      />
    </NonceProvider>,
    {
      nonce,
      signal: request.signal,
      onError(error) {
        console.error(error);
        responseStatusCode = 500;
      },
    },
  );

  if (isbot(request.headers.get('user-agent'))) {
    await body.allReady;
  }

  responseHeaders.set('Content-Type', 'text/html');
  responseHeaders.set('Content-Security-Policy', header);

  return new Response(body, {
    headers: responseHeaders,
    status: responseStatusCode,
  });
}

Configuration Points Explained

CSP DirectivePurposeParcelPanel Requirements
connectSrcControl network requestsGet delivery info, API calls
scriptSrcControl script loadingLoad tracking logic, component features
imgSrcControl image loadingCarrier icons, status icons
fontSrcControl font loadingComponent interface font rendering
frameSrcControl iframeMap display, external tracking pages

Security Notes

These CSP configurations are required but still maintain security:

  • Specific domain whitelist - Only allow ParcelPanel and official Shopify domains
  • Protocol restrictions - Primarily HTTPS, HTTP only for development
  • The Least privilege principle - Only add minimum permissions needed for ParcelPanel
  • Shopify ecosystem compatibility - Fully compatible with Shopify security standards

3. Configure Vite SSR External

In vite.config.ts:

import {defineConfig} from 'vite';
import {hydrogen} from '@shopify/hydrogen/vite';
import {oxygen} from '@shopify/mini-oxygen/vite';
import {reactRouter} from '@react-router/dev/vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [
    tailwindcss(),
    hydrogen(),
    oxygen(),
    reactRouter(),
    tsconfigPaths(),
  ],
  build: {
    // Allow a strict Content-Security-Policy
    // withtout inlining assets as base64:
    assetsInlineLimit: 0,
  },
  ssr: {
    // Exclude ParcelPanel from server-side rendering
    external: ['@cw-parcelpanel/headless-react'],
    optimizeDeps: {
      include: [],
    },
  },
});

4. Use TrackingWidget Component in Tracking Pages

Create app/routes/($locale).ordertrack.tsx:

import {type MetaFunction} from 'react-router';
import { TrackingWidget } from '~/components/ParcelPanelComponents';

export const meta: MetaFunction = () => {
  return [{title: `Hydrogen | Order Track`}];
};

export default function OrderTrack() {
  return (
    <div className="order-track">
      <TrackingWidget
        shopDomain="your-shop.myshopify.com"
        locale="en"
        className="tracking-container"
        onLoaded={() => console.log('Tracking widget loaded')}
        onError={(error) => console.error('Tracking error:', error)}
      />
    </div>
  );
}
​

Benefits of this approach:

  • Dynamic Addition - Order Track will appear regardless of Shopify admin menu configuration
  • Avoid Duplicates - Automatically checks to prevent duplicate menu items
  • More Flexible - Not dependent on FALLBACK_HEADER_MENU, works with all menu configurations

5. Use EDD Component in Product Pages

In app/routes/($locale).products.$handle.tsx:

import { ParcelPanelEDD } from '~/components/ParcelPanelComponents';

export default function Product() {
  const {product} = useLoaderData<typeof loader>();
  
  // ... other code

  return (
    <div className="product">
      {/* Your product content */}
      
      {/* ParcelPanel EDD Widget */}
      <ParcelPanelEDD
        shopDomain="your-shop.myshopify.com"
        productId={product.id}
        locale="en"
        customer={{ // For automatic user location detection (optional)
          country_code: "US", // From user location or checkout info
          province_code: "CA"
        }}
        onLoaded={(sdk) => console.log('EDD widget loaded:', sdk)}
        onError={(error) => console.error('EDD error:', error)}
      />
    </div>
  );
}

Pure Client-Side Rendering Projects (CSR)

If your project is purely client-side rendered (no SSR), you can use components directly without wrappers:

CSP Configuration Differences

Important Distinction:

Project TypeCSP Configuration MethodRequired?
Hydrogen StorefrontConfigure in app/entry.server.tsxREQUIRED - Strict default CSP
Next.js (App Router)next.config.js or middleware⚠️ Might need - Depends on config
Next.js (Pages Router)next.config.js or custom headers⚠️ Might need - Depends on config
Create React AppNo built-in CSPUsually not needed
Vite ReactNo built-in CSPUsually not needed
RemixSimilar config in entry.server.tsx⚠️ Might need - Depends on config

If your project doesn't have CSP enabled, you can skip the CSP configuration steps.

Next.js Pages Router (CSR)

// pages/products/[id].tsx
import { ParcelPanelEDD } from '@cw-parcelpanel/headless-react';

export default function ProductPage({ product }) {
  return (
    <div>
      <h1>{product.title}</h1>
      
      <ParcelPanelEDD
        shopDomain="your-shop.myshopify.com"
        productId={product.id}
        locale="en"
        customer={{ // For automatic user location detection (optional)
          country_code: "US", // From user location or checkout info
          province_code: "CA"
        }}
      />
    </div>
  );
}

React SPA (Vite/CRA)

// src/components/ProductPage.tsx
import { ParcelPanelEDD } from '@cw-parcelpanel/headless-react';

function ProductPage({ product }) {
  return (
    <div>
      <h1>{product.title}</h1>
      
      <ParcelPanelEDD
        shopDomain="your-shop.myshopify.com"
        productId={product.id}
        locale="en"
      />
    </div>
  );
}

Multi-language Support

Components support multiple languages:

// English
<ParcelPanelEDD locale="en" />

// Simplified Chinese
<ParcelPanelEDD locale="zh-CN" />

// Traditional Chinese
<ParcelPanelEDD locale="zh-TW" />

// Japanese
<ParcelPanelEDD locale="ja" />

// Spanish
<ParcelPanelEDD locale="es" />

// French
<ParcelPanelEDD locale="fr" />

Custom Styling

CSS Class Customization

<ParcelPanelEDD
  className="my-custom-edd-widget"
  shopDomain="your-shop.myshopify.com"
  productId={product.id}
  locale="en"
  customer={{ // For automatic user location detection (optional)
    country_code: "US", // From user location or checkout info
    province_code: "CA"
  }}
/>
/* In your CSS file */
.my-custom-edd-widget {
  border: 1px solid #e1e5e9;
  border-radius: 8px;
  padding: 16px;
  margin: 20px 0;
}

Checklist

Hydrogen Storefront Specific Checklist

Before deployment, MUST confirm:

  • Installed @cw-parcelpanel/headless-react
  • Created app/components/ParcelPanelComponents.tsx(with client-only components)
  • Configured vite.config.ts SSR external exclusion(prevent server inclusion)
  • Updated CSP configuration in app/entry.server.tsx(Required step!)
  • Restarted Hydrogen development server for configuration to take effect
  • shopDomain configured correctly (format: your-shop.myshopify.com)
  • Product ID format correct (automatically converts Shopify GID)
  • Tested loading states and error handling
  • Browser console shows no CSP errors

Other Framework Checklist

For non-Hydrogen projects:

  • Installed @cw-parcelpanel/headless-react
  • If using SSR, created wrapper components
  • If project has CSP enabled, configured appropriate policies
  • shopDomain configured correctly
  • Tested component functionality

⚠️ Important Reminder

Hydrogen Storefront users MUST complete CSP configuration, otherwise components won't work!

This is not an optional step, but a security requirement of the Hydrogen framework.

Technical Support

If you encounter issues, please provide the following information:

  1. Project Environment
  2. ParcelPanel Configuration
  3. Error Information

Related Resources

Version Updates

Regularly update to the latest version:

npm update @cw-parcelpanel/headless-react

Version Compatibility

​Current Version​: @cw-parcelpanel/[email protected]+

This guide applies to the latest version of ParcelPanel Headless React components. If you're using an older version, we recommend upgrading to the latest version for the best experience and security.

Version Upgrade

# Check current version
npm list @cw-parcelpanel/headless-react

# Update to latest version
npm update @cw-parcelpanel/headless-react

# Or reinstall latest version
npm install @cw-parcelpanel/headless-react@latest

Compatibility Notes

  • React 16.8+ - Hooks support
  • TypeScript 4.0+ - Full type support
  • Node.js 16+ - Modern Node.js environment
  • Shopify Hydrogen 2024+ - Latest Hydrogen framework
  • Next.js 13+ - App Router and Pages Router
  • Remix 2.0+ - Modern Remix framework

Note​: This guide is continuously updated to keep up with the latest versions. If you have questions, please refer to the Official NPM Page for the latest information.