@dutchiesdk/ecommerce-extensions-sdk 0.31.1 → 0.32.1

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
@@ -1,51 +1,22 @@
1
- ![Dutchie Logo](https://cdn.prod.website-files.com/61930755e474060ca6298871/64a32a14b7840d452f594fbc_Logo.svg)
2
-
3
1
  # @dutchiesdk/ecommerce-extensions-sdk
4
2
 
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
-
7
- > ⚠️ **Alpha Release Warning**
8
- >
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
-
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)
3
+ [![npm](https://img.shields.io/npm/v/@dutchiesdk/ecommerce-extensions-sdk)](https://www.npmjs.com/package/@dutchiesdk/ecommerce-extensions-sdk)
34
4
 
35
- ## Prerequisites
5
+ React SDK for building custom storefront extensions on the **Dutchie Ecommerce Pro**
6
+ platform. It provides the `useDataBridge()` hook and supporting types/components that
7
+ give your extension components typed access to dispensary data, cart state, user
8
+ info, navigation actions, and async data loaders.
36
9
 
37
- ### Development Environment Access
10
+ > **Alpha Release** — This SDK is currently in alpha and subject to breaking changes.
38
11
 
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.
40
-
41
- To request access to the development environment contact: **partners@dutchie.com**. Please include your agency information and intended use case when requesting access.
42
-
43
- ### Requirements
12
+ ## Requirements
44
13
 
45
14
  - Node.js >= 18.0.0
46
- - React ^17.0.0 or ^18.0.0
47
- - react-dom ^17.0.0 or ^18.0.0
15
+ - React ^17.0.0 or ^18.0.0 / react-dom ^17.0.0 or ^18.0.0
48
16
  - react-shadow ^20.5.0
17
+ - A **Dutchie-provided development environment** — the SDK alone isn't sufficient to
18
+ build/deploy extensions. Contact **partners@dutchie.com** to request agency partner
19
+ access.
49
20
 
50
21
  ## Installation
51
22
 
@@ -81,1935 +52,30 @@ MyExtension.DataBridgeVersion = DataBridgeVersion;
81
52
  export default MyExtension;
82
53
  ```
83
54
 
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
97
-
98
- #### `useDataBridge()`
99
-
100
- The primary hook for accessing the Dutchie platform data and functionality.
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
-
125
- ```tsx
126
- import { useDataBridge } from "@dutchiesdk/ecommerce-extensions-sdk";
127
-
128
- const MyComponent = () => {
129
- const {
130
- menuContext, // 'store-front' | 'kiosk'
131
- location, // Current dispensary information
132
- user, // Authenticated user data
133
- cart, // Current cart state
134
- dataLoaders, // Async data loading functions
135
- actions, // Navigation and cart actions
136
- } = useDataBridge();
137
-
138
- return <div>{location?.name}</div>;
139
- };
140
- ```
141
-
142
- #### `useAsyncLoader(fn, params?)`
143
-
144
- A utility hook for handling async data loading with loading states.
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
-
167
- ```tsx
168
- import {
169
- useAsyncLoader,
170
- useDataBridge,
171
- } from "@dutchiesdk/ecommerce-extensions-sdk";
172
-
173
- const ProductList = () => {
174
- const { dataLoaders } = useDataBridge();
175
- const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
176
-
177
- if (isLoading) return <div>Loading products...</div>;
178
-
179
- return (
180
- <div>
181
- {products?.map((product) => (
182
- <div key={product.id}>{product.name}</div>
183
- ))}
184
- </div>
185
- );
186
- };
187
- ```
188
-
189
- ### Components & HOCs
190
-
191
- #### `RemoteBoundaryComponent<P>`
192
-
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.
194
-
195
- **Type Signature:**
196
-
197
- ```typescript
198
- type RemoteBoundaryComponent<P = {}> = React.FC<P> & {
199
- DataBridgeVersion: string;
200
- };
201
- ```
202
-
203
- **Properties:**
204
-
205
- - Component must be a functional React component
206
- - Must have a `DataBridgeVersion` static property matching the SDK version
207
-
208
- **Example:**
209
-
210
- ```tsx
211
- import {
212
- RemoteBoundaryComponent,
213
- DataBridgeVersion,
214
- useDataBridge,
215
- } from "@dutchiesdk/ecommerce-extensions-sdk";
216
-
217
- const MyCustomHeader: RemoteBoundaryComponent = () => {
218
- const { location, user, actions } = useDataBridge();
219
-
220
- return (
221
- <header>
222
- <h1>{location?.name}</h1>
223
- {user ? (
224
- <span>Welcome, {user.firstName}!</span>
225
- ) : (
226
- <button onClick={actions.goToLogin}>Login</button>
227
- )}
228
- </header>
229
- );
230
- };
231
-
232
- // Required: Set the DataBridgeVersion
233
- MyCustomHeader.DataBridgeVersion = DataBridgeVersion;
234
-
235
- export default MyCustomHeader;
236
- ```
237
-
238
- #### `withRemoteBoundary(WrappedComponent)`
239
-
240
- A higher-order component (HOC) that wraps a component with the Data Bridge context provider.
241
-
242
- **Signature:**
243
-
244
- ```typescript
245
- function withRemoteBoundary(
246
- WrappedComponent: ComponentType
247
- ): RemoteBoundaryComponent;
248
- ```
249
-
250
- **Parameters:**
251
-
252
- - `WrappedComponent` - The component to wrap with Data Bridge context
253
-
254
- **Returns:** A `RemoteBoundaryComponent` with Data Bridge context
255
-
256
- **Example:**
257
-
258
- ```tsx
259
- import { withRemoteBoundary } from "@dutchiesdk/ecommerce-extensions-sdk";
260
-
261
- const MyComponent = () => {
262
- return <div>My Component</div>;
263
- };
264
-
265
- export default withRemoteBoundary(MyComponent);
266
- ```
267
-
268
- #### `createLazyRemoteBoundaryComponent(importFn, options?)`
269
-
270
- Creates a lazy-loaded remote boundary component with automatic code splitting and error handling.
271
-
272
- **Signature:**
273
-
274
- ```typescript
275
- function createLazyRemoteBoundaryComponent<P = WithRemoteBoundaryProps>(
276
- importFn: () => Promise<{ default: ComponentType }>,
277
- options?: LazyRemoteBoundaryOptions
278
- ): RemoteBoundaryComponent<P>;
279
- ```
280
-
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:**
291
-
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
- ```
309
-
310
- #### `createRemoteBoundaryComponent(Component)`
311
-
312
- Wraps a component with the Data Bridge context provider synchronously. Use this instead of `createLazyRemoteBoundaryComponent` when you don't need code-splitting (e.g., the component is already in the same bundle, or you're registering a component that receives typed props like `ProductCardGridProps`).
313
-
314
- **Signature:**
315
-
316
- ```typescript
317
- function createRemoteBoundaryComponent<P = WithRemoteBoundaryProps>(
318
- Component: ComponentType
319
- ): RemoteBoundaryComponent<P>;
320
- ```
321
-
322
- **Parameters:**
323
-
324
- - `Component` - The React component to wrap with Data Bridge context
325
-
326
- **Returns:** A `RemoteBoundaryComponent` with Data Bridge context and `DataBridgeVersion` attached
327
-
328
- **Example:**
329
-
330
- ```tsx
331
- import {
332
- createRemoteBoundaryComponent,
333
- type ProductCardGridProps,
334
- } from "@dutchiesdk/ecommerce-extensions-sdk";
335
-
336
- const MyProductCard = ({ product, pricing, onAddToCart }: ProductCardGridProps) => {
337
- return (
338
- <div>
339
- <h3>{product.name}</h3>
340
- <p>{pricing.displayPrice}</p>
341
- <button onClick={onAddToCart}>{pricing.buttonCopy}</button>
342
- </div>
343
- );
344
- };
345
-
346
- export default createRemoteBoundaryComponent<ProductCardGridProps>(MyProductCard);
347
- ```
348
-
349
- ### Context
350
-
351
- #### `DataBridgeContext`
352
-
353
- The React context that provides the Data Bridge interface to all components.
354
-
355
- **Type:**
356
-
357
- ```typescript
358
- React.Context<CommerceComponentsDataInterface | undefined>;
359
- ```
360
-
361
- **Usage:**
362
-
363
- Typically you'll use the `useDataBridge()` hook instead of accessing the context directly. However, you can use it for testing or advanced scenarios:
364
-
365
- ```tsx
366
- import { DataBridgeContext } from "@dutchiesdk/ecommerce-extensions-sdk";
367
-
368
- // Testing example
369
- const mockDataBridge = {
370
- menuContext: "store-front" as const,
371
- location: { id: "1", name: "Test Dispensary" },
372
- dataLoaders: {
373
- /* ... */
374
- },
375
- actions: {
376
- /* ... */
377
- },
378
- };
379
-
380
- render(
381
- <DataBridgeContext.Provider value={mockDataBridge}>
382
- <MyComponent />
383
- </DataBridgeContext.Provider>
384
- );
385
- ```
386
-
387
- #### `DataBridgeVersion`
388
-
389
- A string constant representing the current SDK version, used for compatibility checking.
390
-
391
- **Type:** `string`
392
-
393
- **Usage:**
394
-
395
- ```tsx
396
- import {
397
- DataBridgeVersion,
398
- RemoteBoundaryComponent,
399
- } from "@dutchiesdk/ecommerce-extensions-sdk";
400
-
401
- const MyComponent: RemoteBoundaryComponent = () => {
402
- return <div>Component</div>;
403
- };
404
-
405
- MyComponent.DataBridgeVersion = DataBridgeVersion;
406
- ```
407
-
408
- ### Types
409
-
410
- The SDK exports comprehensive TypeScript types for all data structures. Here are the key types:
411
-
412
- #### Core Interface Types
413
-
414
- ```typescript
415
- import type {
416
- // Main interface
417
- CommerceComponentsDataInterface,
418
-
419
- // Component types
420
- RemoteBoundaryComponent,
421
- RemoteModuleRegistry,
422
- ListPageEntry,
423
- ListPageCategory,
424
-
425
- // Data types
426
- Actions,
427
- DataLoaders,
428
- Cart,
429
- CartItem,
430
- User,
431
- Dispensary,
432
- Product,
433
- Brand,
434
- Category,
435
- Collection,
436
- Special,
437
-
438
- // Metadata
439
- MetaFields,
440
- StoreFrontMetaFieldsFunction,
441
-
442
- // Product card types
443
- ProductCardProductData,
444
- ProductCardImage,
445
- ProductCardBadgeData,
446
- ProductCardPricingData,
447
- ProductCardOptionData,
448
- ProductCardOptionsData,
449
- ProductCardGridProps,
450
- ProductCardListItemProps,
451
-
452
- // Events
453
- Events,
454
- OnAfterCheckoutData,
455
-
456
- // Context
457
- MenuContext,
458
- } from "@dutchiesdk/ecommerce-extensions-sdk";
459
- ```
460
-
461
- ## Data Interface
462
-
463
- ### Actions API
464
-
465
- The `actions` object provides pre-built functions for common e-commerce operations. All actions are accessible via the `useDataBridge()` hook.
466
-
467
- #### Navigation Actions
468
-
469
- ```typescript
470
- // Store navigation
471
- actions.goToStoreFront(params?: { query?: Record<string, string> }): void
472
- actions.goToStore(params: { id?: string; cname?: string; query?: Record<string, string> }): void
473
- actions.goToInfoPage(params?: { query?: Record<string, string> }): void
474
-
475
- // Browse and search
476
- actions.goToStoreBrowser(params?: { query?: Record<string, string> }): void
477
- actions.goToStoreLocator(params?: { query?: Record<string, string> }): void
478
- actions.goToSearch(query?: string, params?: { query?: Record<string, string> }): void
479
- ```
480
-
481
- **Example:**
482
-
483
- ```tsx
484
- const { actions } = useDataBridge();
485
-
486
- // Navigate to home page
487
- actions.goToStoreFront();
488
-
489
- // Navigate with query params
490
- actions.goToSearch("edibles", { query: { sort: "price-asc" } });
491
- ```
492
-
493
- #### Product & Category Navigation
494
-
495
- ```typescript
496
- // Product details
497
- actions.goToProductDetails(params: {
498
- id?: string;
499
- cname?: string;
500
- query?: Record<string, string>;
501
- }): void
502
-
503
- // Category pages
504
- actions.goToCategory(params: {
505
- id?: string;
506
- cname?: string;
507
- query?: Record<string, string>;
508
- }): void
509
-
510
- // Brand pages
511
- actions.goToBrand(params: {
512
- id?: string;
513
- cname?: string;
514
- query?: Record<string, string>;
515
- }): void
516
-
517
- // Collection pages
518
- actions.goToCollection(params: {
519
- id?: string;
520
- cname?: string;
521
- query?: Record<string, string>;
522
- }): void
523
-
524
- // Special pages
525
- actions.goToSpecial(params: {
526
- id: string;
527
- type: 'offer' | 'sale';
528
- query?: Record<string, string>;
529
- }): void
530
- ```
531
-
532
- **Example:**
533
-
534
- ```tsx
535
- const { actions } = useDataBridge();
536
-
537
- // Navigate by ID
538
- actions.goToProductDetails({ id: "product-123" });
539
-
540
- // Navigate by cname (URL-friendly name)
541
- actions.goToCategory({ cname: "edibles" });
542
-
543
- // With query params
544
- actions.goToBrand({
545
- cname: "kiva-confections",
546
- query: { filter: "chocolate" },
547
- });
548
- ```
549
-
550
- #### List Page Actions
551
-
552
- ```typescript
553
- // Product list with filters
554
- actions.goToProductList(params: {
555
- brandId?: string;
556
- brandCname?: string;
557
- categoryId?: string;
558
- categoryCname?: string;
559
- collectionId?: string;
560
- collectionCname?: string;
561
- query?: Record<string, string>;
562
- }): void
563
-
564
- // List pages
565
- actions.goToBrandList(params?: { query?: Record<string, string> }): void
566
- actions.goToSpecialsList(params?: { query?: Record<string, string> }): void
567
- ```
568
-
569
- **Example:**
570
-
571
- ```tsx
572
- const { actions } = useDataBridge();
573
-
574
- // Show products in a category
575
- actions.goToProductList({ categoryCname: "edibles" });
576
-
577
- // Show products by brand and category
578
- actions.goToProductList({
579
- brandCname: "kiva-confections",
580
- categoryCname: "chocolate",
581
- query: { sort: "popular" },
582
- });
583
-
584
- // Show all brands
585
- actions.goToBrandList();
586
- ```
587
-
588
- #### Cart Actions
589
-
590
- ```typescript
591
- // Add items to cart
592
- actions.addToCart(item: CartItem): Promise<void>
593
-
594
- // Remove items from cart
595
- actions.removeFromCart(item: CartItem): void
596
-
597
- // Update cart item
598
- actions.updateCartItem(existingItem: CartItem, newItem: CartItem): void
599
-
600
- // Clear cart
601
- actions.clearCart(): void
602
-
603
- // Cart visibility
604
- actions.showCart(): void
605
- actions.hideCart(): void
606
-
607
- // Checkout
608
- actions.goToCheckout(): void
609
-
610
- // Pricing type
611
- actions.updatePricingType(pricingType: 'med' | 'rec'): void
612
- ```
613
-
614
- **Example:**
615
-
616
- ```tsx
617
- const { actions } = useDataBridge();
618
-
619
- // Add product to cart
620
- await actions.addToCart({
621
- productId: "product-123",
622
- name: "Chocolate Bar",
623
- option: "10mg",
624
- price: 25.0,
625
- quantity: 2,
626
- });
627
-
628
- // Update quantity
629
- actions.updateCartItem(existingItem, { ...existingItem, quantity: 3 });
630
-
631
- // Show cart sidebar
632
- actions.showCart();
633
-
634
- // Proceed to checkout
635
- actions.goToCheckout();
636
- ```
637
-
638
- #### Authentication Actions
639
-
640
- ```typescript
641
- // Navigate to login page
642
- actions.goToLogin(): void
643
-
644
- // Navigate to registration page
645
- actions.goToRegister(): void
646
-
647
- // Navigate to loyalty page (redirects to home if not logged in)
648
- actions.goToLoyalty(params?: { query?: Record<string, string> }): void
649
- ```
650
-
651
- **Example:**
652
-
653
- ```tsx
654
- const { user, actions } = useDataBridge();
655
-
656
- if (!user) {
657
- return (
658
- <div>
659
- <button onClick={actions.goToLogin}>Login</button>
660
- <button onClick={actions.goToRegister}>Sign Up</button>
661
- </div>
662
- );
663
- }
664
- ```
665
-
666
- ### Data Loaders
667
-
668
- Async functions for loading platform data. All loaders return promises and are accessible via `useDataBridge()`.
669
-
670
- #### Available Data Loaders
671
-
672
- ```typescript
673
- interface DataLoaders {
674
- // Product catalog
675
- products(): Promise<Product[]>;
676
- product(): Promise<Product | null>; // Only populated on product detail pages
677
-
678
- // Taxonomy
679
- categories(): Promise<Category[]>;
680
- brands(): Promise<Brand[]>;
681
- collections(): Promise<Collection[]>;
682
-
683
- // Promotions
684
- specials(): Promise<Special[]>;
685
-
686
- // Store locations
687
- locations(): Promise<Dispensary[]>;
688
-
689
- // Integration data
690
- integrationValue(key: string): Promise<string | undefined>;
691
- }
692
- ```
693
-
694
- **Return Values:**
55
+ ## Full Documentation
695
56
 
696
- - All list loaders return empty arrays (`[]`) when no data is available
697
- - `product()` returns `null` when not on a product details page
698
- - `integrationValue()` returns `undefined` when key is not found
57
+ The complete guide architecture, the Data Bridge, full API reference (hooks,
58
+ components, context, types), data interface (actions, data loaders, data types),
59
+ extension development guides, custom product cards, meta/SEO, event handlers, best
60
+ practices, and examples — lives at:
699
61
 
700
- **Example:**
62
+ **👉 [docs.dutchie.com/dutchie-core-docs/dutchie-ecommerce-pro-sdk](https://docs.dutchie.com/dutchie-core-docs/dutchie-ecommerce-pro-sdk)**
701
63
 
702
- ```tsx
703
- import {
704
- useDataBridge,
705
- useAsyncLoader,
706
- } from "@dutchiesdk/ecommerce-extensions-sdk";
64
+ Also see the [`@dutchiesdk/ecommerce-sdk-cli`](https://www.npmjs.com/package/@dutchiesdk/ecommerce-sdk-cli)
65
+ package for the dev server, theme generator, and an MCP server that gives AI coding
66
+ assistants direct search access to these docs.
707
67
 
708
- const ProductCatalog = () => {
709
- const { dataLoaders } = useDataBridge();
68
+ ## Changelog
710
69
 
711
- // Load products with loading state
712
- const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
70
+ Release notes are published automatically to the
71
+ [Changelog](https://docs.dutchie.com/dutchie-core-docs/dutchie-ecommerce-pro-sdk/changelog)
72
+ page in the docs.
713
73
 
714
- // Load categories
715
- const { data: categories } = useAsyncLoader(dataLoaders.categories);
74
+ ## Support
716
75
 
717
- if (isLoading) return <div>Loading...</div>;
718
-
719
- return (
720
- <div>
721
- <h1>Products ({products?.length || 0})</h1>
722
- {/* Render products */}
723
- </div>
724
- );
725
- };
726
- ```
727
-
728
- **Direct usage (async/await):**
729
-
730
- ```tsx
731
- const MyComponent = () => {
732
- const { dataLoaders } = useDataBridge();
733
- const [products, setProducts] = useState<Product[]>([]);
734
-
735
- useEffect(() => {
736
- dataLoaders.products().then(setProducts);
737
- }, [dataLoaders]);
738
-
739
- return <ProductList products={products} />;
740
- };
741
- ```
742
-
743
- ### Data Types
744
-
745
- #### Cart
746
-
747
- ```typescript
748
- type Cart = {
749
- discount: number; // Total discount amount
750
- items: CartItem[]; // Array of cart items
751
- subtotal: number; // Subtotal before tax and discounts
752
- tax: number; // Tax amount
753
- total: number; // Final total
754
- };
755
-
756
- type CartItem = {
757
- productId: string; // Unique product identifier
758
- name: string; // Product name
759
- price: number; // Price per unit
760
- quantity: number; // Quantity in cart
761
- option?: string; // Variant option (e.g., "10mg", "1g")
762
- additionalOption?: string; // Advanced use only
763
- };
764
- ```
765
-
766
- #### User
767
-
768
- ```typescript
769
- type User = {
770
- email: string;
771
- firstName: string;
772
- lastName: string;
773
- birthday: string; // ISO date string
774
- };
775
- ```
776
-
777
- #### Dispensary
778
-
779
- ```typescript
780
- type Dispensary = {
781
- id: string;
782
- name: string;
783
- cname: string; // URL-friendly name
784
- chain: string; // Chain/brand name
785
- email: string;
786
- phone: string;
787
- status: string; // Operating status
788
- medDispensary: boolean; // Supports medical
789
- recDispensary: boolean; // Supports recreational
790
-
791
- address: {
792
- street1: string;
793
- street2: string;
794
- city: string;
795
- state: string;
796
- stateAbbreviation: string;
797
- zip: string;
798
- };
799
-
800
- images: {
801
- logo: string; // Logo URL
802
- };
803
-
804
- links: {
805
- website: string; // Dispensary website
806
- storeFrontRoot: string; // Store front base URL
807
- };
808
-
809
- orderTypes: {
810
- pickup: boolean;
811
- delivery: boolean;
812
- curbsidePickup: boolean;
813
- inStorePickup: boolean;
814
- driveThruPickup: boolean;
815
- kiosk: boolean;
816
- };
817
-
818
- orderTypesConfig: {
819
- offerAnyPickupService?: boolean;
820
- offerDeliveryService?: boolean;
821
- curbsidePickup?: OrderTypeConfig;
822
- delivery?: OrderTypeConfig;
823
- driveThruPickup?: OrderTypeConfig;
824
- inStorePickup?: OrderTypeConfig;
825
- };
826
-
827
- hours: {
828
- curbsidePickup?: HoursSettingsForOrderType;
829
- delivery?: HoursSettingsForOrderType;
830
- driveThruPickup?: HoursSettingsForOrderType;
831
- inStorePickup?: HoursSettingsForOrderType;
832
- };
833
- };
834
-
835
- type OrderTypeConfig = {
836
- enableAfterHoursOrdering?: boolean;
837
- enableASAPOrdering?: boolean;
838
- enableScheduledOrdering?: boolean;
839
- };
840
-
841
- type HoursSettingsForOrderType = {
842
- enabled: boolean;
843
- effectiveHours?: {
844
- Monday?: DayHours;
845
- Tuesday?: DayHours;
846
- Wednesday?: DayHours;
847
- Thursday?: DayHours;
848
- Friday?: DayHours;
849
- Saturday?: DayHours;
850
- Sunday?: DayHours;
851
- };
852
- };
853
-
854
- type DayHours = {
855
- active?: boolean;
856
- start?: string; // Time string (e.g., "09:00")
857
- end?: string; // Time string (e.g., "21:00")
858
- };
859
- ```
860
-
861
- #### Product
862
-
863
- ```typescript
864
- type Product = {
865
- id: string;
866
- cname: string; // URL-friendly name
867
- name: string;
868
- description: string;
869
- image: string; // Primary image URL
870
- price: number;
871
- };
872
- ```
873
-
874
- #### Brand
875
-
876
- ```typescript
877
- type Brand = {
878
- id: string;
879
- cname: string; // URL-friendly name
880
- name: string;
881
- image?: string | null; // Brand logo URL
882
- };
883
- ```
884
-
885
- #### Category
886
-
887
- ```typescript
888
- type Category = {
889
- id: string;
890
- cname: string; // URL-friendly name (e.g., "edibles")
891
- name: string; // Display name (e.g., "Edibles")
892
- };
893
- ```
894
-
895
- #### Collection
896
-
897
- ```typescript
898
- type Collection = {
899
- id: string;
900
- cname: string; // URL-friendly name
901
- name: string;
902
- };
903
- ```
904
-
905
- #### Special
906
-
907
- ```typescript
908
- type Special = {
909
- id: string;
910
- cname: string;
911
- name: string;
912
- description: string;
913
- image: string;
914
- };
915
- ```
916
-
917
- #### Menu Context
918
-
919
- ```typescript
920
- type MenuContext = "store-front" | "kiosk";
921
- ```
922
-
923
- The menu context indicates which interface the extension is running in:
924
-
925
- - `'store-front'` - Online storefront for customers
926
- - `'kiosk'` - In-store kiosk interface
927
-
928
- #### Product Card Types
929
-
930
- The SDK provides types for building custom product card extensions. These types are passed as props to components registered as `ProductCard` (grid layout) and `ProductCardListItem` (list layout) in the module registry.
931
-
932
- ```typescript
933
- type ProductCardImage = {
934
- url: string;
935
- description?: string;
936
- };
937
-
938
- type ProductCardProductData = {
939
- id: string;
940
- name: string;
941
- brandName: string;
942
- href: string;
943
- strainType: string | null;
944
- potency: string[];
945
- images: ProductCardImage[];
946
- fallbackImageUrl: string;
947
- };
948
-
949
- type ProductCardBadgeData = {
950
- isStaffPick: boolean;
951
- isSponsored: boolean;
952
- isSpecialOffer: boolean;
953
- specialNames: string[];
954
- hideSalesLanguage: boolean;
955
- collectionBadge: {
956
- title: string;
957
- color: string | null;
958
- isExclusive: boolean;
959
- } | null;
960
- };
961
-
962
- type ProductCardPricingData = {
963
- displayPrice: string;
964
- standardPrice: string | null;
965
- discount: string | null;
966
- weight: string;
967
- showWeight: boolean;
968
- buttonCopy: string;
969
- };
970
-
971
- type ProductCardOptionData = {
972
- value: string;
973
- label: string;
974
- currentPrice: string;
975
- originalPrice: string | null;
976
- discountLabel: string | null;
977
- };
978
-
979
- type ProductCardOptionsData = {
980
- items: ProductCardOptionData[];
981
- totalCount: number;
982
- hasMultiple: boolean;
983
- shouldDisplayLabel: boolean;
984
- };
985
- ```
986
-
987
- **Grid Card Props** (passed to `ProductCard` extensions):
988
-
989
- ```typescript
990
- type ProductCardGridProps = {
991
- product: ProductCardProductData;
992
- pricing: ProductCardPricingData;
993
- options: ProductCardOptionsData;
994
- badges: ProductCardBadgeData;
995
- onLinkClick: () => void;
996
- onAddToCart: () => void;
997
- productIndex: number;
998
- };
999
- ```
1000
-
1001
- **List Card Props** (passed to `ProductCardListItem` extensions):
1002
-
1003
- ```typescript
1004
- type ProductCardListItemProps = {
1005
- product: ProductCardProductData;
1006
- pricing: ProductCardPricingData;
1007
- options: ProductCardOptionsData;
1008
- badges: ProductCardBadgeData;
1009
- onLinkClick: () => void;
1010
- onOptionClick: (value: string) => void;
1011
- productIndex: number;
1012
- };
1013
- ```
1014
-
1015
- ## Extension Development
1016
-
1017
- ### Creating Components
1018
-
1019
- All extension components should satisfy the `RemoteBoundaryComponent` type and set the `DataBridgeVersion` property.
1020
-
1021
- **Basic Component:**
1022
-
1023
- ```tsx
1024
- import {
1025
- RemoteBoundaryComponent,
1026
- DataBridgeVersion,
1027
- useDataBridge,
1028
- } from "@dutchiesdk/ecommerce-extensions-sdk";
1029
-
1030
- const Header: RemoteBoundaryComponent = () => {
1031
- const { location, user, actions } = useDataBridge();
1032
-
1033
- return (
1034
- <header>
1035
- <h1>{location?.name}</h1>
1036
- {user && <span>Welcome, {user.firstName}</span>}
1037
- </header>
1038
- );
1039
- };
1040
-
1041
- Header.DataBridgeVersion = DataBridgeVersion;
1042
-
1043
- export default Header;
1044
- ```
1045
-
1046
- **Component with Props:**
1047
-
1048
- ```tsx
1049
- import type { RemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1050
- import {
1051
- DataBridgeVersion,
1052
- useDataBridge,
1053
- } from "@dutchiesdk/ecommerce-extensions-sdk";
1054
-
1055
- interface CustomHeaderProps {
1056
- showLogo?: boolean;
1057
- className?: string;
1058
- }
1059
-
1060
- const CustomHeader: RemoteBoundaryComponent<CustomHeaderProps> = ({
1061
- showLogo = true,
1062
- className,
1063
- }) => {
1064
- const { location } = useDataBridge();
1065
-
1066
- return (
1067
- <header className={className}>
1068
- {showLogo && <img src={location?.images.logo} alt={location?.name} />}
1069
- <h1>{location?.name}</h1>
1070
- </header>
1071
- );
1072
- };
1073
-
1074
- CustomHeader.DataBridgeVersion = DataBridgeVersion;
1075
-
1076
- export default CustomHeader;
1077
- ```
1078
-
1079
- ### Module Registry
1080
-
1081
- The `RemoteModuleRegistry` type defines all available extension points in the Dutchie platform.
1082
-
1083
- ```typescript
1084
- type RemoteModuleRegistry = {
1085
- // UI Components
1086
- StoreFrontHeader?: ModuleRegistryEntry;
1087
- StoreFrontNavigation?: ModuleRegistryEntry;
1088
- StoreFrontFooter?: ModuleRegistryEntry;
1089
- StoreFrontHero?: ModuleRegistryEntry;
1090
- StoreFrontCarouselInterstitials?: ModuleRegistryEntry[];
1091
- ProductDetailsPrimary?: ModuleRegistryEntry;
1092
- ListPageHero?: ModuleRegistryEntry;
1093
-
1094
- // Custom product cards
1095
- ProductCard?: ModuleRegistryEntry;
1096
- ProductCardListItem?: ModuleRegistryEntry;
1097
-
1098
- // Category page components (indexed by pagination page number)
1099
- CategoryPageInterstitials?: ListPageEntry[];
1100
- CategoryPageSlots?: ListPageEntry[];
1101
-
1102
- // Custom routable pages
1103
- RouteablePages?: RoutablePageRegistryEntry[];
1104
-
1105
- // Metadata function
1106
- getStoreFrontMetaFields?: StoreFrontMetaFieldsFunction;
1107
-
1108
- // Event handlers
1109
- events?: Events;
1110
-
1111
- // Deprecated - use getStoreFrontMetaFields instead
1112
- StoreFrontMeta?: RemoteBoundaryComponent;
1113
- ProductDetailsMeta?: RemoteBoundaryComponent;
1114
- };
1115
-
1116
- type ModuleRegistryEntry =
1117
- | RemoteBoundaryComponent
1118
- | MenuSpecificRemoteComponent;
1119
-
1120
- type MenuSpecificRemoteComponent = {
1121
- "store-front"?: RemoteBoundaryComponent;
1122
- kiosk?: RemoteBoundaryComponent;
1123
- };
1124
-
1125
- type RoutablePageRegistryEntry = {
1126
- path: string;
1127
- component: RemoteBoundaryComponent;
1128
- };
1129
-
1130
- type ListPageCategory =
1131
- | "accessories"
1132
- | "apparel"
1133
- | "cbd"
1134
- | "clones"
1135
- | "concentrates"
1136
- | "edibles"
1137
- | "flower"
1138
- | "orals"
1139
- | "pre-rolls"
1140
- | "seeds"
1141
- | "tinctures"
1142
- | "topicals"
1143
- | "vaporizers"
1144
- | string;
1145
-
1146
- type ListPageEntry = {
1147
- category?: ListPageCategory;
1148
- components: RemoteBoundaryComponent[];
1149
- };
1150
- ```
1151
-
1152
- **Basic Registry:**
1153
-
1154
- ```tsx
1155
- import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1156
- import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1157
-
1158
- export default {
1159
- StoreFrontHeader: createLazyRemoteBoundaryComponent(() => import("./Header")),
1160
- StoreFrontFooter: createLazyRemoteBoundaryComponent(() => import("./Footer")),
1161
- } satisfies RemoteModuleRegistry;
1162
- ```
1163
-
1164
- **Menu-Specific Components:**
1165
-
1166
- ```tsx
1167
- import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1168
- import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1169
-
1170
- export default {
1171
- StoreFrontHeader: {
1172
- // Different header for storefront vs kiosk
1173
- "store-front": createLazyRemoteBoundaryComponent(
1174
- () => import("./StoreHeader")
1175
- ),
1176
- kiosk: createLazyRemoteBoundaryComponent(() => import("./KioskHeader")),
1177
- },
1178
- StoreFrontFooter: {
1179
- // Only override storefront, use default Dutchie footer for kiosk
1180
- "store-front": createLazyRemoteBoundaryComponent(() => import("./Footer")),
1181
- },
1182
- } satisfies RemoteModuleRegistry;
1183
- ```
1184
-
1185
- **Custom Routable Pages:**
1186
-
1187
- ```tsx
1188
- import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1189
- import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1190
-
1191
- export default {
1192
- RouteablePages: [
1193
- {
1194
- path: "/about",
1195
- component: createLazyRemoteBoundaryComponent(
1196
- () => import("./pages/About")
1197
- ),
1198
- },
1199
- {
1200
- path: "/contact",
1201
- component: createLazyRemoteBoundaryComponent(
1202
- () => import("./pages/Contact")
1203
- ),
1204
- },
1205
- ],
1206
- } satisfies RemoteModuleRegistry;
1207
- ```
1208
-
1209
- #### Category Page Components
1210
-
1211
- 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.
1212
-
1213
- **Type:**
1214
-
1215
- ```typescript
1216
- type ListPageCategory =
1217
- | "accessories"
1218
- | "apparel"
1219
- | "cbd"
1220
- | "clones"
1221
- | "concentrates"
1222
- | "edibles"
1223
- | "flower"
1224
- | "orals"
1225
- | "pre-rolls"
1226
- | "seeds"
1227
- | "tinctures"
1228
- | "topicals"
1229
- | "vaporizers"
1230
- | string; // Custom category names are also supported
1231
-
1232
- type ListPageEntry = {
1233
- category?: ListPageCategory; // Category to match, or undefined for fallback
1234
- components: RemoteBoundaryComponent[]; // Components indexed by page number
1235
- };
1236
- ```
1237
-
1238
- **Category Matching:**
1239
-
1240
- - **Predefined category**: When `category` matches a built-in category (e.g., `"edibles"`, `"flower"`), the components display on that category's listing page
1241
- - **Custom category string**: When `category` is a string that doesn't match a predefined category, it matches a custom category with that name
1242
- - **Fallback (undefined)**: When `category` is omitted, the entry serves as a fallback used when no other category-specific entries match
1243
-
1244
- **Page-Based Component Selection:**
1245
-
1246
- The `components` array is indexed by the current pagination page number:
1247
-
1248
- - `components[0]` displays on page 1
1249
- - `components[1]` displays on page 2
1250
- - And so on...
1251
-
1252
- If the current page number exceeds the array length, no component is displayed for that page.
1253
-
1254
- **Example:**
1255
-
1256
- ```tsx
1257
- import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1258
- import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1259
-
1260
- export default {
1261
- CategoryPageInterstitials: [
1262
- // Components for the "edibles" category
1263
- {
1264
- category: "edibles",
1265
- components: [
1266
- createLazyRemoteBoundaryComponent(
1267
- () => import("./interstitials/EdiblesPage1")
1268
- ),
1269
- createLazyRemoteBoundaryComponent(
1270
- () => import("./interstitials/EdiblesPage2")
1271
- ),
1272
- ],
1273
- },
1274
- // Components for a custom category
1275
- {
1276
- category: "limited-edition",
1277
- components: [
1278
- createLazyRemoteBoundaryComponent(
1279
- () => import("./interstitials/LimitedEditionPromo")
1280
- ),
1281
- ],
1282
- },
1283
- // Fallback for all other categories
1284
- {
1285
- // No category specified - this is the fallback
1286
- components: [
1287
- createLazyRemoteBoundaryComponent(
1288
- () => import("./interstitials/DefaultPage1")
1289
- ),
1290
- createLazyRemoteBoundaryComponent(
1291
- () => import("./interstitials/DefaultPage2")
1292
- ),
1293
- createLazyRemoteBoundaryComponent(
1294
- () => import("./interstitials/DefaultPage3")
1295
- ),
1296
- ],
1297
- },
1298
- ],
1299
-
1300
- CategoryPageSlots: [
1301
- // Slots specific to flower category
1302
- {
1303
- category: "flower",
1304
- components: [
1305
- createLazyRemoteBoundaryComponent(
1306
- () => import("./slots/FlowerFeatured")
1307
- ),
1308
- ],
1309
- },
1310
- // Fallback slots for all other categories
1311
- {
1312
- components: [
1313
- createLazyRemoteBoundaryComponent(
1314
- () => import("./slots/GenericPromo")
1315
- ),
1316
- ],
1317
- },
1318
- ],
1319
- } satisfies RemoteModuleRegistry;
1320
- ```
1321
-
1322
- #### Custom Product Cards
1323
-
1324
- Register custom product card components to replace the default grid and list cards across the storefront. The platform passes pre-computed product data, pricing, badges, and callbacks as props.
1325
-
1326
- **Grid Card:**
1327
-
1328
- ```tsx
1329
- import type {
1330
- RemoteBoundaryComponent,
1331
- ProductCardGridProps,
1332
- } from "@dutchiesdk/ecommerce-extensions-sdk";
1333
- import { DataBridgeVersion } from "@dutchiesdk/ecommerce-extensions-sdk";
1334
-
1335
- const CustomProductCard: RemoteBoundaryComponent<ProductCardGridProps> = ({
1336
- product,
1337
- pricing,
1338
- badges,
1339
- onLinkClick,
1340
- onAddToCart,
1341
- }) => {
1342
- return (
1343
- <div onClick={onLinkClick}>
1344
- <img
1345
- src={product.images[0]?.url || product.fallbackImageUrl}
1346
- alt={product.name}
1347
- />
1348
- {badges.isStaffPick && <span>Staff Pick</span>}
1349
- <h3>{product.name}</h3>
1350
- <p>{product.brandName}</p>
1351
- <p>{pricing.displayPrice}</p>
1352
- {pricing.discount && <span>{pricing.discount} off</span>}
1353
- <button
1354
- onClick={(e) => {
1355
- e.stopPropagation();
1356
- onAddToCart();
1357
- }}
1358
- >
1359
- {pricing.buttonCopy}
1360
- </button>
1361
- </div>
1362
- );
1363
- };
1364
-
1365
- CustomProductCard.DataBridgeVersion = DataBridgeVersion;
1366
- export default CustomProductCard;
1367
- ```
1368
-
1369
- **List Card:**
1370
-
1371
- ```tsx
1372
- import type {
1373
- RemoteBoundaryComponent,
1374
- ProductCardListItemProps,
1375
- } from "@dutchiesdk/ecommerce-extensions-sdk";
1376
- import { DataBridgeVersion } from "@dutchiesdk/ecommerce-extensions-sdk";
1377
-
1378
- const CustomListItem: RemoteBoundaryComponent<ProductCardListItemProps> = ({
1379
- product,
1380
- pricing,
1381
- options,
1382
- onLinkClick,
1383
- onOptionClick,
1384
- }) => {
1385
- return (
1386
- <div onClick={onLinkClick}>
1387
- <img
1388
- src={product.images[0]?.url || product.fallbackImageUrl}
1389
- alt={product.name}
1390
- />
1391
- <div>
1392
- <h3>{product.name}</h3>
1393
- <p>{product.brandName}</p>
1394
- {options.items.map((option) => (
1395
- <button
1396
- key={option.value}
1397
- onClick={(e) => {
1398
- e.stopPropagation();
1399
- onOptionClick(option.value);
1400
- }}
1401
- >
1402
- {option.label} — {option.currentPrice}
1403
- </button>
1404
- ))}
1405
- </div>
1406
- </div>
1407
- );
1408
- };
1409
-
1410
- CustomListItem.DataBridgeVersion = DataBridgeVersion;
1411
- export default CustomListItem;
1412
- ```
1413
-
1414
- **Registration:**
1415
-
1416
- ```tsx
1417
- import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1418
- import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1419
-
1420
- export default {
1421
- ProductCard: createLazyRemoteBoundaryComponent(
1422
- () => import("./components/CustomProductCard")
1423
- ),
1424
- ProductCardListItem: createLazyRemoteBoundaryComponent(
1425
- () => import("./components/CustomListItem")
1426
- ),
1427
- } satisfies RemoteModuleRegistry;
1428
- ```
1429
-
1430
- ### Meta Fields & SEO
1431
-
1432
- 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.
1433
-
1434
- > **🚀 Rollout Status:** The Dutchie platform is currently rolling out support for `getStoreFrontMetaFields` via a feature flag. During the transition period:
1435
- >
1436
- > - When the flag is **enabled**: Your `getStoreFrontMetaFields` function will be used (recommended)
1437
- > - When the flag is **disabled**: The legacy `StoreFrontMeta` component will be used
1438
- > - Both implementations can coexist in your theme during migration
1439
- > - The component approach will be deprecated once the rollout is complete
1440
-
1441
- #### Meta Fields Type
1442
-
1443
- ```typescript
1444
- type MetaFields = {
1445
- title?: string; // Page title
1446
- description?: string; // Meta description
1447
- ogImage?: string; // Open Graph image URL
1448
- canonical?: string; // Canonical URL
1449
- structuredData?: Record<string, any>; // JSON-LD structured data
1450
- customMeta?: Array<{
1451
- // Additional meta tags
1452
- name?: string;
1453
- property?: string;
1454
- content: string;
1455
- }>;
1456
- };
1457
-
1458
- type StoreFrontMetaFieldsFunction = (
1459
- data: CommerceComponentsDataInterface
1460
- ) => MetaFields | Promise<MetaFields>;
1461
- ```
1462
-
1463
- #### Implementation
1464
-
1465
- Create a meta fields function (e.g., `get-meta-fields.ts`):
1466
-
1467
- ```typescript
1468
- import type {
1469
- CommerceComponentsDataInterface,
1470
- MetaFields,
1471
- } from "@dutchiesdk/ecommerce-extensions-sdk";
1472
-
1473
- export const getStoreFrontMetaFields = async (
1474
- data: CommerceComponentsDataInterface
1475
- ): Promise<MetaFields> => {
1476
- const { location, dataLoaders } = data;
1477
- const pathname =
1478
- typeof window !== "undefined" ? window.location.pathname : "";
1479
-
1480
- // Load data for current page
1481
- const product = await dataLoaders.product();
1482
- const categories = await dataLoaders.categories();
1483
-
1484
- // Product detail page
1485
- if (product) {
1486
- return {
1487
- title: `${product.name} | ${location?.name}`,
1488
- description: product.description.substring(0, 155),
1489
- ogImage: product.image,
1490
- canonical: `${location?.links.storeFrontRoot}${pathname}`,
1491
- structuredData: {
1492
- "@context": "https://schema.org",
1493
- "@type": "Product",
1494
- name: product.name,
1495
- description: product.description,
1496
- image: product.image,
1497
- offers: {
1498
- "@type": "Offer",
1499
- price: product.price,
1500
- priceCurrency: "USD",
1501
- },
1502
- },
1503
- };
1504
- }
1505
-
1506
- // Category page
1507
- const categorySlug = pathname.split("/").pop();
1508
- const category = categories.find((c) => c.cname === categorySlug);
1509
- if (category) {
1510
- return {
1511
- title: `${category.name} | ${location?.name}`,
1512
- description: `Shop ${category.name} products at ${location?.name}`,
1513
- canonical: `${location?.links.storeFrontRoot}${pathname}`,
1514
- };
1515
- }
1516
-
1517
- // Home page
1518
- if (pathname === "/" || pathname === "") {
1519
- return {
1520
- title: `${location?.name} - Cannabis Dispensary`,
1521
- description: `Shop cannabis products online at ${location?.name}`,
1522
- ogImage: location?.images.logo,
1523
- canonical: location?.links.storeFrontRoot,
1524
- structuredData: {
1525
- "@context": "https://schema.org",
1526
- "@type": "Organization",
1527
- name: location?.name,
1528
- url: location?.links.storeFrontRoot,
1529
- logo: location?.images.logo,
1530
- },
1531
- customMeta: [
1532
- {
1533
- name: "robots",
1534
- content: "index, follow",
1535
- },
1536
- ],
1537
- };
1538
- }
1539
-
1540
- // Default fallback
1541
- return {
1542
- title: location?.name || "Shop Cannabis",
1543
- description: `Browse our selection at ${location?.name}`,
1544
- };
1545
- };
1546
- ```
1547
-
1548
- Register in your module registry:
1549
-
1550
- ```typescript
1551
- import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1552
- import { getStoreFrontMetaFields } from "./get-meta-fields";
1553
-
1554
- export default {
1555
- StoreFrontHeader: createLazyRemoteBoundaryComponent(() => import("./Header")),
1556
- StoreFrontFooter: createLazyRemoteBoundaryComponent(() => import("./Footer")),
1557
- getStoreFrontMetaFields,
1558
- } satisfies RemoteModuleRegistry;
1559
- ```
1560
-
1561
- ### Event Handlers
1562
-
1563
- Register callbacks that are triggered by platform events.
1564
-
1565
- ```typescript
1566
- type Events = {
1567
- onAfterCheckout?: (data: OnAfterCheckoutData) => void;
1568
- };
1569
-
1570
- type OnAfterCheckoutData = {
1571
- orderNumber: string;
1572
- };
1573
- ```
1574
-
1575
- **Example:**
1576
-
1577
- ```typescript
1578
- import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1579
-
1580
- export default {
1581
- // ... components
1582
-
1583
- events: {
1584
- onAfterCheckout: (data) => {
1585
- console.log("Order completed:", data.orderNumber);
1586
-
1587
- // Track conversion in analytics
1588
- gtag("event", "purchase", {
1589
- transaction_id: data.orderNumber,
1590
- });
1591
- },
1592
- },
1593
- } satisfies RemoteModuleRegistry;
1594
- ```
1595
-
1596
- ## Best Practices
1597
-
1598
- ### Always Handle Loading States
1599
-
1600
- When using data loaders, always handle loading and empty states:
1601
-
1602
- ```tsx
1603
- const ProductList = () => {
1604
- const { dataLoaders } = useDataBridge();
1605
- const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
1606
-
1607
- if (isLoading) {
1608
- return <LoadingSpinner />;
1609
- }
1610
-
1611
- if (!products || products.length === 0) {
1612
- return <EmptyState message="No products available" />;
1613
- }
1614
-
1615
- return (
1616
- <div>
1617
- {products.map((product) => (
1618
- <ProductCard key={product.id} product={product} />
1619
- ))}
1620
- </div>
1621
- );
1622
- };
1623
- ```
1624
-
1625
- ### Check Data Availability
1626
-
1627
- Always check if optional data is available before using it:
1628
-
1629
- ```tsx
1630
- const UserProfile = () => {
1631
- const { user, actions } = useDataBridge();
1632
-
1633
- if (!user) {
1634
- return (
1635
- <div className="login-prompt">
1636
- <p>Please log in to view your profile</p>
1637
- <button onClick={actions.goToLogin}>Login</button>
1638
- </div>
1639
- );
1640
- }
1641
-
1642
- return (
1643
- <div className="user-profile">
1644
- <h2>Welcome, {user.firstName}!</h2>
1645
- <p>Email: {user.email}</p>
1646
- </div>
1647
- );
1648
- };
1649
- ```
1650
-
1651
- ### Use TypeScript for Better DX
1652
-
1653
- Leverage the provided types for autocomplete and type safety:
1654
-
1655
- ```tsx
1656
- import type {
1657
- Product,
1658
- Category,
1659
- RemoteBoundaryComponent,
1660
- } from "@dutchiesdk/ecommerce-extensions-sdk";
1661
-
1662
- interface ProductFilterProps {
1663
- products: Product[];
1664
- categories: Category[];
1665
- onFilterChange: (categoryId: string) => void;
1666
- }
1667
-
1668
- const ProductFilter: React.FC<ProductFilterProps> = ({
1669
- products,
1670
- categories,
1671
- onFilterChange,
1672
- }) => {
1673
- return (
1674
- <div>
1675
- {categories.map((category) => (
1676
- <button key={category.id} onClick={() => onFilterChange(category.id)}>
1677
- {category.name}
1678
- </button>
1679
- ))}
1680
- </div>
1681
- );
1682
- };
1683
- ```
1684
-
1685
- ### Error Boundaries
1686
-
1687
- Any uncaught errors in your extension will be caught by Dutchie's error boundary. However, you should still handle expected errors gracefully:
1688
-
1689
- ```tsx
1690
- const ProductDetails = () => {
1691
- const { dataLoaders } = useDataBridge();
1692
- const [error, setError] = useState<string | null>(null);
1693
- const { data: product, isLoading } = useAsyncLoader(dataLoaders.product);
1694
-
1695
- if (error) {
1696
- return <ErrorMessage message={error} />;
1697
- }
1698
-
1699
- if (isLoading) {
1700
- return <LoadingSpinner />;
1701
- }
1702
-
1703
- if (!product) {
1704
- return <NotFound message="Product not found" />;
1705
- }
1706
-
1707
- return <ProductCard product={product} />;
1708
- };
1709
- ```
1710
-
1711
- ### Optimize Performance
1712
-
1713
- Use lazy loading for large components and routes:
1714
-
1715
- ```tsx
1716
- import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1717
-
1718
- // Components will be code-split and loaded on demand
1719
- export default {
1720
- StoreFrontHeader: createLazyRemoteBoundaryComponent(
1721
- () => import("./components/Header"),
1722
- { fallback: <HeaderSkeleton /> }
1723
- ),
1724
- StoreFrontFooter: createLazyRemoteBoundaryComponent(
1725
- () => import("./components/Footer"),
1726
- { fallback: <FooterSkeleton /> }
1727
- ),
1728
- } satisfies RemoteModuleRegistry;
1729
- ```
1730
-
1731
- ### Testing Components
1732
-
1733
- Test your components using the `DataBridgeContext` provider:
1734
-
1735
- ```tsx
1736
- import { render, screen } from "@testing-library/react";
1737
- import { DataBridgeContext } from "@dutchiesdk/ecommerce-extensions-sdk";
1738
- import MyComponent from "./MyComponent";
1739
-
1740
- const mockDataBridge = {
1741
- menuContext: "store-front" as const,
1742
- location: {
1743
- id: "1",
1744
- name: "Test Dispensary",
1745
- cname: "test-dispensary",
1746
- // ... other required fields
1747
- },
1748
- dataLoaders: {
1749
- products: jest.fn().mockResolvedValue([]),
1750
- categories: jest.fn().mockResolvedValue([]),
1751
- brands: jest.fn().mockResolvedValue([]),
1752
- collections: jest.fn().mockResolvedValue([]),
1753
- specials: jest.fn().mockResolvedValue([]),
1754
- locations: jest.fn().mockResolvedValue([]),
1755
- product: jest.fn().mockResolvedValue(null),
1756
- integrationValue: jest.fn().mockResolvedValue(undefined),
1757
- },
1758
- actions: {
1759
- addToCart: jest.fn(),
1760
- goToProductDetails: jest.fn(),
1761
- goToCategory: jest.fn(),
1762
- // ... other actions
1763
- },
1764
- };
1765
-
1766
- test("renders component correctly", () => {
1767
- render(
1768
- <DataBridgeContext.Provider value={mockDataBridge}>
1769
- <MyComponent />
1770
- </DataBridgeContext.Provider>
1771
- );
1772
-
1773
- expect(screen.getByText("Test Dispensary")).toBeInTheDocument();
1774
- });
1775
-
1776
- test("handles cart actions", async () => {
1777
- render(
1778
- <DataBridgeContext.Provider value={mockDataBridge}>
1779
- <MyComponent />
1780
- </DataBridgeContext.Provider>
1781
- );
1782
-
1783
- const addButton = screen.getByRole("button", { name: /add to cart/i });
1784
- addButton.click();
1785
-
1786
- expect(mockDataBridge.actions.addToCart).toHaveBeenCalledWith({
1787
- productId: "123",
1788
- name: "Test Product",
1789
- price: 25.0,
1790
- quantity: 1,
1791
- });
1792
- });
1793
- ```
1794
-
1795
- ## Examples
1796
-
1797
- ### Custom Product Listing
1798
-
1799
- ```tsx
1800
- import {
1801
- RemoteBoundaryComponent,
1802
- DataBridgeVersion,
1803
- useDataBridge,
1804
- useAsyncLoader,
1805
- type Product,
1806
- } from "@dutchiesdk/ecommerce-extensions-sdk";
1807
-
1808
- const ProductListing: RemoteBoundaryComponent = () => {
1809
- const { dataLoaders, actions } = useDataBridge();
1810
- const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
1811
- const { data: categories } = useAsyncLoader(dataLoaders.categories);
1812
- const [filter, setFilter] = useState<string | null>(null);
1813
-
1814
- const filteredProducts = products?.filter(
1815
- (p) => !filter || p.categoryId === filter
1816
- );
1817
-
1818
- if (isLoading) {
1819
- return <div className="loading">Loading products...</div>;
1820
- }
1821
-
1822
- return (
1823
- <div className="product-listing">
1824
- <div className="filters">
1825
- <button onClick={() => setFilter(null)}>All</button>
1826
- {categories?.map((cat) => (
1827
- <button key={cat.id} onClick={() => setFilter(cat.id)}>
1828
- {cat.name}
1829
- </button>
1830
- ))}
1831
- </div>
1832
-
1833
- <div className="products-grid">
1834
- {filteredProducts?.map((product) => (
1835
- <div
1836
- key={product.id}
1837
- className="product-card"
1838
- onClick={() => actions.goToProductDetails({ id: product.id })}
1839
- >
1840
- <img src={product.image} alt={product.name} />
1841
- <h3>{product.name}</h3>
1842
- <p>${product.price.toFixed(2)}</p>
1843
- <button
1844
- onClick={(e) => {
1845
- e.stopPropagation();
1846
- actions.addToCart({
1847
- productId: product.id,
1848
- name: product.name,
1849
- price: product.price,
1850
- quantity: 1,
1851
- });
1852
- }}
1853
- >
1854
- Add to Cart
1855
- </button>
1856
- </div>
1857
- ))}
1858
- </div>
1859
- </div>
1860
- );
1861
- };
1862
-
1863
- ProductListing.DataBridgeVersion = DataBridgeVersion;
1864
-
1865
- export default ProductListing;
1866
- ```
1867
-
1868
- ### Custom Header with Cart
1869
-
1870
- ```tsx
1871
- import {
1872
- RemoteBoundaryComponent,
1873
- DataBridgeVersion,
1874
- useDataBridge,
1875
- } from "@dutchiesdk/ecommerce-extensions-sdk";
1876
-
1877
- const Header: RemoteBoundaryComponent = () => {
1878
- const { location, user, cart, actions } = useDataBridge();
1879
-
1880
- const cartItemCount =
1881
- cart?.items.reduce((sum, item) => sum + item.quantity, 0) || 0;
1882
-
1883
- return (
1884
- <header className="site-header">
1885
- <div className="logo" onClick={() => actions.goToStoreFront()}>
1886
- <img src={location?.images.logo} alt={location?.name} />
1887
- <h1>{location?.name}</h1>
1888
- </div>
1889
-
1890
- <nav>
1891
- <button onClick={() => actions.goToProductList({})}>Shop All</button>
1892
- <button onClick={() => actions.goToSpecialsList()}>Deals</button>
1893
- <button onClick={() => actions.goToStoreLocator()}>Locations</button>
1894
- </nav>
1895
-
1896
- <div className="user-actions">
1897
- {user ? (
1898
- <>
1899
- <span>Hello, {user.firstName}</span>
1900
- <button onClick={() => actions.goToLoyalty()}>Rewards</button>
1901
- </>
1902
- ) : (
1903
- <>
1904
- <button onClick={actions.goToLogin}>Login</button>
1905
- <button onClick={actions.goToRegister}>Sign Up</button>
1906
- </>
1907
- )}
1908
-
1909
- <button className="cart-button" onClick={actions.showCart}>
1910
- Cart ({cartItemCount})
1911
- </button>
1912
- </div>
1913
- </header>
1914
- );
1915
- };
1916
-
1917
- Header.DataBridgeVersion = DataBridgeVersion;
1918
-
1919
- export default Header;
1920
- ```
1921
-
1922
- ### Complete Module Registry Example
1923
-
1924
- ```tsx
1925
- import type { RemoteModuleRegistry } from "@dutchiesdk/ecommerce-extensions-sdk";
1926
- import { createLazyRemoteBoundaryComponent } from "@dutchiesdk/ecommerce-extensions-sdk";
1927
- import { getStoreFrontMetaFields } from "./get-meta-fields";
1928
-
1929
- export default {
1930
- // Header and footer
1931
- StoreFrontHeader: createLazyRemoteBoundaryComponent(
1932
- () => import("./components/Header"),
1933
- { fallback: <div>Loading...</div> }
1934
- ),
1935
-
1936
- StoreFrontFooter: createLazyRemoteBoundaryComponent(
1937
- () => import("./components/Footer")
1938
- ),
1939
-
1940
- // Hero section
1941
- StoreFrontHero: createLazyRemoteBoundaryComponent(
1942
- () => import("./components/Hero")
1943
- ),
1944
-
1945
- // Hero on Product List pages
1946
- ListPageHero: createLazyRemoteBoundaryComponent(
1947
- () => import("./store-front/list-page-hero")
1948
- ),
1949
-
1950
- // Custom product page
1951
- ProductDetailsPrimary: createLazyRemoteBoundaryComponent(
1952
- () => import("./components/ProductDetails")
1953
- ),
1954
-
1955
- // Custom routable pages
1956
- RouteablePages: [
1957
- {
1958
- path: "/about",
1959
- component: createLazyRemoteBoundaryComponent(
1960
- () => import("./pages/About")
1961
- ),
1962
- },
1963
- {
1964
- path: "/contact",
1965
- component: createLazyRemoteBoundaryComponent(
1966
- () => import("./pages/Contact")
1967
- ),
1968
- },
1969
- ],
1970
-
1971
- // Meta fields for SEO
1972
- getStoreFrontMetaFields,
1973
-
1974
- // Event handlers
1975
- events: {
1976
- onAfterCheckout: (data) => {
1977
- // Track conversion
1978
- console.log("Order completed:", data.orderNumber);
1979
-
1980
- // Send to analytics
1981
- if (typeof gtag !== "undefined") {
1982
- gtag("event", "purchase", {
1983
- transaction_id: data.orderNumber,
1984
- });
1985
- }
1986
- },
1987
- },
1988
- } satisfies RemoteModuleRegistry;
1989
- ```
1990
-
1991
- ## Support
1992
-
1993
- For technical support and questions about the Dutchie Ecommerce Extensions SDK:
1994
-
1995
- - 📧 Contact your Dutchie agency partner representative
1996
- - 📚 Refer to the Dutchie Pro platform documentation
1997
- - 🐛 Report issues through the official Dutchie support channels
1998
-
1999
- ## Updates
2000
-
2001
- Stay up to date with this SDK by checking the repository for changes and updating to the latest version with:
2002
-
2003
- ```bash
2004
- npm install @dutchiesdk/ecommerce-extensions-sdk@latest
2005
- # or
2006
- pnpm update @dutchiesdk/ecommerce-extensions-sdk@latest
2007
- ```
76
+ - 📧 Contact your Dutchie agency partner representative, or **partners@dutchie.com**
77
+ - 📚 [docs.dutchie.com](https://docs.dutchie.com/dutchie-core-docs/dutchie-ecommerce-pro-sdk)
2008
78
 
2009
79
  ## License
2010
80
 
2011
- MIT
2012
-
2013
- ---
2014
-
2015
- **Made with 💚 by Dutchie**
81
+ MIT — Made with 💚 by Dutchie