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