@dutchiesdk/ecommerce-extensions-sdk
v0.19.4
Published

Readme
@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
- Installation
- Quick Start
- Core Concepts
- API Reference
- Data Interface
- Extension Development
- Best Practices
- Examples
- Support
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-sdkQuick 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:
- Data Bridge: A unified interface for accessing dispensary data, user information, and cart state through React Context
- Actions: Pre-built navigation and cart management functions that interact with the platform
- Remote Components: Extension components that integrate seamlessly with the Dutchie platform using module federation
- Data Loaders: Async functions for fetching product catalogs, categories, brands, and other platform data
- 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, ornullif still loadingisLoading-truewhile data is being fetched,falseonce 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
DataBridgeVersionstatic 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 promiseoptions- Optional configuration object:fallback?: ReactNode- Component to show while loadingonError?: (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> }): voidExample:
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>;
}): voidExample:
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> }): voidExample:
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'): voidExample:
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> }): voidExample:
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()returnsnullwhen not on a product details pageintegrationValue()returnsundefinedwhen 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
categorymatches a built-in category (e.g.,"edibles","flower"), the components display on that category's listing page - Custom category string: When
categoryis a string that doesn't match a predefined category, it matches a custom category with that name - Fallback (undefined): When
categoryis 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 1components[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
getStoreFrontMetaFieldsvia a feature flag. During the transition period:
- When the flag is enabled: Your
getStoreFrontMetaFieldsfunction will be used (recommended)- When the flag is disabled: The legacy
StoreFrontMetacomponent 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@latestLicense
MIT
Made with 💚 by Dutchie
