
npm install @dutchiesdk/ecommerce-extensions-sdkA 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.
- Prerequisites
- Installation
- Quick Start
- Core Concepts
- API Reference
- Hooks
- Components & HOCs
- Context
- Types
- Data Interface
- Actions API
- Data Loaders
- Data Types
- Extension Development
- Creating Components
- Module Registry
- Meta Fields & SEO
- Event Handlers
- Best Practices
- Examples
- Support
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: partners@dutchie.com. Please include your agency information and intended use case when requesting access.
- 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
``bash`
npm install @dutchiesdk/ecommerce-extensions-sdkor
yarn add @dutchiesdk/ecommerce-extensions-sdk
`tsx
import React from "react";
import {
useDataBridge,
RemoteBoundaryComponent,
DataBridgeVersion,
} from "@dutchiesdk/ecommerce-extensions-sdk";
const MyExtension: RemoteBoundaryComponent = () => {
const { dataLoaders, actions, location, user, cart } = useDataBridge();
return (
Cart items: {cart?.items.length || 0}
MyExtension.DataBridgeVersion = DataBridgeVersion;
export default MyExtension;
`
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
#### useDataBridge()
The primary hook for accessing the Dutchie platform data and functionality.
Signature:
`typescript`
function useDataBridge(): CommerceComponentsDataInterface;
Returns:
`typescript`
{
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:
`tsx
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
####
useAsyncLoader(fn, params?)A utility hook for handling async data loading with loading states.
Signature:
`typescript
function useAsyncLoader(
fn: (params: P) => Promise,
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 functionReturns:
-
data - The loaded data, or null if still loading
- isLoading - true while data is being fetched, false once completeExample:
`tsx
import {
useAsyncLoader,
useDataBridge,
} from "@dutchiesdk/ecommerce-extensions-sdk";const ProductList = () => {
const { dataLoaders } = useDataBridge();
const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
if (isLoading) return
Loading products...; return (
{products?.map((product) => (
{product.name}
))}
);
};
`$3
####
RemoteBoundaryComponentA 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:
` = React.FC & {typescript`
type RemoteBoundaryComponent
DataBridgeVersion: string;
};
Properties:
- Component must be a functional React component
- Must have a DataBridgeVersion static property matching the SDK version
Example:
`tsx
import {
RemoteBoundaryComponent,
DataBridgeVersion,
useDataBridge,
} from "@dutchiesdk/ecommerce-extensions-sdk";
const MyCustomHeader: RemoteBoundaryComponent = () => {
const { location, user, actions } = useDataBridge();
return (
{location?.name}
{user ? (
Welcome, {user.firstName}!
) : (
)}
);
};
// 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:
`typescript`
function withRemoteBoundary(
WrappedComponent: ComponentType
): RemoteBoundaryComponent;
Parameters:
- WrappedComponent - The component to wrap with Data Bridge context
Returns: A RemoteBoundaryComponent with Data Bridge context
Example:
`tsx
import { withRemoteBoundary } from "@dutchiesdk/ecommerce-extensions-sdk";
const MyComponent = () => {
return
export default withRemoteBoundary(MyComponent);
`
#### createLazyRemoteBoundaryComponent(importFn, options?)
Creates a lazy-loaded remote boundary component with automatic code splitting and error handling.
Signature:
` ( ;typescript`
function createLazyRemoteBoundaryComponent
importFn: () => Promise<{ default: ComponentType }>,
options?: LazyRemoteBoundaryOptions
): RemoteBoundaryComponent
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:
`tsx
import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
// Basic usage
const LazyHeader = createLazyRemoteBoundaryComponent(
() => import("./components/Header")
);
// With options
const LazyFooter = createLazyRemoteBoundaryComponent(
() => import("./components/Footer"),
{
fallback:
$3
####
DataBridgeContextThe React context that provides the Data Bridge interface to all components.
Type:
`typescript
React.Context;
`Usage:
Typically you'll use the
useDataBridge() hook instead of accessing the context directly. However, you can use it for testing or advanced scenarios:`tsx
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(
);
`####
DataBridgeVersionA string constant representing the current SDK version, used for compatibility checking.
Type:
stringUsage:
`tsx
import {
DataBridgeVersion,
RemoteBoundaryComponent,
} from "@dutchiesdk/ecommerce-extensions-sdk";const MyComponent: RemoteBoundaryComponent = () => {
return
Component;
};MyComponent.DataBridgeVersion = DataBridgeVersion;
`$3
The SDK exports comprehensive TypeScript types for all data structures. Here are the key types:
#### Core Interface Types
`typescript
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
$3
The
actions object provides pre-built functions for common e-commerce operations. All actions are accessible via the useDataBridge() hook.#### Navigation Actions
`typescript
// Store navigation
actions.goToStoreFront(params?: { query?: Record }): void
actions.goToStore(params: { id?: string; cname?: string; query?: Record }): void
actions.goToInfoPage(params?: { query?: Record }): void// Browse and search
actions.goToStoreBrowser(params?: { query?: Record }): void
actions.goToStoreLocator(params?: { query?: Record }): void
actions.goToSearch(query?: string, params?: { query?: Record }): void
`Example:
`tsx
const { actions } = useDataBridge();// Navigate to home page
actions.goToStoreFront();
// Navigate with query params
actions.goToSearch("edibles", { query: { sort: "price-asc" } });
`#### Product & Category Navigation
`typescript
// Product details
actions.goToProductDetails(params: {
id?: string;
cname?: string;
query?: Record;
}): void// Category pages
actions.goToCategory(params: {
id?: string;
cname?: string;
query?: Record;
}): void
// Brand pages
actions.goToBrand(params: {
id?: string;
cname?: string;
query?: Record;
}): void
// Collection pages
actions.goToCollection(params: {
id?: string;
cname?: string;
query?: Record;
}): void
// Special pages
actions.goToSpecial(params: {
id: string;
type: 'offer' | 'sale';
query?: Record;
}): void
`Example:
`tsx
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
`typescript
// Product list with filters
actions.goToProductList(params: {
brandId?: string;
brandCname?: string;
categoryId?: string;
categoryCname?: string;
collectionId?: string;
collectionCname?: string;
query?: Record;
}): void// List pages
actions.goToBrandList(params?: { query?: Record }): void
actions.goToSpecialsList(params?: { query?: Record }): void
`Example:
`tsx
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
`typescript
// Add items to cart
actions.addToCart(item: CartItem): Promise// 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:
`tsx
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
`typescript
// 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 }): void
`Example:
`tsx
const { user, actions } = useDataBridge();if (!user) {
return (
);
}
`$3
Async functions for loading platform data. All loaders return promises and are accessible via
useDataBridge().#### Available Data Loaders
`typescript
interface DataLoaders {
// Product catalog
products(): Promise;
product(): Promise; // Only populated on product detail pages // Taxonomy
categories(): Promise;
brands(): Promise;
collections(): Promise;
// Promotions
specials(): Promise;
// Store locations
locations(): Promise;
// Integration data
integrationValue(key: string): Promise;
}
`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 foundExample:
`tsx
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
Loading...; return (
Products ({products?.length || 0})
{/ Render products /}
);
};
`Direct usage (async/await):
`tsx
const MyComponent = () => {
const { dataLoaders } = useDataBridge();
const [products, setProducts] = useState([]); useEffect(() => {
dataLoaders.products().then(setProducts);
}, [dataLoaders]);
return ;
};
`$3
#### Cart
`typescript
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
`typescript
type User = {
email: string;
firstName: string;
lastName: string;
birthday: string; // ISO date string
};
`#### Dispensary
`typescript
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
`typescript
type Product = {
id: string;
cname: string; // URL-friendly name
name: string;
description: string;
image: string; // Primary image URL
price: number;
};
`#### Brand
`typescript
type Brand = {
id: string;
cname: string; // URL-friendly name
name: string;
image?: string | null; // Brand logo URL
};
`#### Category
`typescript
type Category = {
id: string;
cname: string; // URL-friendly name (e.g., "edibles")
name: string; // Display name (e.g., "Edibles")
};
`#### Collection
`typescript
type Collection = {
id: string;
cname: string; // URL-friendly name
name: string;
};
`#### Special
`typescript
type Special = {
id: string;
cname: string;
name: string;
description: string;
image: string;
};
`#### Menu Context
`typescript
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 interfaceExtension Development
$3
All extension components should satisfy the
RemoteBoundaryComponent type and set the DataBridgeVersion property.Basic Component:
`tsx
import {
RemoteBoundaryComponent,
DataBridgeVersion,
useDataBridge,
} from "@dutchiesdk/ecommerce-extensions-sdk";const Header: RemoteBoundaryComponent = () => {
const { location, user, actions } = useDataBridge();
return (
{location?.name}
{user && Welcome, {user.firstName}}
);
};
Header.DataBridgeVersion = DataBridgeVersion;
export default Header;
`Component with Props:
`tsx
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 = ({
showLogo = true,
className,
}) => {
const { location } = useDataBridge();
return (
{showLogo &&
}
{location?.name}
);
};
CustomHeader.DataBridgeVersion = DataBridgeVersion;
export default CustomHeader;
`$3
The
RemoteModuleRegistry type defines all available extension points in the Dutchie platform.`typescript
type RemoteModuleRegistry = {
// UI Components
StoreFrontHeader?: ModuleRegistryEntry;
StoreFrontNavigation?: ModuleRegistryEntry;
StoreFrontFooter?: ModuleRegistryEntry;
StoreFrontHero?: ModuleRegistryEntry;
StoreFrontCarouselInterstitials?: ModuleRegistryEntry[];
ProductDetailsPrimary?: ModuleRegistryEntry;
ListPageHero?: 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:
`tsx
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:
`tsx
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:
`tsx
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:
`typescript
type ListPageCategory =
| "accessories"
| "apparel"
| "cbd"
| "clones"
| "concentrates"
| "edibles"
| "flower"
| "orals"
| "pre-rolls"
| "seeds"
| "tinctures"
| "topicals"
| "vaporizers"
| string; // Custom category names are also supportedtype 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 matchPage-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:
`tsx
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;
`$3
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
`typescript
type MetaFields = {
title?: string; // Page title
description?: string; // Meta description
ogImage?: string; // Open Graph image URL
canonical?: string; // Canonical URL
structuredData?: Record; // JSON-LD structured data
customMeta?: Array<{
// Additional meta tags
name?: string;
property?: string;
content: string;
}>;
};type StoreFrontMetaFieldsFunction = (
data: CommerceComponentsDataInterface
) => MetaFields | Promise;
`#### Implementation
Create a meta fields function (e.g.,
get-meta-fields.ts):`typescript
import type {
CommerceComponentsDataInterface,
MetaFields,
} from "@dutchiesdk/ecommerce-extensions-sdk";export const getStoreFrontMetaFields = async (
data: CommerceComponentsDataInterface
): Promise => {
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:
`typescript
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;
`$3
Register callbacks that are triggered by platform events.
`typescript
type Events = {
onAfterCheckout?: (data: OnAfterCheckoutData) => void;
};type OnAfterCheckoutData = {
orderNumber: string;
};
`Example:
`typescript
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
$3
When using data loaders, always handle loading and empty states:
`tsx
const ProductList = () => {
const { dataLoaders } = useDataBridge();
const { data: products, isLoading } = useAsyncLoader(dataLoaders.products); if (isLoading) {
return ;
}
if (!products || products.length === 0) {
return ;
}
return (
{products.map((product) => (
))}
);
};
`$3
Always check if optional data is available before using it:
`tsx
const UserProfile = () => {
const { user, actions } = useDataBridge(); if (!user) {
return (
Please log in to view your profile
);
} return (
Welcome, {user.firstName}!
Email: {user.email}
);
};
`$3
Leverage the provided types for autocomplete and type safety:
`tsx
import type {
Product,
Category,
RemoteBoundaryComponent,
} from "@dutchiesdk/ecommerce-extensions-sdk";interface ProductFilterProps {
products: Product[];
categories: Category[];
onFilterChange: (categoryId: string) => void;
}
const ProductFilter: React.FC = ({
products,
categories,
onFilterChange,
}) => {
return (
{categories.map((category) => (
))}
);
};
`$3
Any uncaught errors in your extension will be caught by Dutchie's error boundary. However, you should still handle expected errors gracefully:
`tsx
const ProductDetails = () => {
const { dataLoaders } = useDataBridge();
const [error, setError] = useState(null);
const { data: product, isLoading } = useAsyncLoader(dataLoaders.product); if (error) {
return ;
}
if (isLoading) {
return ;
}
if (!product) {
return ;
}
return ;
};
`$3
Use lazy loading for large components and routes:
`tsx
import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";// Components will be code-split and loaded on demand
export default {
StoreFrontHeader: createLazyRemoteBoundaryComponent(
() => import("./components/Header"),
{ fallback: }
),
StoreFrontFooter: createLazyRemoteBoundaryComponent(
() => import("./components/Footer"),
{ fallback: }
),
} satisfies RemoteModuleRegistry;
`$3
Test your components using the
DataBridgeContext provider:`tsx
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(
);
expect(screen.getByText("Test Dispensary")).toBeInTheDocument();
});
test("handles cart actions", async () => {
render(
);
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
$3
`tsx
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(null);
const filteredProducts = products?.filter(
(p) => !filter || p.categoryId === filter
);
if (isLoading) {
return
Loading products...;
} return (
{categories?.map((cat) => (
))}
{filteredProducts?.map((product) => (
key={product.id}
className="product-card"
onClick={() => actions.goToProductDetails({ id: product.id })}
>

{product.name}
${product.price.toFixed(2)}
onClick={(e) => {
e.stopPropagation();
actions.addToCart({
productId: product.id,
name: product.name,
price: product.price,
quantity: 1,
});
}}
>
Add to Cart
))}
ProductListing.DataBridgeVersion = DataBridgeVersion;
export default ProductListing;
`
`tsx
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 (
actions.goToStoreFront()}>
{location?.name}
Header.DataBridgeVersion = DataBridgeVersion;
export default Header;
`
`tsx
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:
StoreFrontFooter: createLazyRemoteBoundaryComponent(
() => import("./components/Footer")
),
// Hero section
StoreFrontHero: createLazyRemoteBoundaryComponent(
() => import("./components/Hero")
),
// Hero on Product List pages
ListPageHero: createLazyRemoteBoundaryComponent(
() => import("./store-front/list-page-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;
`
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
Stay up to date with this SDK by checking the repository for changes and updating to the latest version with:
`bash``
npm install @dutchiesdk/ecommerce-extensions-sdk@latestor
yarn upgrade @dutchiesdk/ecommerce-extensions-sdk@latest
MIT
---
Made with 💚 by Dutchie