npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dutchiesdk/ecommerce-extensions-sdk

v0.19.4

Published

![Dutchie Logo](https://cdn.prod.website-files.com/61930755e474060ca6298871/64a32a14b7840d452f594fbc_Logo.svg)

Readme

Dutchie Logo

@dutchiesdk/ecommerce-extensions-sdk

A comprehensive SDK for building e-commerce extensions for cannabis retailers on the Dutchie Ecommerce Pro platform. This SDK provides certified agency partners with type-safe access to platform data, cart management, navigation, and customizable UI components.

⚠️ Alpha Release Warning

This SDK is currently in alpha and is subject to breaking changes. APIs, types, and functionality may change significantly between versions. Please use with caution in production environments and be prepared to update your extensions as the SDK evolves.

Table of Contents

Prerequisites

Development Environment Access

This SDK requires a Dutchie-provided development environment to build, test, and deploy extensions. The SDK alone is not sufficient for development - you must have access to the complete Dutchie Pro development and deployment infrastructure.

To request access to the development environment contact: [email protected]. Please include your agency information and intended use case when requesting access.

Requirements

  • Node.js >= 18.0.0
  • React ^17.0.0 or ^18.0.0
  • react-dom ^17.0.0 or ^18.0.0
  • react-shadow ^20.5.0

Installation

npm install @dutchiesdk/ecommerce-extensions-sdk
# or
yarn add @dutchiesdk/ecommerce-extensions-sdk

Quick Start

import React from "react";
import {
  useDataBridge,
  RemoteBoundaryComponent,
  DataBridgeVersion,
} from "@dutchiesdk/ecommerce-extensions-sdk";

const MyExtension: RemoteBoundaryComponent = () => {
  const { dataLoaders, actions, location, user, cart } = useDataBridge();

  return (
    <div>
      <h1>Welcome to {location?.name}</h1>
      <p>Cart items: {cart?.items.length || 0}</p>
    </div>
  );
};

MyExtension.DataBridgeVersion = DataBridgeVersion;

export default MyExtension;

Core Concepts

The Dutchie Ecommerce Extensions SDK is built around several key concepts:

  1. Data Bridge: A unified interface for accessing dispensary data, user information, and cart state through React Context
  2. Actions: Pre-built navigation and cart management functions that interact with the platform
  3. Remote Components: Extension components that integrate seamlessly with the Dutchie platform using module federation
  4. Data Loaders: Async functions for fetching product catalogs, categories, brands, and other platform data
  5. Type Safety: Comprehensive TypeScript types for all data structures and APIs

API Reference

Hooks

useDataBridge()

The primary hook for accessing the Dutchie platform data and functionality.

Signature:

function useDataBridge(): CommerceComponentsDataInterface;

Returns:

{
  menuContext: 'store-front' | 'kiosk';
  location?: Dispensary;
  user?: User;
  cart?: Cart;
  dataLoaders: DataLoaders;
  actions: Actions;
}

Throws: Error if used outside of a DataBridgeProvider or RemoteBoundaryComponent

Example:

import { useDataBridge } from "@dutchiesdk/ecommerce-extensions-sdk";

const MyComponent = () => {
  const {
    menuContext, // 'store-front' | 'kiosk'
    location, // Current dispensary information
    user, // Authenticated user data
    cart, // Current cart state
    dataLoaders, // Async data loading functions
    actions, // Navigation and cart actions
  } = useDataBridge();

  return <div>{location?.name}</div>;
};

useAsyncLoader(fn, params?)

A utility hook for handling async data loading with loading states.

Signature:

function useAsyncLoader<S, P = void>(
  fn: (params: P) => Promise<S>,
  params?: P
): { data: S | null; isLoading: boolean };

Parameters:

  • fn - An async function that returns a promise (typically a data loader)
  • params - Optional parameters to pass to the function

Returns:

  • data - The loaded data, or null if still loading
  • isLoading - true while data is being fetched, false once complete

Example:

import {
  useAsyncLoader,
  useDataBridge,
} from "@dutchiesdk/ecommerce-extensions-sdk";

const ProductList = () => {
  const { dataLoaders } = useDataBridge();
  const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);

  if (isLoading) return <div>Loading products...</div>;

  return (
    <div>
      {products?.map((product) => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
};

Components & HOCs

RemoteBoundaryComponent<P>

A type that all extension components must satisfy to integrate with the Dutchie platform. This ensures proper data bridge version compatibility and context provision.

Type Signature:

type RemoteBoundaryComponent<P = {}> = React.FC<P> & {
  DataBridgeVersion: string;
};

Properties:

  • Component must be a functional React component
  • Must have a DataBridgeVersion static property matching the SDK version

Example:

import {
  RemoteBoundaryComponent,
  DataBridgeVersion,
  useDataBridge,
} from "@dutchiesdk/ecommerce-extensions-sdk";

const MyCustomHeader: RemoteBoundaryComponent = () => {
  const { location, user, actions } = useDataBridge();

  return (
    <header>
      <h1>{location?.name}</h1>
      {user ? (
        <span>Welcome, {user.firstName}!</span>
      ) : (
        <button onClick={actions.goToLogin}>Login</button>
      )}
    </header>
  );
};

// Required: Set the DataBridgeVersion
MyCustomHeader.DataBridgeVersion = DataBridgeVersion;

export default MyCustomHeader;

withRemoteBoundary(WrappedComponent)

A higher-order component (HOC) that wraps a component with the Data Bridge context provider.

Signature:

function withRemoteBoundary(
  WrappedComponent: ComponentType
): RemoteBoundaryComponent;

Parameters:

  • WrappedComponent - The component to wrap with Data Bridge context

Returns: A RemoteBoundaryComponent with Data Bridge context

Example:

import { withRemoteBoundary } from "@dutchiesdk/ecommerce-extensions-sdk";

const MyComponent = () => {
  return <div>My Component</div>;
};

export default withRemoteBoundary(MyComponent);

createLazyRemoteBoundaryComponent(importFn, options?)

Creates a lazy-loaded remote boundary component with automatic code splitting and error handling.

Signature:

function createLazyRemoteBoundaryComponent<P = WithRemoteBoundaryProps>(
  importFn: () => Promise<{ default: ComponentType }>,
  options?: LazyRemoteBoundaryOptions
): RemoteBoundaryComponent<P>;

Parameters:

  • importFn - A function that returns a dynamic import promise
  • options - Optional configuration object:
    • fallback?: ReactNode - Component to show while loading
    • onError?: (error: Error) => void - Error handler callback

Returns: A lazy-loaded RemoteBoundaryComponent

Example:

import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";

// Basic usage
const LazyHeader = createLazyRemoteBoundaryComponent(
  () => import("./components/Header")
);

// With options
const LazyFooter = createLazyRemoteBoundaryComponent(
  () => import("./components/Footer"),
  {
    fallback: <div>Loading footer...</div>,
    onError: (error) => console.error("Failed to load footer:", error),
  }
);

Context

DataBridgeContext

The React context that provides the Data Bridge interface to all components.

Type:

React.Context<CommerceComponentsDataInterface | undefined>;

Usage:

Typically you'll use the useDataBridge() hook instead of accessing the context directly. However, you can use it for testing or advanced scenarios:

import { DataBridgeContext } from "@dutchiesdk/ecommerce-extensions-sdk";

// Testing example
const mockDataBridge = {
  menuContext: "store-front" as const,
  location: { id: "1", name: "Test Dispensary" },
  dataLoaders: {
    /* ... */
  },
  actions: {
    /* ... */
  },
};

render(
  <DataBridgeContext.Provider value={mockDataBridge}>
    <MyComponent />
  </DataBridgeContext.Provider>
);

DataBridgeVersion

A string constant representing the current SDK version, used for compatibility checking.

Type: string

Usage:

import {
  DataBridgeVersion,
  RemoteBoundaryComponent,
} from "@dutchiesdk/ecommerce-extensions-sdk";

const MyComponent: RemoteBoundaryComponent = () => {
  return <div>Component</div>;
};

MyComponent.DataBridgeVersion = DataBridgeVersion;

Types

The SDK exports comprehensive TypeScript types for all data structures. Here are the key types:

Core Interface Types

import type {
  // Main interface
  CommerceComponentsDataInterface,

  // Component types
  RemoteBoundaryComponent,
  RemoteModuleRegistry,
  ListPageEntry,
  ListPageCategory,

  // Data types
  Actions,
  DataLoaders,
  Cart,
  CartItem,
  User,
  Dispensary,
  Product,
  Brand,
  Category,
  Collection,
  Special,

  // Metadata
  MetaFields,
  StoreFrontMetaFieldsFunction,

  // Events
  Events,
  OnAfterCheckoutData,

  // Context
  MenuContext,
} from "@dutchiesdk/ecommerce-extensions-sdk";

Data Interface

Actions API

The actions object provides pre-built functions for common e-commerce operations. All actions are accessible via the useDataBridge() hook.

Navigation Actions

// Store navigation
actions.goToStoreFront(params?: { query?: Record<string, string> }): void
actions.goToStore(params: { id?: string; cname?: string; query?: Record<string, string> }): void
actions.goToInfoPage(params?: { query?: Record<string, string> }): void

// Browse and search
actions.goToStoreBrowser(params?: { query?: Record<string, string> }): void
actions.goToStoreLocator(params?: { query?: Record<string, string> }): void
actions.goToSearch(query?: string, params?: { query?: Record<string, string> }): void

Example:

const { actions } = useDataBridge();

// Navigate to home page
actions.goToStoreFront();

// Navigate with query params
actions.goToSearch("edibles", { query: { sort: "price-asc" } });

Product & Category Navigation

// Product details
actions.goToProductDetails(params: {
  id?: string;
  cname?: string;
  query?: Record<string, string>;
}): void

// Category pages
actions.goToCategory(params: {
  id?: string;
  cname?: string;
  query?: Record<string, string>;
}): void

// Brand pages
actions.goToBrand(params: {
  id?: string;
  cname?: string;
  query?: Record<string, string>;
}): void

// Collection pages
actions.goToCollection(params: {
  id?: string;
  cname?: string;
  query?: Record<string, string>;
}): void

// Special pages
actions.goToSpecial(params: {
  id: string;
  type: 'offer' | 'sale';
  query?: Record<string, string>;
}): void

Example:

const { actions } = useDataBridge();

// Navigate by ID
actions.goToProductDetails({ id: "product-123" });

// Navigate by cname (URL-friendly name)
actions.goToCategory({ cname: "edibles" });

// With query params
actions.goToBrand({
  cname: "kiva-confections",
  query: { filter: "chocolate" },
});

List Page Actions

// Product list with filters
actions.goToProductList(params: {
  brandId?: string;
  brandCname?: string;
  categoryId?: string;
  categoryCname?: string;
  collectionId?: string;
  collectionCname?: string;
  query?: Record<string, string>;
}): void

// List pages
actions.goToBrandList(params?: { query?: Record<string, string> }): void
actions.goToSpecialsList(params?: { query?: Record<string, string> }): void

Example:

const { actions } = useDataBridge();

// Show products in a category
actions.goToProductList({ categoryCname: "edibles" });

// Show products by brand and category
actions.goToProductList({
  brandCname: "kiva-confections",
  categoryCname: "chocolate",
  query: { sort: "popular" },
});

// Show all brands
actions.goToBrandList();

Cart Actions

// Add items to cart
actions.addToCart(item: CartItem): Promise<void>

// Remove items from cart
actions.removeFromCart(item: CartItem): void

// Update cart item
actions.updateCartItem(existingItem: CartItem, newItem: CartItem): void

// Clear cart
actions.clearCart(): void

// Cart visibility
actions.showCart(): void
actions.hideCart(): void

// Checkout
actions.goToCheckout(): void

// Pricing type
actions.updatePricingType(pricingType: 'med' | 'rec'): void

Example:

const { actions } = useDataBridge();

// Add product to cart
await actions.addToCart({
  productId: "product-123",
  name: "Chocolate Bar",
  option: "10mg",
  price: 25.0,
  quantity: 2,
});

// Update quantity
actions.updateCartItem(existingItem, { ...existingItem, quantity: 3 });

// Show cart sidebar
actions.showCart();

// Proceed to checkout
actions.goToCheckout();

Authentication Actions

// Navigate to login page
actions.goToLogin(): void

// Navigate to registration page
actions.goToRegister(): void

// Navigate to loyalty page (redirects to home if not logged in)
actions.goToLoyalty(params?: { query?: Record<string, string> }): void

Example:

const { user, actions } = useDataBridge();

if (!user) {
  return (
    <div>
      <button onClick={actions.goToLogin}>Login</button>
      <button onClick={actions.goToRegister}>Sign Up</button>
    </div>
  );
}

Data Loaders

Async functions for loading platform data. All loaders return promises and are accessible via useDataBridge().

Available Data Loaders

interface DataLoaders {
  // Product catalog
  products(): Promise<Product[]>;
  product(): Promise<Product | null>; // Only populated on product detail pages

  // Taxonomy
  categories(): Promise<Category[]>;
  brands(): Promise<Brand[]>;
  collections(): Promise<Collection[]>;

  // Promotions
  specials(): Promise<Special[]>;

  // Store locations
  locations(): Promise<Dispensary[]>;

  // Integration data
  integrationValue(key: string): Promise<string | undefined>;
}

Return Values:

  • All list loaders return empty arrays ([]) when no data is available
  • product() returns null when not on a product details page
  • integrationValue() returns undefined when key is not found

Example:

import {
  useDataBridge,
  useAsyncLoader,
} from "@dutchiesdk/ecommerce-extensions-sdk";

const ProductCatalog = () => {
  const { dataLoaders } = useDataBridge();

  // Load products with loading state
  const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);

  // Load categories
  const { data: categories } = useAsyncLoader(dataLoaders.categories);

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>Products ({products?.length || 0})</h1>
      {/* Render products */}
    </div>
  );
};

Direct usage (async/await):

const MyComponent = () => {
  const { dataLoaders } = useDataBridge();
  const [products, setProducts] = useState<Product[]>([]);

  useEffect(() => {
    dataLoaders.products().then(setProducts);
  }, [dataLoaders]);

  return <ProductList products={products} />;
};

Data Types

Cart

type Cart = {
  discount: number; // Total discount amount
  items: CartItem[]; // Array of cart items
  subtotal: number; // Subtotal before tax and discounts
  tax: number; // Tax amount
  total: number; // Final total
};

type CartItem = {
  productId: string; // Unique product identifier
  name: string; // Product name
  price: number; // Price per unit
  quantity: number; // Quantity in cart
  option?: string; // Variant option (e.g., "10mg", "1g")
  additionalOption?: string; // Advanced use only
};

User

type User = {
  email: string;
  firstName: string;
  lastName: string;
  birthday: string; // ISO date string
};

Dispensary

type Dispensary = {
  id: string;
  name: string;
  cname: string; // URL-friendly name
  chain: string; // Chain/brand name
  email: string;
  phone: string;
  status: string; // Operating status
  medDispensary: boolean; // Supports medical
  recDispensary: boolean; // Supports recreational

  address: {
    street1: string;
    street2: string;
    city: string;
    state: string;
    stateAbbreviation: string;
    zip: string;
  };

  images: {
    logo: string; // Logo URL
  };

  links: {
    website: string; // Dispensary website
    storeFrontRoot: string; // Store front base URL
  };

  orderTypes: {
    pickup: boolean;
    delivery: boolean;
    curbsidePickup: boolean;
    inStorePickup: boolean;
    driveThruPickup: boolean;
    kiosk: boolean;
  };

  orderTypesConfig: {
    offerAnyPickupService?: boolean;
    offerDeliveryService?: boolean;
    curbsidePickup?: OrderTypeConfig;
    delivery?: OrderTypeConfig;
    driveThruPickup?: OrderTypeConfig;
    inStorePickup?: OrderTypeConfig;
  };

  hours: {
    curbsidePickup?: HoursSettingsForOrderType;
    delivery?: HoursSettingsForOrderType;
    driveThruPickup?: HoursSettingsForOrderType;
    inStorePickup?: HoursSettingsForOrderType;
  };
};

type OrderTypeConfig = {
  enableAfterHoursOrdering?: boolean;
  enableASAPOrdering?: boolean;
  enableScheduledOrdering?: boolean;
};

type HoursSettingsForOrderType = {
  enabled: boolean;
  effectiveHours?: {
    Monday?: DayHours;
    Tuesday?: DayHours;
    Wednesday?: DayHours;
    Thursday?: DayHours;
    Friday?: DayHours;
    Saturday?: DayHours;
    Sunday?: DayHours;
  };
};

type DayHours = {
  active?: boolean;
  start?: string; // Time string (e.g., "09:00")
  end?: string; // Time string (e.g., "21:00")
};

Product

type Product = {
  id: string;
  cname: string; // URL-friendly name
  name: string;
  description: string;
  image: string; // Primary image URL
  price: number;
};

Brand

type Brand = {
  id: string;
  cname: string; // URL-friendly name
  name: string;
  image?: string | null; // Brand logo URL
};

Category

type Category = {
  id: string;
  cname: string; // URL-friendly name (e.g., "edibles")
  name: string; // Display name (e.g., "Edibles")
};

Collection

type Collection = {
  id: string;
  cname: string; // URL-friendly name
  name: string;
};

Special

type Special = {
  id: string;
  cname: string;
  name: string;
  description: string;
  image: string;
};

Menu Context

type MenuContext = "store-front" | "kiosk";

The menu context indicates which interface the extension is running in:

  • 'store-front' - Online storefront for customers
  • 'kiosk' - In-store kiosk interface

Extension Development

Creating Components

All extension components should satisfy the RemoteBoundaryComponent type and set the DataBridgeVersion property.

Basic Component:

import {
  RemoteBoundaryComponent,
  DataBridgeVersion,
  useDataBridge,
} from "@dutchiesdk/ecommerce-extensions-sdk";

const Header: RemoteBoundaryComponent = () => {
  const { location, user, actions } = useDataBridge();

  return (
    <header>
      <h1>{location?.name}</h1>
      {user && <span>Welcome, {user.firstName}</span>}
    </header>
  );
};

Header.DataBridgeVersion = DataBridgeVersion;

export default Header;

Component with Props:

import type { RemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
import {
  DataBridgeVersion,
  useDataBridge,
} from "@dutchiesdk/ecommerce-extensions-sdk";

interface CustomHeaderProps {
  showLogo?: boolean;
  className?: string;
}

const CustomHeader: RemoteBoundaryComponent<CustomHeaderProps> = ({
  showLogo = true,
  className,
}) => {
  const { location } = useDataBridge();

  return (
    <header className={className}>
      {showLogo && <img src={location?.images.logo} alt={location?.name} />}
      <h1>{location?.name}</h1>
    </header>
  );
};

CustomHeader.DataBridgeVersion = DataBridgeVersion;

export default CustomHeader;

Module Registry

The RemoteModuleRegistry type defines all available extension points in the Dutchie platform.

type RemoteModuleRegistry = {
  // UI Components
  StoreFrontHeader?: ModuleRegistryEntry;
  StoreFrontNavigation?: ModuleRegistryEntry;
  StoreFrontFooter?: ModuleRegistryEntry;
  StoreFrontHero?: ModuleRegistryEntry;
  StoreFrontCarouselInterstitials?: ModuleRegistryEntry[];
  ProductDetailsPrimary?: ModuleRegistryEntry;

  // Category page components (indexed by pagination page number)
  CategoryPageInterstitials?: ListPageEntry[];
  CategoryPageSlots?: ListPageEntry[];

  // Custom routable pages
  RouteablePages?: RoutablePageRegistryEntry[];

  // Metadata function
  getStoreFrontMetaFields?: StoreFrontMetaFieldsFunction;

  // Event handlers
  events?: Events;

  // Deprecated - use getStoreFrontMetaFields instead
  StoreFrontMeta?: RemoteBoundaryComponent;
  ProductDetailsMeta?: RemoteBoundaryComponent;
};

type ModuleRegistryEntry =
  | RemoteBoundaryComponent
  | MenuSpecificRemoteComponent;

type MenuSpecificRemoteComponent = {
  "store-front"?: RemoteBoundaryComponent;
  kiosk?: RemoteBoundaryComponent;
};

type RoutablePageRegistryEntry = {
  path: string;
  component: RemoteBoundaryComponent;
};

type ListPageCategory =
  | "accessories"
  | "apparel"
  | "cbd"
  | "clones"
  | "concentrates"
  | "edibles"
  | "flower"
  | "orals"
  | "pre-rolls"
  | "seeds"
  | "tinctures"
  | "topicals"
  | "vaporizers"
  | string;

type ListPageEntry = {
  category?: ListPageCategory;
  components: RemoteBoundaryComponent[];
};

Basic Registry:

import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";

export default {
  StoreFrontHeader: createLazyRemoteBoundaryComponent(() => import("./Header")),
  StoreFrontFooter: createLazyRemoteBoundaryComponent(() => import("./Footer")),
} satisfies RemoteModuleRegistry;

Menu-Specific Components:

import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";

export default {
  StoreFrontHeader: {
    // Different header for storefront vs kiosk
    "store-front": createLazyRemoteBoundaryComponent(
      () => import("./StoreHeader")
    ),
    kiosk: createLazyRemoteBoundaryComponent(() => import("./KioskHeader")),
  },
  StoreFrontFooter: {
    // Only override storefront, use default Dutchie footer for kiosk
    "store-front": createLazyRemoteBoundaryComponent(() => import("./Footer")),
  },
} satisfies RemoteModuleRegistry;

Custom Routable Pages:

import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";

export default {
  RouteablePages: [
    {
      path: "/about",
      component: createLazyRemoteBoundaryComponent(
        () => import("./pages/About")
      ),
    },
    {
      path: "/contact",
      component: createLazyRemoteBoundaryComponent(
        () => import("./pages/Contact")
      ),
    },
  ],
} satisfies RemoteModuleRegistry;

Category Page Components

The CategoryPageInterstitials and CategoryPageSlots registry entries allow you to inject custom components into category listing pages. Both use the same configuration structure but appear in different locations on the page.

Type:

type ListPageCategory =
  | "accessories"
  | "apparel"
  | "cbd"
  | "clones"
  | "concentrates"
  | "edibles"
  | "flower"
  | "orals"
  | "pre-rolls"
  | "seeds"
  | "tinctures"
  | "topicals"
  | "vaporizers"
  | string; // Custom category names are also supported

type ListPageEntry = {
  category?: ListPageCategory; // Category to match, or undefined for fallback
  components: RemoteBoundaryComponent[]; // Components indexed by page number
};

Category Matching:

  • Predefined category: When category matches a built-in category (e.g., "edibles", "flower"), the components display on that category's listing page
  • Custom category string: When category is a string that doesn't match a predefined category, it matches a custom category with that name
  • Fallback (undefined): When category is omitted, the entry serves as a fallback used when no other category-specific entries match

Page-Based Component Selection:

The components array is indexed by the current pagination page number:

  • components[0] displays on page 1
  • components[1] displays on page 2
  • And so on...

If the current page number exceeds the array length, no component is displayed for that page.

Example:

import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";

export default {
  CategoryPageInterstitials: [
    // Components for the "edibles" category
    {
      category: "edibles",
      components: [
        createLazyRemoteBoundaryComponent(
          () => import("./interstitials/EdiblesPage1")
        ),
        createLazyRemoteBoundaryComponent(
          () => import("./interstitials/EdiblesPage2")
        ),
      ],
    },
    // Components for a custom category
    {
      category: "limited-edition",
      components: [
        createLazyRemoteBoundaryComponent(
          () => import("./interstitials/LimitedEditionPromo")
        ),
      ],
    },
    // Fallback for all other categories
    {
      // No category specified - this is the fallback
      components: [
        createLazyRemoteBoundaryComponent(
          () => import("./interstitials/DefaultPage1")
        ),
        createLazyRemoteBoundaryComponent(
          () => import("./interstitials/DefaultPage2")
        ),
        createLazyRemoteBoundaryComponent(
          () => import("./interstitials/DefaultPage3")
        ),
      ],
    },
  ],

  CategoryPageSlots: [
    // Slots specific to flower category
    {
      category: "flower",
      components: [
        createLazyRemoteBoundaryComponent(
          () => import("./slots/FlowerFeatured")
        ),
      ],
    },
    // Fallback slots for all other categories
    {
      components: [
        createLazyRemoteBoundaryComponent(
          () => import("./slots/GenericPromo")
        ),
      ],
    },
  ],
} satisfies RemoteModuleRegistry;

Meta Fields & SEO

The getStoreFrontMetaFields function allows you to dynamically generate page metadata (title, description, Open Graph tags, structured data) based on the current page and available data.

🚀 Rollout Status: The Dutchie platform is currently rolling out support for getStoreFrontMetaFields via a feature flag. During the transition period:

  • When the flag is enabled: Your getStoreFrontMetaFields function will be used (recommended)
  • When the flag is disabled: The legacy StoreFrontMeta component will be used
  • Both implementations can coexist in your theme during migration
  • The component approach will be deprecated once the rollout is complete

Meta Fields Type

type MetaFields = {
  title?: string; // Page title
  description?: string; // Meta description
  ogImage?: string; // Open Graph image URL
  canonical?: string; // Canonical URL
  structuredData?: Record<string, any>; // JSON-LD structured data
  customMeta?: Array<{
    // Additional meta tags
    name?: string;
    property?: string;
    content: string;
  }>;
};

type StoreFrontMetaFieldsFunction = (
  data: CommerceComponentsDataInterface
) => MetaFields | Promise<MetaFields>;

Implementation

Create a meta fields function (e.g., get-meta-fields.ts):

import type {
  CommerceComponentsDataInterface,
  MetaFields,
} from "@dutchiesdk/ecommerce-extensions-sdk";

export const getStoreFrontMetaFields = async (
  data: CommerceComponentsDataInterface
): Promise<MetaFields> => {
  const { location, dataLoaders } = data;
  const pathname =
    typeof window !== "undefined" ? window.location.pathname : "";

  // Load data for current page
  const product = await dataLoaders.product();
  const categories = await dataLoaders.categories();

  // Product detail page
  if (product) {
    return {
      title: `${product.name} | ${location?.name}`,
      description: product.description.substring(0, 155),
      ogImage: product.image,
      canonical: `${location?.links.storeFrontRoot}${pathname}`,
      structuredData: {
        "@context": "https://schema.org",
        "@type": "Product",
        name: product.name,
        description: product.description,
        image: product.image,
        offers: {
          "@type": "Offer",
          price: product.price,
          priceCurrency: "USD",
        },
      },
    };
  }

  // Category page
  const categorySlug = pathname.split("/").pop();
  const category = categories.find((c) => c.cname === categorySlug);
  if (category) {
    return {
      title: `${category.name} | ${location?.name}`,
      description: `Shop ${category.name} products at ${location?.name}`,
      canonical: `${location?.links.storeFrontRoot}${pathname}`,
    };
  }

  // Home page
  if (pathname === "/" || pathname === "") {
    return {
      title: `${location?.name} - Cannabis Dispensary`,
      description: `Shop cannabis products online at ${location?.name}`,
      ogImage: location?.images.logo,
      canonical: location?.links.storeFrontRoot,
      structuredData: {
        "@context": "https://schema.org",
        "@type": "Organization",
        name: location?.name,
        url: location?.links.storeFrontRoot,
        logo: location?.images.logo,
      },
      customMeta: [
        {
          name: "robots",
          content: "index, follow",
        },
      ],
    };
  }

  // Default fallback
  return {
    title: location?.name || "Shop Cannabis",
    description: `Browse our selection at ${location?.name}`,
  };
};

Register in your module registry:

import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
import { getStoreFrontMetaFields } from "./get-meta-fields";

export default {
  StoreFrontHeader: createLazyRemoteBoundaryComponent(() => import("./Header")),
  StoreFrontFooter: createLazyRemoteBoundaryComponent(() => import("./Footer")),
  getStoreFrontMetaFields,
} satisfies RemoteModuleRegistry;

Event Handlers

Register callbacks that are triggered by platform events.

type Events = {
  onAfterCheckout?: (data: OnAfterCheckoutData) => void;
};

type OnAfterCheckoutData = {
  orderNumber: string;
};

Example:

import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";

export default {
  // ... components

  events: {
    onAfterCheckout: (data) => {
      console.log("Order completed:", data.orderNumber);

      // Track conversion in analytics
      gtag("event", "purchase", {
        transaction_id: data.orderNumber,
      });
    },
  },
} satisfies RemoteModuleRegistry;

Best Practices

Always Handle Loading States

When using data loaders, always handle loading and empty states:

const ProductList = () => {
  const { dataLoaders } = useDataBridge();
  const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);

  if (isLoading) {
    return <LoadingSpinner />;
  }

  if (!products || products.length === 0) {
    return <EmptyState message="No products available" />;
  }

  return (
    <div>
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
};

Check Data Availability

Always check if optional data is available before using it:

const UserProfile = () => {
  const { user, actions } = useDataBridge();

  if (!user) {
    return (
      <div className="login-prompt">
        <p>Please log in to view your profile</p>
        <button onClick={actions.goToLogin}>Login</button>
      </div>
    );
  }

  return (
    <div className="user-profile">
      <h2>Welcome, {user.firstName}!</h2>
      <p>Email: {user.email}</p>
    </div>
  );
};

Use TypeScript for Better DX

Leverage the provided types for autocomplete and type safety:

import type {
  Product,
  Category,
  RemoteBoundaryComponent,
} from "@dutchiesdk/ecommerce-extensions-sdk";

interface ProductFilterProps {
  products: Product[];
  categories: Category[];
  onFilterChange: (categoryId: string) => void;
}

const ProductFilter: React.FC<ProductFilterProps> = ({
  products,
  categories,
  onFilterChange,
}) => {
  return (
    <div>
      {categories.map((category) => (
        <button key={category.id} onClick={() => onFilterChange(category.id)}>
          {category.name}
        </button>
      ))}
    </div>
  );
};

Error Boundaries

Any uncaught errors in your extension will be caught by Dutchie's error boundary. However, you should still handle expected errors gracefully:

const ProductDetails = () => {
  const { dataLoaders } = useDataBridge();
  const [error, setError] = useState<string | null>(null);
  const { data: product, isLoading } = useAsyncLoader(dataLoaders.product);

  if (error) {
    return <ErrorMessage message={error} />;
  }

  if (isLoading) {
    return <LoadingSpinner />;
  }

  if (!product) {
    return <NotFound message="Product not found" />;
  }

  return <ProductCard product={product} />;
};

Optimize Performance

Use lazy loading for large components and routes:

import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";

// Components will be code-split and loaded on demand
export default {
  StoreFrontHeader: createLazyRemoteBoundaryComponent(
    () => import("./components/Header"),
    { fallback: <HeaderSkeleton /> }
  ),
  StoreFrontFooter: createLazyRemoteBoundaryComponent(
    () => import("./components/Footer"),
    { fallback: <FooterSkeleton /> }
  ),
} satisfies RemoteModuleRegistry;

Testing Components

Test your components using the DataBridgeContext provider:

import { render, screen } from "@testing-library/react";
import { DataBridgeContext } from "@dutchiesdk/ecommerce-extensions-sdk";
import MyComponent from "./MyComponent";

const mockDataBridge = {
  menuContext: "store-front" as const,
  location: {
    id: "1",
    name: "Test Dispensary",
    cname: "test-dispensary",
    // ... other required fields
  },
  dataLoaders: {
    products: jest.fn().mockResolvedValue([]),
    categories: jest.fn().mockResolvedValue([]),
    brands: jest.fn().mockResolvedValue([]),
    collections: jest.fn().mockResolvedValue([]),
    specials: jest.fn().mockResolvedValue([]),
    locations: jest.fn().mockResolvedValue([]),
    product: jest.fn().mockResolvedValue(null),
    integrationValue: jest.fn().mockResolvedValue(undefined),
  },
  actions: {
    addToCart: jest.fn(),
    goToProductDetails: jest.fn(),
    goToCategory: jest.fn(),
    // ... other actions
  },
};

test("renders component correctly", () => {
  render(
    <DataBridgeContext.Provider value={mockDataBridge}>
      <MyComponent />
    </DataBridgeContext.Provider>
  );

  expect(screen.getByText("Test Dispensary")).toBeInTheDocument();
});

test("handles cart actions", async () => {
  render(
    <DataBridgeContext.Provider value={mockDataBridge}>
      <MyComponent />
    </DataBridgeContext.Provider>
  );

  const addButton = screen.getByRole("button", { name: /add to cart/i });
  addButton.click();

  expect(mockDataBridge.actions.addToCart).toHaveBeenCalledWith({
    productId: "123",
    name: "Test Product",
    price: 25.0,
    quantity: 1,
  });
});

Examples

Custom Product Listing

import {
  RemoteBoundaryComponent,
  DataBridgeVersion,
  useDataBridge,
  useAsyncLoader,
  type Product,
} from "@dutchiesdk/ecommerce-extensions-sdk";

const ProductListing: RemoteBoundaryComponent = () => {
  const { dataLoaders, actions } = useDataBridge();
  const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
  const { data: categories } = useAsyncLoader(dataLoaders.categories);
  const [filter, setFilter] = useState<string | null>(null);

  const filteredProducts = products?.filter(
    (p) => !filter || p.categoryId === filter
  );

  if (isLoading) {
    return <div className="loading">Loading products...</div>;
  }

  return (
    <div className="product-listing">
      <div className="filters">
        <button onClick={() => setFilter(null)}>All</button>
        {categories?.map((cat) => (
          <button key={cat.id} onClick={() => setFilter(cat.id)}>
            {cat.name}
          </button>
        ))}
      </div>

      <div className="products-grid">
        {filteredProducts?.map((product) => (
          <div
            key={product.id}
            className="product-card"
            onClick={() => actions.goToProductDetails({ id: product.id })}
          >
            <img src={product.image} alt={product.name} />
            <h3>{product.name}</h3>
            <p>${product.price.toFixed(2)}</p>
            <button
              onClick={(e) => {
                e.stopPropagation();
                actions.addToCart({
                  productId: product.id,
                  name: product.name,
                  price: product.price,
                  quantity: 1,
                });
              }}
            >
              Add to Cart
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

ProductListing.DataBridgeVersion = DataBridgeVersion;

export default ProductListing;

Custom Header with Cart

import {
  RemoteBoundaryComponent,
  DataBridgeVersion,
  useDataBridge,
} from "@dutchiesdk/ecommerce-extensions-sdk";

const Header: RemoteBoundaryComponent = () => {
  const { location, user, cart, actions } = useDataBridge();

  const cartItemCount =
    cart?.items.reduce((sum, item) => sum + item.quantity, 0) || 0;

  return (
    <header className="site-header">
      <div className="logo" onClick={() => actions.goToStoreFront()}>
        <img src={location?.images.logo} alt={location?.name} />
        <h1>{location?.name}</h1>
      </div>

      <nav>
        <button onClick={() => actions.goToProductList({})}>Shop All</button>
        <button onClick={() => actions.goToSpecialsList()}>Deals</button>
        <button onClick={() => actions.goToStoreLocator()}>Locations</button>
      </nav>

      <div className="user-actions">
        {user ? (
          <>
            <span>Hello, {user.firstName}</span>
            <button onClick={() => actions.goToLoyalty()}>Rewards</button>
          </>
        ) : (
          <>
            <button onClick={actions.goToLogin}>Login</button>
            <button onClick={actions.goToRegister}>Sign Up</button>
          </>
        )}

        <button className="cart-button" onClick={actions.showCart}>
          Cart ({cartItemCount})
        </button>
      </div>
    </header>
  );
};

Header.DataBridgeVersion = DataBridgeVersion;

export default Header;

Complete Module Registry Example

import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
import { getStoreFrontMetaFields } from "./get-meta-fields";

export default {
  // Header and footer
  StoreFrontHeader: createLazyRemoteBoundaryComponent(
    () => import("./components/Header"),
    { fallback: <div>Loading...</div> }
  ),

  StoreFrontFooter: createLazyRemoteBoundaryComponent(
    () => import("./components/Footer")
  ),

  // Hero section
  StoreFrontHero: createLazyRemoteBoundaryComponent(
    () => import("./components/Hero")
  ),

  // Custom product page
  ProductDetailsPrimary: createLazyRemoteBoundaryComponent(
    () => import("./components/ProductDetails")
  ),

  // Custom routable pages
  RouteablePages: [
    {
      path: "/about",
      component: createLazyRemoteBoundaryComponent(
        () => import("./pages/About")
      ),
    },
    {
      path: "/contact",
      component: createLazyRemoteBoundaryComponent(
        () => import("./pages/Contact")
      ),
    },
  ],

  // Meta fields for SEO
  getStoreFrontMetaFields,

  // Event handlers
  events: {
    onAfterCheckout: (data) => {
      // Track conversion
      console.log("Order completed:", data.orderNumber);

      // Send to analytics
      if (typeof gtag !== "undefined") {
        gtag("event", "purchase", {
          transaction_id: data.orderNumber,
        });
      }
    },
  },
} satisfies RemoteModuleRegistry;

Support

For technical support and questions about the Dutchie Ecommerce Extensions SDK:

  • 📧 Contact your Dutchie agency partner representative
  • 📚 Refer to the Dutchie Pro platform documentation
  • 🐛 Report issues through the official Dutchie support channels

Updates

Stay up to date with this SDK by checking the repository for changes and updating to the latest version with:

npm install @dutchiesdk/ecommerce-extensions-sdk@latest
# or
yarn upgrade @dutchiesdk/ecommerce-extensions-sdk@latest

License

MIT


Made with 💚 by Dutchie