@dutchiesdk/ecommerce-extensions-sdk 0.18.0 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,31 +2,68 @@
2
2
 
3
3
  # @dutchiesdk/ecommerce-extensions-sdk
4
4
 
5
- This SDK provides certified agency partners with the tools needed to create powerful, integrated e-commerce experiences for cannabis retailers on the Dutchie Ecommerce Pro platform.
5
+ 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.
6
6
 
7
7
  > ⚠️ **Alpha Release Warning**
8
8
  >
9
9
  > 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.
10
10
 
11
- ## Development Environment Access
11
+ ## Table of Contents
12
+
13
+ - [Prerequisites](#prerequisites)
14
+ - [Installation](#installation)
15
+ - [Quick Start](#quick-start)
16
+ - [Core Concepts](#core-concepts)
17
+ - [API Reference](#api-reference)
18
+ - [Hooks](#hooks)
19
+ - [Components & HOCs](#components--hocs)
20
+ - [Context](#context)
21
+ - [Types](#types)
22
+ - [Data Interface](#data-interface)
23
+ - [Actions API](#actions-api)
24
+ - [Data Loaders](#data-loaders)
25
+ - [Data Types](#data-types)
26
+ - [Extension Development](#extension-development)
27
+ - [Creating Components](#creating-components)
28
+ - [Module Registry](#module-registry)
29
+ - [Meta Fields & SEO](#meta-fields--seo)
30
+ - [Event Handlers](#event-handlers)
31
+ - [Best Practices](#best-practices)
32
+ - [Examples](#examples)
33
+ - [Support](#support)
34
+
35
+ ## Prerequisites
36
+
37
+ ### Development Environment Access
12
38
 
13
39
  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.
14
40
 
15
41
  To request access to the development environment contact: **partners@dutchie.com**. Please include your agency information and intended use case when requesting access.
16
42
 
43
+ ### Requirements
44
+
45
+ - Node.js >= 18.0.0
46
+ - React ^17.0.0 or ^18.0.0
47
+ - react-dom ^17.0.0 or ^18.0.0
48
+ - react-shadow ^20.5.0
49
+
17
50
  ## Installation
18
51
 
19
52
  ```bash
20
- npm install @dutchie/ecommerce-extensions-sdk
53
+ npm install @dutchiesdk/ecommerce-extensions-sdk
21
54
  # or
22
- yarn add @dutchie/ecommerce-extensions-sdk
55
+ yarn add @dutchiesdk/ecommerce-extensions-sdk
23
56
  ```
24
57
 
25
58
  ## Quick Start
26
59
 
27
60
  ```tsx
28
61
  import React from "react";
29
- import { useDataBridge, RemoteBoundaryComponent, DataBridgeVersion } from "@dutchie/ecommerce-extensions-sdk";
62
+ import {
63
+ useDataBridge,
64
+ RemoteBoundaryComponent,
65
+ DataBridgeVersion,
66
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
30
67
 
31
68
  const MyExtension: RemoteBoundaryComponent = () => {
32
69
  const { dataLoaders, actions, location, user, cart } = useDataBridge();
@@ -44,14 +81,49 @@ MyExtension.DataBridgeVersion = DataBridgeVersion;
44
81
  export default MyExtension;
45
82
  ```
46
83
 
47
- ## React Context & Hooks
84
+ ## Core Concepts
85
+
86
+ The Dutchie Ecommerce Extensions SDK is built around several key concepts:
87
+
88
+ 1. **Data Bridge**: A unified interface for accessing dispensary data, user information, and cart state through React Context
89
+ 2. **Actions**: Pre-built navigation and cart management functions that interact with the platform
90
+ 3. **Remote Components**: Extension components that integrate seamlessly with the Dutchie platform using module federation
91
+ 4. **Data Loaders**: Async functions for fetching product catalogs, categories, brands, and other platform data
92
+ 5. **Type Safety**: Comprehensive TypeScript types for all data structures and APIs
93
+
94
+ ## API Reference
95
+
96
+ ### Hooks
48
97
 
49
- ### `useDataBridge()`
98
+ #### `useDataBridge()`
50
99
 
51
100
  The primary hook for accessing the Dutchie platform data and functionality.
52
101
 
102
+ **Signature:**
103
+
104
+ ```typescript
105
+ function useDataBridge(): CommerceComponentsDataInterface;
106
+ ```
107
+
108
+ **Returns:**
109
+
110
+ ```typescript
111
+ {
112
+ menuContext: 'store-front' | 'kiosk';
113
+ location?: Dispensary;
114
+ user?: User;
115
+ cart?: Cart;
116
+ dataLoaders: DataLoaders;
117
+ actions: Actions;
118
+ }
119
+ ```
120
+
121
+ **Throws:** Error if used outside of a `DataBridgeProvider` or `RemoteBoundaryComponent`
122
+
123
+ **Example:**
124
+
53
125
  ```tsx
54
- import { useDataBridge } from "@dutchie/ecommerce-extensions-sdk";
126
+ import { useDataBridge } from "@dutchiesdk/ecommerce-extensions-sdk";
55
127
 
56
128
  const MyComponent = () => {
57
129
  const {
@@ -63,18 +135,40 @@ const MyComponent = () => {
63
135
  actions, // Navigation and cart actions
64
136
  } = useDataBridge();
65
137
 
66
- // Component logic here
138
+ return <div>{location?.name}</div>;
67
139
  };
68
140
  ```
69
141
 
70
- **Throws**: Error if used outside of a `DataBridgeProvider`
71
-
72
- ### `useAsyncLoader(fn)`
142
+ #### `useAsyncLoader(fn, params?)`
73
143
 
74
144
  A utility hook for handling async data loading with loading states.
75
145
 
146
+ **Signature:**
147
+
148
+ ```typescript
149
+ function useAsyncLoader<S, P = void>(
150
+ fn: (params: P) => Promise<S>,
151
+ params?: P
152
+ ): { data: S | null; isLoading: boolean };
153
+ ```
154
+
155
+ **Parameters:**
156
+
157
+ - `fn` - An async function that returns a promise (typically a data loader)
158
+ - `params` - Optional parameters to pass to the function
159
+
160
+ **Returns:**
161
+
162
+ - `data` - The loaded data, or `null` if still loading
163
+ - `isLoading` - `true` while data is being fetched, `false` once complete
164
+
165
+ **Example:**
166
+
76
167
  ```tsx
77
- import { useAsyncLoader, useDataBridge } from "@dutchie/ecommerce-extensions-sdk";
168
+ import {
169
+ useAsyncLoader,
170
+ useDataBridge,
171
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
78
172
 
79
173
  const ProductList = () => {
80
174
  const { dataLoaders } = useDataBridge();
@@ -92,33 +186,33 @@ const ProductList = () => {
92
186
  };
93
187
  ```
94
188
 
95
- ## Data Types
189
+ ### Components & HOCs
190
+
191
+ #### `RemoteBoundaryComponent<P>`
96
192
 
97
- ### `CommerceComponentsDataInterface`
193
+ A type that all extension components must satisfy to integrate with the Dutchie platform. This ensures proper data bridge version compatibility and context provision.
98
194
 
99
- The main interface providing access to all platform data and functionality. [Full interface can be found here](./src/types/interface.ts).
195
+ **Type Signature:**
100
196
 
101
197
  ```typescript
102
- interface CommerceComponentsDataInterface {
103
- menuContext: "store-front" | "kiosk";
104
- location?: Dispensary;
105
- user?: User;
106
- cart?: Cart;
107
- dataLoaders: DataLoaders;
108
- actions: Actions;
109
- }
198
+ type RemoteBoundaryComponent<P = {}> = React.FC<P> & {
199
+ DataBridgeVersion: string;
200
+ };
110
201
  ```
111
202
 
112
- ## Extension Components
113
-
114
- ### `RemoteBoundaryComponent`
203
+ **Properties:**
115
204
 
116
- All extension components that integrate with the Dutchie platform should satisfy the `RemoteBoundaryComponent` type.
205
+ - Component must be a functional React component
206
+ - Must have a `DataBridgeVersion` static property matching the SDK version
117
207
 
118
208
  **Example:**
119
209
 
120
210
  ```tsx
121
- import { RemoteBoundaryComponent, useDataBridge, DataBridgeVersion } from "@dutchie/ecommerce-extensions-sdk";
211
+ import {
212
+ RemoteBoundaryComponent,
213
+ DataBridgeVersion,
214
+ useDataBridge,
215
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
122
216
 
123
217
  const MyCustomHeader: RemoteBoundaryComponent = () => {
124
218
  const { location, user, actions } = useDataBridge();
@@ -126,399 +220,1402 @@ const MyCustomHeader: RemoteBoundaryComponent = () => {
126
220
  return (
127
221
  <header>
128
222
  <h1>{location?.name}</h1>
129
- {user ? <span>Welcome, {user.firstName}!</span> : <button onClick={actions.goToLogin}>Login</button>}
223
+ {user ? (
224
+ <span>Welcome, {user.firstName}!</span>
225
+ ) : (
226
+ <button onClick={actions.goToLogin}>Login</button>
227
+ )}
130
228
  </header>
131
229
  );
132
230
  };
133
231
 
232
+ // Required: Set the DataBridgeVersion
134
233
  MyCustomHeader.DataBridgeVersion = DataBridgeVersion;
234
+
235
+ export default MyCustomHeader;
135
236
  ```
136
237
 
137
- ## Actions API
238
+ #### `withRemoteBoundary(WrappedComponent)`
138
239
 
139
- The actions object provides pre-built functions for common e-commerce operations.
240
+ A higher-order component (HOC) that wraps a component with the Data Bridge context provider.
140
241
 
141
- It can be used to route:
242
+ **Signature:**
142
243
 
143
244
  ```typescript
144
- const { actions } = useDataBridge();
245
+ function withRemoteBoundary(
246
+ WrappedComponent: ComponentType
247
+ ): RemoteBoundaryComponent;
248
+ ```
145
249
 
146
- actions.goToLogin(); // Navigate to login page
250
+ **Parameters:**
147
251
 
148
- // Product & category navigation
149
- actions.goToProductDetails("product-123"); // Navigate to product details
150
- ```
252
+ - `WrappedComponent` - The component to wrap with Data Bridge context
151
253
 
152
- Or to update state:
254
+ **Returns:** A `RemoteBoundaryComponent` with Data Bridge context
153
255
 
154
- ```typescript
155
- const { actions } = useDataBridge();
256
+ **Example:**
257
+
258
+ ```tsx
259
+ import { withRemoteBoundary } from "@dutchiesdk/ecommerce-extensions-sdk";
156
260
 
157
- actions.addToCart("product-123", 2); // Add 2 items to cart
158
- actions.clearCart(); // Remove all items from cart
261
+ const MyComponent = () => {
262
+ return <div>My Component</div>;
263
+ };
264
+
265
+ export default withRemoteBoundary(MyComponent);
159
266
  ```
160
267
 
161
- ## Data Loaders
268
+ #### `createLazyRemoteBoundaryComponent(importFn, options?)`
162
269
 
163
- Async functions for loading platform data. All loaders return promises that resolve to arrays.
270
+ Creates a lazy-loaded remote boundary component with automatic code splitting and error handling.
164
271
 
165
- ```typescript
166
- const { dataLoaders } = useDataBridge();
272
+ **Signature:**
167
273
 
168
- // Location data
169
- const locations = await dataLoaders.getAllLocations(); // All dispensary locations
170
- const currentLocations = await dataLoaders.locations(); // Current context locations
274
+ ```typescript
275
+ function createLazyRemoteBoundaryComponent<P = WithRemoteBoundaryProps>(
276
+ importFn: () => Promise<{ default: ComponentType }>,
277
+ options?: LazyRemoteBoundaryOptions
278
+ ): RemoteBoundaryComponent<P>;
171
279
  ```
172
280
 
173
- **Note:** All data loaders return empty arrays (`[]`) when no data is available.
281
+ **Parameters:**
282
+
283
+ - `importFn` - A function that returns a dynamic import promise
284
+ - `options` - Optional configuration object:
285
+ - `fallback?: ReactNode` - Component to show while loading
286
+ - `onError?: (error: Error) => void` - Error handler callback
287
+
288
+ **Returns:** A lazy-loaded `RemoteBoundaryComponent`
289
+
290
+ **Example:**
174
291
 
175
- ## Events
292
+ ```tsx
293
+ import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
294
+
295
+ // Basic usage
296
+ const LazyHeader = createLazyRemoteBoundaryComponent(
297
+ () => import("./components/Header")
298
+ );
299
+
300
+ // With options
301
+ const LazyFooter = createLazyRemoteBoundaryComponent(
302
+ () => import("./components/Footer"),
303
+ {
304
+ fallback: <div>Loading footer...</div>,
305
+ onError: (error) => console.error("Failed to load footer:", error),
306
+ }
307
+ );
308
+ ```
176
309
 
177
- You can register callback functions that will be triggered by certain events in the Dutchie platform in your extension's `RemoteModuleRegistry` object.
310
+ ### Context
178
311
 
179
- ## Meta Fields Function
312
+ #### `DataBridgeContext`
180
313
 
181
- ### Overview
314
+ The React context that provides the Data Bridge interface to all components.
182
315
 
183
- The `getStoreFrontMetaFields` function is the **recommended approach** for generating page metadata (title tags, meta descriptions, Open Graph tags, etc.) in your theme. This function-based approach replaces the `StoreFrontMeta` component pattern.
316
+ **Type:**
184
317
 
185
- **Why use this approach:**
318
+ ```typescript
319
+ React.Context<CommerceComponentsDataInterface | undefined>;
320
+ ```
186
321
 
187
- - ✅ **Full data access** - Direct access to data loaders (products, categories, etc.)
188
- - ✅ **Async support** - Make API calls to fetch data for dynamic meta tags
189
- - ✅ **Centralized control** - Single function handles all page metadata logic
190
- - ✅ **Better performance** - Called once per navigation, not re-rendered on state changes
322
+ **Usage:**
191
323
 
192
- > **🚀 Rollout Status:** The Dutchie platform is currently rolling out support for `getStoreFrontMetaFields` via a feature flag. During the transition period:
193
- >
194
- > - When the flag is **enabled**: Your `getStoreFrontMetaFields` function will be used (recommended)
195
- > - When the flag is **disabled**: The legacy `StoreFrontMeta` component will be used
196
- > - Both implementations can coexist in your theme during migration
197
- > - The component approach will be deprecated once the rollout is complete
324
+ Typically you'll use the `useDataBridge()` hook instead of accessing the context directly. However, you can use it for testing or advanced scenarios:
198
325
 
199
- > **Note:** When you provide `getStoreFrontMetaFields` and the feature flag is enabled, the `StoreFrontMeta` component will not be used. We recommend implementing the function approach now to prepare for the full migration
326
+ ```tsx
327
+ import { DataBridgeContext } from "@dutchiesdk/ecommerce-extensions-sdk";
200
328
 
201
- ### Function Signature
329
+ // Testing example
330
+ const mockDataBridge = {
331
+ menuContext: "store-front" as const,
332
+ location: { id: "1", name: "Test Dispensary" },
333
+ dataLoaders: {
334
+ /* ... */
335
+ },
336
+ actions: {
337
+ /* ... */
338
+ },
339
+ };
202
340
 
203
- ```typescript
204
- export type StoreFrontMetaFieldsFunction = (data: CommerceComponentsDataInterface) => MetaFields | Promise<MetaFields>;
341
+ render(
342
+ <DataBridgeContext.Provider value={mockDataBridge}>
343
+ <MyComponent />
344
+ </DataBridgeContext.Provider>
345
+ );
205
346
  ```
206
347
 
207
- The function receives the full data bridge interface and can return meta fields synchronously or asynchronously.
348
+ #### `DataBridgeVersion`
208
349
 
209
- ### Meta Fields Type
350
+ A string constant representing the current SDK version, used for compatibility checking.
210
351
 
211
- ```typescript
212
- type MetaFields = {
213
- title?: string; // Page title
214
- description?: string; // Meta description
215
- ogImage?: string; // Open Graph image URL
216
- canonical?: string; // Canonical URL
217
- structuredData?: Record<string, any>; // JSON-LD structured data
218
- customMeta?: Array<{
219
- // Additional meta tags
220
- name?: string;
221
- property?: string;
222
- content: string;
223
- }>;
352
+ **Type:** `string`
353
+
354
+ **Usage:**
355
+
356
+ ```tsx
357
+ import {
358
+ DataBridgeVersion,
359
+ RemoteBoundaryComponent,
360
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
361
+
362
+ const MyComponent: RemoteBoundaryComponent = () => {
363
+ return <div>Component</div>;
224
364
  };
365
+
366
+ MyComponent.DataBridgeVersion = DataBridgeVersion;
225
367
  ```
226
368
 
227
- ### Implementation
369
+ ### Types
228
370
 
229
- Create a file in your theme (e.g., `get-meta-fields.ts`):
371
+ The SDK exports comprehensive TypeScript types for all data structures. Here are the key types:
372
+
373
+ #### Core Interface Types
230
374
 
231
375
  ```typescript
232
- import { CommerceComponentsDataInterface, MetaFields } from "@dutchiesdk/ecommerce-extensions-sdk";
376
+ import type {
377
+ // Main interface
378
+ CommerceComponentsDataInterface,
379
+
380
+ // Component types
381
+ RemoteBoundaryComponent,
382
+ RemoteModuleRegistry,
383
+
384
+ // Data types
385
+ Actions,
386
+ DataLoaders,
387
+ Cart,
388
+ CartItem,
389
+ User,
390
+ Dispensary,
391
+ Product,
392
+ Brand,
393
+ Category,
394
+ Collection,
395
+ Special,
396
+
397
+ // Metadata
398
+ MetaFields,
399
+ StoreFrontMetaFieldsFunction,
400
+
401
+ // Events
402
+ Events,
403
+ OnAfterCheckoutData,
404
+
405
+ // Context
406
+ MenuContext,
407
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
408
+ ```
233
409
 
234
- export const getStoreFrontMetaFields = async (data: CommerceComponentsDataInterface): Promise<MetaFields> => {
235
- const { location, dataLoaders } = data;
236
- const page = typeof window !== "undefined" ? window.location.pathname : "";
410
+ ## Data Interface
237
411
 
238
- // Access data loaders for dynamic content
239
- const product = await dataLoaders.product();
240
- const categories = await dataLoaders.categories();
412
+ ### Actions API
241
413
 
242
- // Generate page-specific metadata
243
- let pageTitle = location?.name || "Shop Cannabis";
244
- let pageDescription = `Browse our selection at ${location?.name}`;
414
+ The `actions` object provides pre-built functions for common e-commerce operations. All actions are accessible via the `useDataBridge()` hook.
245
415
 
246
- if (product) {
247
- pageTitle = `${product.name} | ${location?.name}`;
248
- pageDescription = `${product.description?.substring(0, 155)}...`;
249
- }
416
+ #### Navigation Actions
250
417
 
251
- return {
252
- title: pageTitle,
253
- description: pageDescription,
254
- ogImage: location?.images?.logo,
255
- canonical: `${location?.links?.storeFrontRoot}${page}`,
256
- structuredData: {
257
- "@context": "https://schema.org",
258
- "@type": "WebPage",
259
- name: pageTitle,
260
- description: pageDescription,
261
- },
262
- customMeta: [
263
- {
264
- name: "robots",
265
- content: "index, follow",
266
- },
267
- ],
268
- };
269
- };
418
+ ```typescript
419
+ // Store navigation
420
+ actions.goToStoreFront(params?: { query?: Record<string, string> }): void
421
+ actions.goToStore(params: { id?: string; cname?: string; query?: Record<string, string> }): void
422
+ actions.goToInfoPage(params?: { query?: Record<string, string> }): void
423
+
424
+ // Browse and search
425
+ actions.goToStoreBrowser(params?: { query?: Record<string, string> }): void
426
+ actions.goToStoreLocator(params?: { query?: Record<string, string> }): void
427
+ actions.goToSearch(query?: string, params?: { query?: Record<string, string> }): void
270
428
  ```
271
429
 
272
- ### Register in Module Registry
430
+ **Example:**
273
431
 
274
- Add the function to your theme's `index.tsx`:
432
+ ```tsx
433
+ const { actions } = useDataBridge();
275
434
 
276
- ```typescript
277
- import { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
278
- import { getStoreFrontMetaFields } from "./get-meta-fields";
435
+ // Navigate to home page
436
+ actions.goToStoreFront();
279
437
 
280
- export default {
281
- StoreFrontHeader: createLazyRemoteBoundaryComponent(() => import("./header")),
282
- StoreFrontFooter: createLazyRemoteBoundaryComponent(() => import("./footer")),
283
- getStoreFrontMetaFields, // Register the function
284
- } satisfies RemoteModuleRegistry;
438
+ // Navigate with query params
439
+ actions.goToSearch("edibles", { query: { sort: "price-asc" } });
285
440
  ```
286
441
 
287
- You may also register components for only specific menu contexts:
442
+ #### Product & Category Navigation
288
443
 
289
444
  ```typescript
290
- import { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
291
-
292
- export default {
293
- StoreFrontHeader: {
294
- // use one component for storefront and a different one for kiosk
295
- 'store-front': createLazyRemoteBoundaryComponent(() => import("./store-header")),
296
- kiosk: createLazyRemoteBoundaryComponent(() => import("./kiosk-header")),
297
- },
298
- StoreFrontFooter: {
299
- // only override storefront but not kiosk, kiosk will have default dutchie footer
300
- 'store-front': createLazyRemoteBoundaryComponent(() => import("./footer")),
301
- },
302
- } satisfies RemoteModuleRegistry;
445
+ // Product details
446
+ actions.goToProductDetails(params: {
447
+ id?: string;
448
+ cname?: string;
449
+ query?: Record<string, string>;
450
+ }): void
451
+
452
+ // Category pages
453
+ actions.goToCategory(params: {
454
+ id?: string;
455
+ cname?: string;
456
+ query?: Record<string, string>;
457
+ }): void
458
+
459
+ // Brand pages
460
+ actions.goToBrand(params: {
461
+ id?: string;
462
+ cname?: string;
463
+ query?: Record<string, string>;
464
+ }): void
465
+
466
+ // Collection pages
467
+ actions.goToCollection(params: {
468
+ id?: string;
469
+ cname?: string;
470
+ query?: Record<string, string>;
471
+ }): void
472
+
473
+ // Special pages
474
+ actions.goToSpecial(params: {
475
+ id: string;
476
+ type: 'offer' | 'sale';
477
+ query?: Record<string, string>;
478
+ }): void
303
479
  ```
304
480
 
305
- ### Available Data
481
+ **Example:**
306
482
 
307
- The function receives the full `CommerceComponentsDataInterface`:
483
+ ```tsx
484
+ const { actions } = useDataBridge();
308
485
 
309
- ```typescript
310
- {
311
- menuContext: 'store-front' | 'kiosk',
312
- location?: Dispensary,
313
- user?: User,
314
- cart?: Cart,
315
- dataLoaders: {
316
- product: () => Promise<Product | null>,
317
- products: () => Promise<Product[]>,
318
- categories: () => Promise<Category[]>,
319
- // ... other loaders
320
- },
321
- actions: {
322
- // Navigation and cart actions
323
- }
324
- }
325
- ```
486
+ // Navigate by ID
487
+ actions.goToProductDetails({ id: "product-123" });
326
488
 
327
- ### Example Use Cases
489
+ // Navigate by cname (URL-friendly name)
490
+ actions.goToCategory({ cname: "edibles" });
491
+
492
+ // With query params
493
+ actions.goToBrand({
494
+ cname: "kiva-confections",
495
+ query: { filter: "chocolate" },
496
+ });
497
+ ```
328
498
 
329
- **Product Pages:**
499
+ #### List Page Actions
330
500
 
331
501
  ```typescript
332
- const product = await dataLoaders.product();
333
- if (product) {
334
- return {
335
- title: `${product.name} - ${location?.name}`,
336
- description: product.description,
337
- ogImage: product.images?.[0]?.url,
338
- structuredData: {
339
- "@context": "https://schema.org",
340
- "@type": "Product",
341
- name: product.name,
342
- description: product.description,
343
- offers: {
344
- "@type": "Offer",
345
- price: product.variants?.[0]?.priceRec,
346
- priceCurrency: "USD",
347
- },
348
- },
349
- };
350
- }
502
+ // Product list with filters
503
+ actions.goToProductList(params: {
504
+ brandId?: string;
505
+ brandCname?: string;
506
+ categoryId?: string;
507
+ categoryCname?: string;
508
+ collectionId?: string;
509
+ collectionCname?: string;
510
+ query?: Record<string, string>;
511
+ }): void
512
+
513
+ // List pages
514
+ actions.goToBrandList(params?: { query?: Record<string, string> }): void
515
+ actions.goToSpecialsList(params?: { query?: Record<string, string> }): void
351
516
  ```
352
517
 
353
- **Category Pages:**
518
+ **Example:**
354
519
 
355
- ```typescript
356
- const categories = await dataLoaders.categories();
357
- const pathname = window.location.pathname;
358
- const categorySlug = pathname.split("/").pop();
359
- const category = categories.find((c) => c.slug === categorySlug);
520
+ ```tsx
521
+ const { actions } = useDataBridge();
360
522
 
361
- if (category) {
362
- return {
363
- title: `${category.name} | ${location?.name}`,
364
- description: `Shop ${category.name} products at ${location?.name}`,
365
- };
366
- }
367
- ```
523
+ // Show products in a category
524
+ actions.goToProductList({ categoryCname: "edibles" });
368
525
 
369
- **Home Page:**
526
+ // Show products by brand and category
527
+ actions.goToProductList({
528
+ brandCname: "kiva-confections",
529
+ categoryCname: "chocolate",
530
+ query: { sort: "popular" },
531
+ });
370
532
 
371
- ```typescript
372
- if (pathname === "/" || pathname === "") {
373
- return {
374
- title: `${location?.name} - Cannabis Dispensary`,
375
- description: `Shop cannabis products online at ${location?.name}`,
376
- structuredData: {
377
- "@context": "https://schema.org",
378
- "@type": "Organization",
379
- name: location?.name,
380
- url: location?.links?.storeFrontRoot,
381
- logo: location?.images?.logo,
382
- },
383
- };
384
- }
533
+ // Show all brands
534
+ actions.goToBrandList();
385
535
  ```
386
536
 
387
- ### Notes
537
+ #### Cart Actions
388
538
 
389
- - The function is called on every page navigation
390
- - Async operations (like `dataLoaders.product()`) are supported
391
- - Return values are rendered into `<head>` tags automatically
392
- - The title tag will update dynamically as meta fields load
393
- - All fields are optional - return only what's needed for each page
539
+ ```typescript
540
+ // Add items to cart
541
+ actions.addToCart(item: CartItem): Promise<void>
394
542
 
395
- ### Migration Strategy
543
+ // Remove items from cart
544
+ actions.removeFromCart(item: CartItem): void
396
545
 
397
- If you have an existing theme using the `StoreFrontMeta` component:
546
+ // Update cart item
547
+ actions.updateCartItem(existingItem: CartItem, newItem: CartItem): void
398
548
 
399
- 1. **Implement the function**: Add `getStoreFrontMetaFields` to your theme's module registry
400
- 2. **Test both approaches**: Keep your existing `StoreFrontMeta` component during the transition
401
- 3. **Feature flag controls behavior**: Dutchie's feature flag determines which implementation is used
402
- 4. **Future-proof**: Once the rollout completes, only the function approach will be supported
549
+ // Clear cart
550
+ actions.clearCart(): void
403
551
 
404
- ## Best Practices
552
+ // Cart visibility
553
+ actions.showCart(): void
554
+ actions.hideCart(): void
405
555
 
406
- ### Data Loading
556
+ // Checkout
557
+ actions.goToCheckout(): void
558
+
559
+ // Pricing type
560
+ actions.updatePricingType(pricingType: 'med' | 'rec'): void
561
+ ```
407
562
 
408
- When using the `useDataBridge()` hook in a component, always handle loading states:
563
+ **Example:**
409
564
 
410
565
  ```tsx
411
- const MyComponent = () => {
412
- const { dataLoaders } = useDataBridge();
413
- const { data, isLoading } = useAsyncLoader(dataLoaders.products);
566
+ const { actions } = useDataBridge();
414
567
 
415
- if (isLoading) return <LoadingSpinner />;
416
- if (!data) return <EmptyProducts message="No products found" />;
568
+ // Add product to cart
569
+ await actions.addToCart({
570
+ productId: "product-123",
571
+ name: "Chocolate Bar",
572
+ option: "10mg",
573
+ price: 25.0,
574
+ quantity: 2,
575
+ });
417
576
 
418
- return <ProductList products={data} />;
419
- };
577
+ // Update quantity
578
+ actions.updateCartItem(existingItem, { ...existingItem, quantity: 3 });
579
+
580
+ // Show cart sidebar
581
+ actions.showCart();
582
+
583
+ // Proceed to checkout
584
+ actions.goToCheckout();
420
585
  ```
421
586
 
422
- ### Error Handling
587
+ #### Authentication Actions
423
588
 
424
- Any uncaught errors will be caught by a Dutchie error boundary, and the fallback will be rendered.
589
+ ```typescript
590
+ // Navigate to login page
591
+ actions.goToLogin(): void
592
+
593
+ // Navigate to registration page
594
+ actions.goToRegister(): void
425
595
 
426
- ### TypeScript Best Practices
596
+ // Navigate to loyalty page (redirects to home if not logged in)
597
+ actions.goToLoyalty(params?: { query?: Record<string, string> }): void
598
+ ```
427
599
 
428
- Leverage the provided types for better development experience:
600
+ **Example:**
429
601
 
430
602
  ```tsx
431
- import { Product, Category, useDataBridge } from "@dutchie/ecommerce-extensions-sdk";
603
+ const { user, actions } = useDataBridge();
432
604
 
433
- interface ProductFilterProps {
434
- products: Product[];
435
- categories: Category[];
436
- onFilterChange: (categoryId: string) => void;
605
+ if (!user) {
606
+ return (
607
+ <div>
608
+ <button onClick={actions.goToLogin}>Login</button>
609
+ <button onClick={actions.goToRegister}>Sign Up</button>
610
+ </div>
611
+ );
437
612
  }
613
+ ```
438
614
 
439
- const ProductFilter: React.FC<ProductFilterProps> = ({ products, categories, onFilterChange }) => {
440
- // Implementation
441
- };
615
+ ### Data Loaders
616
+
617
+ Async functions for loading platform data. All loaders return promises and are accessible via `useDataBridge()`.
618
+
619
+ #### Available Data Loaders
620
+
621
+ ```typescript
622
+ interface DataLoaders {
623
+ // Product catalog
624
+ products(): Promise<Product[]>;
625
+ product(): Promise<Product | null>; // Only populated on product detail pages
626
+
627
+ // Taxonomy
628
+ categories(): Promise<Category[]>;
629
+ brands(): Promise<Brand[]>;
630
+ collections(): Promise<Collection[]>;
631
+
632
+ // Promotions
633
+ specials(): Promise<Special[]>;
634
+
635
+ // Store locations
636
+ locations(): Promise<Dispensary[]>;
637
+
638
+ // Integration data
639
+ integrationValue(key: string): Promise<string | undefined>;
640
+ }
442
641
  ```
443
642
 
444
- ### Context Usage
643
+ **Return Values:**
644
+
645
+ - All list loaders return empty arrays (`[]`) when no data is available
646
+ - `product()` returns `null` when not on a product details page
647
+ - `integrationValue()` returns `undefined` when key is not found
445
648
 
446
- Always check for data availability before using:
649
+ **Example:**
447
650
 
448
651
  ```tsx
449
- const UserProfile = () => {
450
- const { user, actions } = useDataBridge();
652
+ import {
653
+ useDataBridge,
654
+ useAsyncLoader,
655
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
451
656
 
452
- if (!user) {
453
- return (
454
- <div className="login-prompt">
455
- <p>Please log in to view your profile</p>
456
- <button onClick={actions.goToLogin}>Login</button>
457
- </div>
458
- );
459
- }
657
+ const ProductCatalog = () => {
658
+ const { dataLoaders } = useDataBridge();
659
+
660
+ // Load products with loading state
661
+ const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
662
+
663
+ // Load categories
664
+ const { data: categories } = useAsyncLoader(dataLoaders.categories);
665
+
666
+ if (isLoading) return <div>Loading...</div>;
460
667
 
461
668
  return (
462
- <div className="user-profile">
463
- <h2>Welcome, {user.firstName}!</h2>
464
- {/* Profile content */}
669
+ <div>
670
+ <h1>Products ({products?.length || 0})</h1>
671
+ {/* Render products */}
465
672
  </div>
466
673
  );
467
674
  };
468
675
  ```
469
676
 
470
- ### Testing
677
+ **Direct usage (async/await):**
471
678
 
472
679
  ```tsx
473
- import { render, screen } from "@testing-library/react";
474
- import { DataBridgeContext } from "@dutchie/ecommerce-extensions-sdk";
475
- import MyExtension from "./MyExtension";
476
-
477
- const mockDataBridge = {
478
- menuContext: "store-front" as const,
479
- location: { id: "1", name: "Test Dispensary" },
480
- dataLoaders: {
481
- products: jest.fn().mockResolvedValue([]),
482
- // ... other loaders
483
- },
484
- actions: {
485
- addToCart: jest.fn(),
486
- // ... other actions
487
- },
488
- };
680
+ const MyComponent = () => {
681
+ const { dataLoaders } = useDataBridge();
682
+ const [products, setProducts] = useState<Product[]>([]);
489
683
 
490
- test("renders extension correctly", () => {
491
- render(
492
- <DataBridgeContext.Provider value={mockDataBridge}>
493
- <MyExtension />
494
- </DataBridgeContext.Provider>
495
- );
684
+ useEffect(() => {
685
+ dataLoaders.products().then(setProducts);
686
+ }, [dataLoaders]);
496
687
 
497
- expect(screen.getByText("Test Dispensary")).toBeInTheDocument();
498
- });
688
+ return <ProductList products={products} />;
689
+ };
499
690
  ```
500
691
 
501
- ## Core Concepts
692
+ ### Data Types
502
693
 
503
- The Dutchie Ecommerce Extensions SDK is built around several key concepts:
694
+ #### Cart
504
695
 
505
- 1. **Data Bridge**: A unified interface for accessing dispensary data, user information, and cart state
506
- 2. **Actions**: Pre-built navigation and cart management functions
507
- 3. **Remote Components**: Extension components that integrate seamlessly with the Dutchie platform
508
- 4. **Context-Driven Architecture**: React context provides data and actions throughout your extension
696
+ ```typescript
697
+ type Cart = {
698
+ discount: number; // Total discount amount
699
+ items: CartItem[]; // Array of cart items
700
+ subtotal: number; // Subtotal before tax and discounts
701
+ tax: number; // Tax amount
702
+ total: number; // Final total
703
+ };
509
704
 
510
- ## Support
705
+ type CartItem = {
706
+ productId: string; // Unique product identifier
707
+ name: string; // Product name
708
+ price: number; // Price per unit
709
+ quantity: number; // Quantity in cart
710
+ option?: string; // Variant option (e.g., "10mg", "1g")
711
+ additionalOption?: string; // Advanced use only
712
+ };
713
+ ```
511
714
 
512
- For technical support and questions about the Dutchie Ecommerce Extensions SDK:
715
+ #### User
513
716
 
514
- - 📧 Contact your Dutchie agency partner representative
515
- - 📚 Refer to the Dutchie Pro platform documentation
516
- - 🐛 Report issues through the official Dutchie support channels
717
+ ```typescript
718
+ type User = {
719
+ email: string;
720
+ firstName: string;
721
+ lastName: string;
722
+ birthday: string; // ISO date string
723
+ };
724
+ ```
517
725
 
518
- ## Updates
726
+ #### Dispensary
519
727
 
520
- Stay up to date with this SDK by checking here for changes, and updating to the latest version with:
728
+ ```typescript
729
+ type Dispensary = {
730
+ id: string;
731
+ name: string;
732
+ cname: string; // URL-friendly name
733
+ chain: string; // Chain/brand name
734
+ email: string;
735
+ phone: string;
736
+ status: string; // Operating status
737
+ medDispensary: boolean; // Supports medical
738
+ recDispensary: boolean; // Supports recreational
739
+
740
+ address: {
741
+ street1: string;
742
+ street2: string;
743
+ city: string;
744
+ state: string;
745
+ stateAbbreviation: string;
746
+ zip: string;
747
+ };
748
+
749
+ images: {
750
+ logo: string; // Logo URL
751
+ };
752
+
753
+ links: {
754
+ website: string; // Dispensary website
755
+ storeFrontRoot: string; // Store front base URL
756
+ };
757
+
758
+ orderTypes: {
759
+ pickup: boolean;
760
+ delivery: boolean;
761
+ curbsidePickup: boolean;
762
+ inStorePickup: boolean;
763
+ driveThruPickup: boolean;
764
+ kiosk: boolean;
765
+ };
766
+
767
+ orderTypesConfig: {
768
+ offerAnyPickupService?: boolean;
769
+ offerDeliveryService?: boolean;
770
+ curbsidePickup?: OrderTypeConfig;
771
+ delivery?: OrderTypeConfig;
772
+ driveThruPickup?: OrderTypeConfig;
773
+ inStorePickup?: OrderTypeConfig;
774
+ };
775
+
776
+ hours: {
777
+ curbsidePickup?: HoursSettingsForOrderType;
778
+ delivery?: HoursSettingsForOrderType;
779
+ driveThruPickup?: HoursSettingsForOrderType;
780
+ inStorePickup?: HoursSettingsForOrderType;
781
+ };
782
+ };
783
+
784
+ type OrderTypeConfig = {
785
+ enableAfterHoursOrdering?: boolean;
786
+ enableASAPOrdering?: boolean;
787
+ enableScheduledOrdering?: boolean;
788
+ };
789
+
790
+ type HoursSettingsForOrderType = {
791
+ enabled: boolean;
792
+ effectiveHours?: {
793
+ Monday?: DayHours;
794
+ Tuesday?: DayHours;
795
+ Wednesday?: DayHours;
796
+ Thursday?: DayHours;
797
+ Friday?: DayHours;
798
+ Saturday?: DayHours;
799
+ Sunday?: DayHours;
800
+ };
801
+ };
802
+
803
+ type DayHours = {
804
+ active?: boolean;
805
+ start?: string; // Time string (e.g., "09:00")
806
+ end?: string; // Time string (e.g., "21:00")
807
+ };
808
+ ```
809
+
810
+ #### Product
811
+
812
+ ```typescript
813
+ type Product = {
814
+ id: string;
815
+ cname: string; // URL-friendly name
816
+ name: string;
817
+ description: string;
818
+ image: string; // Primary image URL
819
+ price: number;
820
+ };
821
+ ```
822
+
823
+ #### Brand
824
+
825
+ ```typescript
826
+ type Brand = {
827
+ id: string;
828
+ cname: string; // URL-friendly name
829
+ name: string;
830
+ image?: string | null; // Brand logo URL
831
+ };
832
+ ```
833
+
834
+ #### Category
835
+
836
+ ```typescript
837
+ type Category = {
838
+ id: string;
839
+ cname: string; // URL-friendly name (e.g., "edibles")
840
+ name: string; // Display name (e.g., "Edibles")
841
+ };
842
+ ```
843
+
844
+ #### Collection
845
+
846
+ ```typescript
847
+ type Collection = {
848
+ id: string;
849
+ cname: string; // URL-friendly name
850
+ name: string;
851
+ };
852
+ ```
853
+
854
+ #### Special
855
+
856
+ ```typescript
857
+ type Special = {
858
+ id: string;
859
+ cname: string;
860
+ name: string;
861
+ description: string;
862
+ image: string;
863
+ };
864
+ ```
865
+
866
+ #### Menu Context
867
+
868
+ ```typescript
869
+ type MenuContext = "store-front" | "kiosk";
870
+ ```
871
+
872
+ The menu context indicates which interface the extension is running in:
873
+
874
+ - `'store-front'` - Online storefront for customers
875
+ - `'kiosk'` - In-store kiosk interface
876
+
877
+ ## Extension Development
878
+
879
+ ### Creating Components
880
+
881
+ All extension components should satisfy the `RemoteBoundaryComponent` type and set the `DataBridgeVersion` property.
882
+
883
+ **Basic Component:**
884
+
885
+ ```tsx
886
+ import {
887
+ RemoteBoundaryComponent,
888
+ DataBridgeVersion,
889
+ useDataBridge,
890
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
891
+
892
+ const Header: RemoteBoundaryComponent = () => {
893
+ const { location, user, actions } = useDataBridge();
894
+
895
+ return (
896
+ <header>
897
+ <h1>{location?.name}</h1>
898
+ {user && <span>Welcome, {user.firstName}</span>}
899
+ </header>
900
+ );
901
+ };
902
+
903
+ Header.DataBridgeVersion = DataBridgeVersion;
904
+
905
+ export default Header;
906
+ ```
907
+
908
+ **Component with Props:**
909
+
910
+ ```tsx
911
+ import type { RemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
912
+ import {
913
+ DataBridgeVersion,
914
+ useDataBridge,
915
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
916
+
917
+ interface CustomHeaderProps {
918
+ showLogo?: boolean;
919
+ className?: string;
920
+ }
921
+
922
+ const CustomHeader: RemoteBoundaryComponent<CustomHeaderProps> = ({
923
+ showLogo = true,
924
+ className,
925
+ }) => {
926
+ const { location } = useDataBridge();
927
+
928
+ return (
929
+ <header className={className}>
930
+ {showLogo && <img src={location?.images.logo} alt={location?.name} />}
931
+ <h1>{location?.name}</h1>
932
+ </header>
933
+ );
934
+ };
935
+
936
+ CustomHeader.DataBridgeVersion = DataBridgeVersion;
937
+
938
+ export default CustomHeader;
939
+ ```
940
+
941
+ ### Module Registry
942
+
943
+ The `RemoteModuleRegistry` type defines all available extension points in the Dutchie platform.
944
+
945
+ ```typescript
946
+ type RemoteModuleRegistry = {
947
+ // UI Components
948
+ StoreFrontHeader?: ModuleRegistryEntry;
949
+ StoreFrontNavigation?: ModuleRegistryEntry;
950
+ StoreFrontFooter?: ModuleRegistryEntry;
951
+ StoreFrontHero?: ModuleRegistryEntry;
952
+ StoreFrontCarouselInterstitials?: ModuleRegistryEntry[];
953
+ ProductDetailsPrimary?: ModuleRegistryEntry;
954
+
955
+ // Custom routable pages
956
+ RouteablePages?: RoutablePageRegistryEntry[];
957
+
958
+ // Metadata function
959
+ getStoreFrontMetaFields?: StoreFrontMetaFieldsFunction;
960
+
961
+ // Event handlers
962
+ events?: Events;
963
+
964
+ // Deprecated - use getStoreFrontMetaFields instead
965
+ StoreFrontMeta?: RemoteBoundaryComponent;
966
+ ProductDetailsMeta?: RemoteBoundaryComponent;
967
+ };
968
+
969
+ type ModuleRegistryEntry =
970
+ | RemoteBoundaryComponent
971
+ | MenuSpecificRemoteComponent;
972
+
973
+ type MenuSpecificRemoteComponent = {
974
+ "store-front"?: RemoteBoundaryComponent;
975
+ kiosk?: RemoteBoundaryComponent;
976
+ };
977
+
978
+ type RoutablePageRegistryEntry = {
979
+ path: string;
980
+ component: RemoteBoundaryComponent;
981
+ };
982
+ ```
983
+
984
+ **Basic Registry:**
985
+
986
+ ```tsx
987
+ import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
988
+ import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
989
+
990
+ export default {
991
+ StoreFrontHeader: createLazyRemoteBoundaryComponent(() => import("./Header")),
992
+ StoreFrontFooter: createLazyRemoteBoundaryComponent(() => import("./Footer")),
993
+ } satisfies RemoteModuleRegistry;
994
+ ```
995
+
996
+ **Menu-Specific Components:**
997
+
998
+ ```tsx
999
+ import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1000
+ import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1001
+
1002
+ export default {
1003
+ StoreFrontHeader: {
1004
+ // Different header for storefront vs kiosk
1005
+ "store-front": createLazyRemoteBoundaryComponent(
1006
+ () => import("./StoreHeader")
1007
+ ),
1008
+ kiosk: createLazyRemoteBoundaryComponent(() => import("./KioskHeader")),
1009
+ },
1010
+ StoreFrontFooter: {
1011
+ // Only override storefront, use default Dutchie footer for kiosk
1012
+ "store-front": createLazyRemoteBoundaryComponent(() => import("./Footer")),
1013
+ },
1014
+ } satisfies RemoteModuleRegistry;
1015
+ ```
1016
+
1017
+ **Custom Routable Pages:**
1018
+
1019
+ ```tsx
1020
+ import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1021
+ import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1022
+
1023
+ export default {
1024
+ RouteablePages: [
1025
+ {
1026
+ path: "/about",
1027
+ component: createLazyRemoteBoundaryComponent(
1028
+ () => import("./pages/About")
1029
+ ),
1030
+ },
1031
+ {
1032
+ path: "/contact",
1033
+ component: createLazyRemoteBoundaryComponent(
1034
+ () => import("./pages/Contact")
1035
+ ),
1036
+ },
1037
+ ],
1038
+ } satisfies RemoteModuleRegistry;
1039
+ ```
1040
+
1041
+ ### Meta Fields & SEO
1042
+
1043
+ 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.
1044
+
1045
+ > **🚀 Rollout Status:** The Dutchie platform is currently rolling out support for `getStoreFrontMetaFields` via a feature flag. During the transition period:
1046
+ >
1047
+ > - When the flag is **enabled**: Your `getStoreFrontMetaFields` function will be used (recommended)
1048
+ > - When the flag is **disabled**: The legacy `StoreFrontMeta` component will be used
1049
+ > - Both implementations can coexist in your theme during migration
1050
+ > - The component approach will be deprecated once the rollout is complete
1051
+
1052
+ #### Meta Fields Type
1053
+
1054
+ ```typescript
1055
+ type MetaFields = {
1056
+ title?: string; // Page title
1057
+ description?: string; // Meta description
1058
+ ogImage?: string; // Open Graph image URL
1059
+ canonical?: string; // Canonical URL
1060
+ structuredData?: Record<string, any>; // JSON-LD structured data
1061
+ customMeta?: Array<{
1062
+ // Additional meta tags
1063
+ name?: string;
1064
+ property?: string;
1065
+ content: string;
1066
+ }>;
1067
+ };
1068
+
1069
+ type StoreFrontMetaFieldsFunction = (
1070
+ data: CommerceComponentsDataInterface
1071
+ ) => MetaFields | Promise<MetaFields>;
1072
+ ```
1073
+
1074
+ #### Implementation
1075
+
1076
+ Create a meta fields function (e.g., `get-meta-fields.ts`):
1077
+
1078
+ ```typescript
1079
+ import type {
1080
+ CommerceComponentsDataInterface,
1081
+ MetaFields,
1082
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
1083
+
1084
+ export const getStoreFrontMetaFields = async (
1085
+ data: CommerceComponentsDataInterface
1086
+ ): Promise<MetaFields> => {
1087
+ const { location, dataLoaders } = data;
1088
+ const pathname =
1089
+ typeof window !== "undefined" ? window.location.pathname : "";
1090
+
1091
+ // Load data for current page
1092
+ const product = await dataLoaders.product();
1093
+ const categories = await dataLoaders.categories();
1094
+
1095
+ // Product detail page
1096
+ if (product) {
1097
+ return {
1098
+ title: `${product.name} | ${location?.name}`,
1099
+ description: product.description.substring(0, 155),
1100
+ ogImage: product.image,
1101
+ canonical: `${location?.links.storeFrontRoot}${pathname}`,
1102
+ structuredData: {
1103
+ "@context": "https://schema.org",
1104
+ "@type": "Product",
1105
+ name: product.name,
1106
+ description: product.description,
1107
+ image: product.image,
1108
+ offers: {
1109
+ "@type": "Offer",
1110
+ price: product.price,
1111
+ priceCurrency: "USD",
1112
+ },
1113
+ },
1114
+ };
1115
+ }
1116
+
1117
+ // Category page
1118
+ const categorySlug = pathname.split("/").pop();
1119
+ const category = categories.find((c) => c.cname === categorySlug);
1120
+ if (category) {
1121
+ return {
1122
+ title: `${category.name} | ${location?.name}`,
1123
+ description: `Shop ${category.name} products at ${location?.name}`,
1124
+ canonical: `${location?.links.storeFrontRoot}${pathname}`,
1125
+ };
1126
+ }
1127
+
1128
+ // Home page
1129
+ if (pathname === "/" || pathname === "") {
1130
+ return {
1131
+ title: `${location?.name} - Cannabis Dispensary`,
1132
+ description: `Shop cannabis products online at ${location?.name}`,
1133
+ ogImage: location?.images.logo,
1134
+ canonical: location?.links.storeFrontRoot,
1135
+ structuredData: {
1136
+ "@context": "https://schema.org",
1137
+ "@type": "Organization",
1138
+ name: location?.name,
1139
+ url: location?.links.storeFrontRoot,
1140
+ logo: location?.images.logo,
1141
+ },
1142
+ customMeta: [
1143
+ {
1144
+ name: "robots",
1145
+ content: "index, follow",
1146
+ },
1147
+ ],
1148
+ };
1149
+ }
1150
+
1151
+ // Default fallback
1152
+ return {
1153
+ title: location?.name || "Shop Cannabis",
1154
+ description: `Browse our selection at ${location?.name}`,
1155
+ };
1156
+ };
1157
+ ```
1158
+
1159
+ Register in your module registry:
1160
+
1161
+ ```typescript
1162
+ import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1163
+ import { getStoreFrontMetaFields } from "./get-meta-fields";
1164
+
1165
+ export default {
1166
+ StoreFrontHeader: createLazyRemoteBoundaryComponent(() => import("./Header")),
1167
+ StoreFrontFooter: createLazyRemoteBoundaryComponent(() => import("./Footer")),
1168
+ getStoreFrontMetaFields,
1169
+ } satisfies RemoteModuleRegistry;
1170
+ ```
1171
+
1172
+ ### Event Handlers
1173
+
1174
+ Register callbacks that are triggered by platform events.
1175
+
1176
+ ```typescript
1177
+ type Events = {
1178
+ onAfterCheckout?: (data: OnAfterCheckoutData) => void;
1179
+ };
1180
+
1181
+ type OnAfterCheckoutData = {
1182
+ orderNumber: string;
1183
+ };
1184
+ ```
1185
+
1186
+ **Example:**
1187
+
1188
+ ```typescript
1189
+ import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1190
+
1191
+ export default {
1192
+ // ... components
1193
+
1194
+ events: {
1195
+ onAfterCheckout: (data) => {
1196
+ console.log("Order completed:", data.orderNumber);
1197
+
1198
+ // Track conversion in analytics
1199
+ gtag("event", "purchase", {
1200
+ transaction_id: data.orderNumber,
1201
+ });
1202
+ },
1203
+ },
1204
+ } satisfies RemoteModuleRegistry;
1205
+ ```
1206
+
1207
+ ## Best Practices
1208
+
1209
+ ### Always Handle Loading States
1210
+
1211
+ When using data loaders, always handle loading and empty states:
1212
+
1213
+ ```tsx
1214
+ const ProductList = () => {
1215
+ const { dataLoaders } = useDataBridge();
1216
+ const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
1217
+
1218
+ if (isLoading) {
1219
+ return <LoadingSpinner />;
1220
+ }
1221
+
1222
+ if (!products || products.length === 0) {
1223
+ return <EmptyState message="No products available" />;
1224
+ }
1225
+
1226
+ return (
1227
+ <div>
1228
+ {products.map((product) => (
1229
+ <ProductCard key={product.id} product={product} />
1230
+ ))}
1231
+ </div>
1232
+ );
1233
+ };
1234
+ ```
1235
+
1236
+ ### Check Data Availability
1237
+
1238
+ Always check if optional data is available before using it:
1239
+
1240
+ ```tsx
1241
+ const UserProfile = () => {
1242
+ const { user, actions } = useDataBridge();
1243
+
1244
+ if (!user) {
1245
+ return (
1246
+ <div className="login-prompt">
1247
+ <p>Please log in to view your profile</p>
1248
+ <button onClick={actions.goToLogin}>Login</button>
1249
+ </div>
1250
+ );
1251
+ }
1252
+
1253
+ return (
1254
+ <div className="user-profile">
1255
+ <h2>Welcome, {user.firstName}!</h2>
1256
+ <p>Email: {user.email}</p>
1257
+ </div>
1258
+ );
1259
+ };
1260
+ ```
1261
+
1262
+ ### Use TypeScript for Better DX
1263
+
1264
+ Leverage the provided types for autocomplete and type safety:
1265
+
1266
+ ```tsx
1267
+ import type {
1268
+ Product,
1269
+ Category,
1270
+ RemoteBoundaryComponent,
1271
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
1272
+
1273
+ interface ProductFilterProps {
1274
+ products: Product[];
1275
+ categories: Category[];
1276
+ onFilterChange: (categoryId: string) => void;
1277
+ }
1278
+
1279
+ const ProductFilter: React.FC<ProductFilterProps> = ({
1280
+ products,
1281
+ categories,
1282
+ onFilterChange,
1283
+ }) => {
1284
+ return (
1285
+ <div>
1286
+ {categories.map((category) => (
1287
+ <button key={category.id} onClick={() => onFilterChange(category.id)}>
1288
+ {category.name}
1289
+ </button>
1290
+ ))}
1291
+ </div>
1292
+ );
1293
+ };
1294
+ ```
1295
+
1296
+ ### Error Boundaries
1297
+
1298
+ Any uncaught errors in your extension will be caught by Dutchie's error boundary. However, you should still handle expected errors gracefully:
1299
+
1300
+ ```tsx
1301
+ const ProductDetails = () => {
1302
+ const { dataLoaders } = useDataBridge();
1303
+ const [error, setError] = useState<string | null>(null);
1304
+ const { data: product, isLoading } = useAsyncLoader(dataLoaders.product);
1305
+
1306
+ if (error) {
1307
+ return <ErrorMessage message={error} />;
1308
+ }
1309
+
1310
+ if (isLoading) {
1311
+ return <LoadingSpinner />;
1312
+ }
1313
+
1314
+ if (!product) {
1315
+ return <NotFound message="Product not found" />;
1316
+ }
1317
+
1318
+ return <ProductCard product={product} />;
1319
+ };
1320
+ ```
1321
+
1322
+ ### Optimize Performance
1323
+
1324
+ Use lazy loading for large components and routes:
1325
+
1326
+ ```tsx
1327
+ import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1328
+
1329
+ // Components will be code-split and loaded on demand
1330
+ export default {
1331
+ StoreFrontHeader: createLazyRemoteBoundaryComponent(
1332
+ () => import("./components/Header"),
1333
+ { fallback: <HeaderSkeleton /> }
1334
+ ),
1335
+ StoreFrontFooter: createLazyRemoteBoundaryComponent(
1336
+ () => import("./components/Footer"),
1337
+ { fallback: <FooterSkeleton /> }
1338
+ ),
1339
+ } satisfies RemoteModuleRegistry;
1340
+ ```
1341
+
1342
+ ### Testing Components
1343
+
1344
+ Test your components using the `DataBridgeContext` provider:
1345
+
1346
+ ```tsx
1347
+ import { render, screen } from "@testing-library/react";
1348
+ import { DataBridgeContext } from "@dutchiesdk/ecommerce-extensions-sdk";
1349
+ import MyComponent from "./MyComponent";
1350
+
1351
+ const mockDataBridge = {
1352
+ menuContext: "store-front" as const,
1353
+ location: {
1354
+ id: "1",
1355
+ name: "Test Dispensary",
1356
+ cname: "test-dispensary",
1357
+ // ... other required fields
1358
+ },
1359
+ dataLoaders: {
1360
+ products: jest.fn().mockResolvedValue([]),
1361
+ categories: jest.fn().mockResolvedValue([]),
1362
+ brands: jest.fn().mockResolvedValue([]),
1363
+ collections: jest.fn().mockResolvedValue([]),
1364
+ specials: jest.fn().mockResolvedValue([]),
1365
+ locations: jest.fn().mockResolvedValue([]),
1366
+ product: jest.fn().mockResolvedValue(null),
1367
+ integrationValue: jest.fn().mockResolvedValue(undefined),
1368
+ },
1369
+ actions: {
1370
+ addToCart: jest.fn(),
1371
+ goToProductDetails: jest.fn(),
1372
+ goToCategory: jest.fn(),
1373
+ // ... other actions
1374
+ },
1375
+ };
1376
+
1377
+ test("renders component correctly", () => {
1378
+ render(
1379
+ <DataBridgeContext.Provider value={mockDataBridge}>
1380
+ <MyComponent />
1381
+ </DataBridgeContext.Provider>
1382
+ );
1383
+
1384
+ expect(screen.getByText("Test Dispensary")).toBeInTheDocument();
1385
+ });
1386
+
1387
+ test("handles cart actions", async () => {
1388
+ render(
1389
+ <DataBridgeContext.Provider value={mockDataBridge}>
1390
+ <MyComponent />
1391
+ </DataBridgeContext.Provider>
1392
+ );
1393
+
1394
+ const addButton = screen.getByRole("button", { name: /add to cart/i });
1395
+ addButton.click();
1396
+
1397
+ expect(mockDataBridge.actions.addToCart).toHaveBeenCalledWith({
1398
+ productId: "123",
1399
+ name: "Test Product",
1400
+ price: 25.0,
1401
+ quantity: 1,
1402
+ });
1403
+ });
1404
+ ```
1405
+
1406
+ ## Examples
1407
+
1408
+ ### Custom Product Listing
1409
+
1410
+ ```tsx
1411
+ import {
1412
+ RemoteBoundaryComponent,
1413
+ DataBridgeVersion,
1414
+ useDataBridge,
1415
+ useAsyncLoader,
1416
+ type Product,
1417
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
1418
+
1419
+ const ProductListing: RemoteBoundaryComponent = () => {
1420
+ const { dataLoaders, actions } = useDataBridge();
1421
+ const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
1422
+ const { data: categories } = useAsyncLoader(dataLoaders.categories);
1423
+ const [filter, setFilter] = useState<string | null>(null);
1424
+
1425
+ const filteredProducts = products?.filter(
1426
+ (p) => !filter || p.categoryId === filter
1427
+ );
1428
+
1429
+ if (isLoading) {
1430
+ return <div className="loading">Loading products...</div>;
1431
+ }
1432
+
1433
+ return (
1434
+ <div className="product-listing">
1435
+ <div className="filters">
1436
+ <button onClick={() => setFilter(null)}>All</button>
1437
+ {categories?.map((cat) => (
1438
+ <button key={cat.id} onClick={() => setFilter(cat.id)}>
1439
+ {cat.name}
1440
+ </button>
1441
+ ))}
1442
+ </div>
1443
+
1444
+ <div className="products-grid">
1445
+ {filteredProducts?.map((product) => (
1446
+ <div
1447
+ key={product.id}
1448
+ className="product-card"
1449
+ onClick={() => actions.goToProductDetails({ id: product.id })}
1450
+ >
1451
+ <img src={product.image} alt={product.name} />
1452
+ <h3>{product.name}</h3>
1453
+ <p>${product.price.toFixed(2)}</p>
1454
+ <button
1455
+ onClick={(e) => {
1456
+ e.stopPropagation();
1457
+ actions.addToCart({
1458
+ productId: product.id,
1459
+ name: product.name,
1460
+ price: product.price,
1461
+ quantity: 1,
1462
+ });
1463
+ }}
1464
+ >
1465
+ Add to Cart
1466
+ </button>
1467
+ </div>
1468
+ ))}
1469
+ </div>
1470
+ </div>
1471
+ );
1472
+ };
1473
+
1474
+ ProductListing.DataBridgeVersion = DataBridgeVersion;
1475
+
1476
+ export default ProductListing;
1477
+ ```
1478
+
1479
+ ### Custom Header with Cart
1480
+
1481
+ ```tsx
1482
+ import {
1483
+ RemoteBoundaryComponent,
1484
+ DataBridgeVersion,
1485
+ useDataBridge,
1486
+ } from "@dutchiesdk/ecommerce-extensions-sdk";
1487
+
1488
+ const Header: RemoteBoundaryComponent = () => {
1489
+ const { location, user, cart, actions } = useDataBridge();
1490
+
1491
+ const cartItemCount =
1492
+ cart?.items.reduce((sum, item) => sum + item.quantity, 0) || 0;
1493
+
1494
+ return (
1495
+ <header className="site-header">
1496
+ <div className="logo" onClick={() => actions.goToStoreFront()}>
1497
+ <img src={location?.images.logo} alt={location?.name} />
1498
+ <h1>{location?.name}</h1>
1499
+ </div>
1500
+
1501
+ <nav>
1502
+ <button onClick={() => actions.goToProductList({})}>Shop All</button>
1503
+ <button onClick={() => actions.goToSpecialsList()}>Deals</button>
1504
+ <button onClick={() => actions.goToStoreLocator()}>Locations</button>
1505
+ </nav>
1506
+
1507
+ <div className="user-actions">
1508
+ {user ? (
1509
+ <>
1510
+ <span>Hello, {user.firstName}</span>
1511
+ <button onClick={() => actions.goToLoyalty()}>Rewards</button>
1512
+ </>
1513
+ ) : (
1514
+ <>
1515
+ <button onClick={actions.goToLogin}>Login</button>
1516
+ <button onClick={actions.goToRegister}>Sign Up</button>
1517
+ </>
1518
+ )}
1519
+
1520
+ <button className="cart-button" onClick={actions.showCart}>
1521
+ Cart ({cartItemCount})
1522
+ </button>
1523
+ </div>
1524
+ </header>
1525
+ );
1526
+ };
1527
+
1528
+ Header.DataBridgeVersion = DataBridgeVersion;
1529
+
1530
+ export default Header;
1531
+ ```
1532
+
1533
+ ### Complete Module Registry Example
1534
+
1535
+ ```tsx
1536
+ import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1537
+ import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1538
+ import { getStoreFrontMetaFields } from "./get-meta-fields";
1539
+
1540
+ export default {
1541
+ // Header and footer
1542
+ StoreFrontHeader: createLazyRemoteBoundaryComponent(
1543
+ () => import("./components/Header"),
1544
+ { fallback: <div>Loading...</div> }
1545
+ ),
1546
+
1547
+ StoreFrontFooter: createLazyRemoteBoundaryComponent(
1548
+ () => import("./components/Footer")
1549
+ ),
1550
+
1551
+ // Hero section
1552
+ StoreFrontHero: createLazyRemoteBoundaryComponent(
1553
+ () => import("./components/Hero")
1554
+ ),
1555
+
1556
+ // Custom product page
1557
+ ProductDetailsPrimary: createLazyRemoteBoundaryComponent(
1558
+ () => import("./components/ProductDetails")
1559
+ ),
1560
+
1561
+ // Custom routable pages
1562
+ RouteablePages: [
1563
+ {
1564
+ path: "/about",
1565
+ component: createLazyRemoteBoundaryComponent(
1566
+ () => import("./pages/About")
1567
+ ),
1568
+ },
1569
+ {
1570
+ path: "/contact",
1571
+ component: createLazyRemoteBoundaryComponent(
1572
+ () => import("./pages/Contact")
1573
+ ),
1574
+ },
1575
+ ],
1576
+
1577
+ // Meta fields for SEO
1578
+ getStoreFrontMetaFields,
1579
+
1580
+ // Event handlers
1581
+ events: {
1582
+ onAfterCheckout: (data) => {
1583
+ // Track conversion
1584
+ console.log("Order completed:", data.orderNumber);
1585
+
1586
+ // Send to analytics
1587
+ if (typeof gtag !== "undefined") {
1588
+ gtag("event", "purchase", {
1589
+ transaction_id: data.orderNumber,
1590
+ });
1591
+ }
1592
+ },
1593
+ },
1594
+ } satisfies RemoteModuleRegistry;
1595
+ ```
1596
+
1597
+ ## Support
1598
+
1599
+ For technical support and questions about the Dutchie Ecommerce Extensions SDK:
1600
+
1601
+ - 📧 Contact your Dutchie agency partner representative
1602
+ - 📚 Refer to the Dutchie Pro platform documentation
1603
+ - 🐛 Report issues through the official Dutchie support channels
1604
+
1605
+ ## Updates
1606
+
1607
+ Stay up to date with this SDK by checking the repository for changes and updating to the latest version with:
521
1608
 
522
1609
  ```bash
523
1610
  npm install @dutchiesdk/ecommerce-extensions-sdk@latest
1611
+ # or
1612
+ yarn upgrade @dutchiesdk/ecommerce-extensions-sdk@latest
524
1613
  ```
1614
+
1615
+ ## License
1616
+
1617
+ MIT
1618
+
1619
+ ---
1620
+
1621
+ **Made with 💚 by Dutchie**