@dutchiesdk/ecommerce-extensions-sdk 0.32.0 → 0.32.2
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
|
-

|
|
2
|
-
|
|
3
1
|
# @dutchiesdk/ecommerce-extensions-sdk
|
|
4
2
|
|
|
5
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@dutchiesdk/ecommerce-extensions-sdk)
|
|
34
4
|
|
|
35
|
-
|
|
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
|
-
|
|
10
|
+
> **Alpha Release** — This SDK is currently in alpha and subject to breaking changes.
|
|
38
11
|
|
|
39
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
709
|
-
const { dataLoaders } = useDataBridge();
|
|
68
|
+
## Changelog
|
|
710
69
|
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
715
|
-
const { data: categories } = useAsyncLoader(dataLoaders.categories);
|
|
74
|
+
## Support
|
|
716
75
|
|
|
717
|
-
|
|
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
|