@dutchiesdk/ecommerce-extensions-sdk 0.6.3 → 0.8.0
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 +99 -632
- package/package.json +8 -4
- package/dist/context/ecommerce-data-bridge.cjs +0 -64
- package/dist/context/ecommerce-data-bridge.d.ts +0 -8
- package/dist/esm/context/ecommerce-data-bridge.d.ts +0 -8
- package/dist/esm/context/ecommerce-data-bridge.js +0 -21
- package/dist/esm/index.d.ts +0 -3
- package/dist/esm/index.js +0 -3
- package/dist/esm/types/dutchie-data-bridge.d.ts +0 -162
- package/dist/esm/types/dutchie-data-bridge.js +0 -0
- package/dist/esm/types/ecommerce-extension.d.ts +0 -17
- package/dist/esm/types/ecommerce-extension.js +0 -1
- package/dist/index.cjs +0 -78
- package/dist/index.d.ts +0 -3
- package/dist/types/dutchie-data-bridge.cjs +0 -18
- package/dist/types/dutchie-data-bridge.d.ts +0 -162
- package/dist/types/ecommerce-extension.cjs +0 -7
- package/dist/types/ecommerce-extension.d.ts +0 -17
package/README.md
CHANGED
|
@@ -1,37 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|

|
|
4
2
|
|
|
5
|
-
# @dutchiesdk/ecommerce-extensions-sdk
|
|
3
|
+
# @dutchiesdk/ecommerce-extensions-sdk
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
This SDK provides certified agency partners with the tools needed to create powerful, integrated e-commerce experiences for cannabis retailers on the Dutchie Ecommerce Pro platform.
|
|
8
6
|
|
|
9
7
|
> ⚠️ **Alpha Release Warning**
|
|
10
|
-
>
|
|
8
|
+
>
|
|
11
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.
|
|
12
10
|
|
|
13
11
|
## Development Environment Access
|
|
14
12
|
|
|
15
13
|
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.
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
📧 Contact: **partners@dutchie.com**
|
|
19
|
-
|
|
20
|
-
Please include your agency information and intended use case when requesting access.
|
|
21
|
-
|
|
22
|
-
## Table of Contents
|
|
23
|
-
|
|
24
|
-
- [Installation](#installation)
|
|
25
|
-
- [Quick Start](#quick-start)
|
|
26
|
-
- [Core Concepts](#core-concepts)
|
|
27
|
-
- [React Context & Hooks](#react-context--hooks)
|
|
28
|
-
- [Data Types Reference](#data-types-reference)
|
|
29
|
-
- [Extension Components](#extension-components)
|
|
30
|
-
- [Actions API](#actions-api)
|
|
31
|
-
- [Data Loaders](#data-loaders)
|
|
32
|
-
- [Examples](#examples)
|
|
33
|
-
- [Best Practices](#best-practices)
|
|
34
|
-
- [Development](#development)
|
|
15
|
+
To request access to the development environment contact: **partners@dutchie.com**. Please include your agency information and intended use case when requesting access.
|
|
35
16
|
|
|
36
17
|
## Installation
|
|
37
18
|
|
|
@@ -39,19 +20,21 @@ Please include your agency information and intended use case when requesting acc
|
|
|
39
20
|
npm install @dutchie/ecommerce-extensions-sdk
|
|
40
21
|
# or
|
|
41
22
|
yarn add @dutchie/ecommerce-extensions-sdk
|
|
42
|
-
# or
|
|
43
|
-
pnpm add @dutchie/ecommerce-extensions-sdk
|
|
44
23
|
```
|
|
45
24
|
|
|
46
25
|
## Quick Start
|
|
47
26
|
|
|
48
27
|
```tsx
|
|
49
|
-
import React from
|
|
50
|
-
import {
|
|
28
|
+
import React from "react";
|
|
29
|
+
import {
|
|
30
|
+
useDataBridge,
|
|
31
|
+
RemoteBoundaryComponent,
|
|
32
|
+
DataBridgeVersion,
|
|
33
|
+
} from "@dutchie/ecommerce-extensions-sdk";
|
|
51
34
|
|
|
52
35
|
const MyExtension: RemoteBoundaryComponent = () => {
|
|
53
36
|
const { dataLoaders, actions, location, user, cart } = useDataBridge();
|
|
54
|
-
|
|
37
|
+
|
|
55
38
|
return (
|
|
56
39
|
<div>
|
|
57
40
|
<h1>Welcome to {location?.name}</h1>
|
|
@@ -60,20 +43,11 @@ const MyExtension: RemoteBoundaryComponent = () => {
|
|
|
60
43
|
);
|
|
61
44
|
};
|
|
62
45
|
|
|
63
|
-
MyExtension.DataBridgeVersion =
|
|
46
|
+
MyExtension.DataBridgeVersion = DataBridgeVersion;
|
|
64
47
|
|
|
65
48
|
export default MyExtension;
|
|
66
49
|
```
|
|
67
50
|
|
|
68
|
-
## Core Concepts
|
|
69
|
-
|
|
70
|
-
The Dutchie Ecommerce Extensions SDK is built around several key concepts:
|
|
71
|
-
|
|
72
|
-
1. **Data Bridge**: A unified interface for accessing dispensary data, user information, and cart state
|
|
73
|
-
2. **Actions**: Pre-built navigation and cart management functions
|
|
74
|
-
3. **Remote Components**: Extension components that integrate seamlessly with the Dutchie platform
|
|
75
|
-
4. **Context-Driven Architecture**: React context provides data and actions throughout your extension
|
|
76
|
-
|
|
77
51
|
## React Context & Hooks
|
|
78
52
|
|
|
79
53
|
### `useDataBridge()`
|
|
@@ -81,18 +55,18 @@ The Dutchie Ecommerce Extensions SDK is built around several key concepts:
|
|
|
81
55
|
The primary hook for accessing the Dutchie platform data and functionality.
|
|
82
56
|
|
|
83
57
|
```tsx
|
|
84
|
-
import { useDataBridge } from
|
|
58
|
+
import { useDataBridge } from "@dutchie/ecommerce-extensions-sdk";
|
|
85
59
|
|
|
86
60
|
const MyComponent = () => {
|
|
87
61
|
const {
|
|
88
|
-
menuContext,
|
|
89
|
-
location,
|
|
90
|
-
user,
|
|
91
|
-
cart,
|
|
92
|
-
dataLoaders,
|
|
93
|
-
actions
|
|
62
|
+
menuContext, // 'store-front' | 'kiosk'
|
|
63
|
+
location, // Current dispensary information
|
|
64
|
+
user, // Authenticated user data
|
|
65
|
+
cart, // Current cart state
|
|
66
|
+
dataLoaders, // Async data loading functions
|
|
67
|
+
actions, // Navigation and cart actions
|
|
94
68
|
} = useDataBridge();
|
|
95
|
-
|
|
69
|
+
|
|
96
70
|
// Component logic here
|
|
97
71
|
};
|
|
98
72
|
```
|
|
@@ -104,17 +78,20 @@ const MyComponent = () => {
|
|
|
104
78
|
A utility hook for handling async data loading with loading states.
|
|
105
79
|
|
|
106
80
|
```tsx
|
|
107
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
useAsyncLoader,
|
|
83
|
+
useDataBridge,
|
|
84
|
+
} from "@dutchie/ecommerce-extensions-sdk";
|
|
108
85
|
|
|
109
86
|
const ProductList = () => {
|
|
110
87
|
const { dataLoaders } = useDataBridge();
|
|
111
88
|
const { data: products, isLoading } = useAsyncLoader(dataLoaders.products);
|
|
112
|
-
|
|
89
|
+
|
|
113
90
|
if (isLoading) return <div>Loading products...</div>;
|
|
114
|
-
|
|
91
|
+
|
|
115
92
|
return (
|
|
116
93
|
<div>
|
|
117
|
-
{products?.map(product => (
|
|
94
|
+
{products?.map((product) => (
|
|
118
95
|
<div key={product.id}>{product.name}</div>
|
|
119
96
|
))}
|
|
120
97
|
</div>
|
|
@@ -122,24 +99,15 @@ const ProductList = () => {
|
|
|
122
99
|
};
|
|
123
100
|
```
|
|
124
101
|
|
|
125
|
-
|
|
126
|
-
- `fn`: `() => Promise<unknown>` - Async function to execute
|
|
102
|
+
## Data Types
|
|
127
103
|
|
|
128
|
-
|
|
129
|
-
- `data`: The resolved data or `null` while loading
|
|
130
|
-
- `isLoading`: Boolean indicating loading state
|
|
131
|
-
|
|
132
|
-
## Data Types Reference
|
|
133
|
-
|
|
134
|
-
### Core Platform Types
|
|
135
|
-
|
|
136
|
-
#### `CommerceComponentsDataInterface`
|
|
104
|
+
### `CommerceComponentsDataInterface`
|
|
137
105
|
|
|
138
106
|
The main interface providing access to all platform data and functionality.
|
|
139
107
|
|
|
140
108
|
```typescript
|
|
141
109
|
interface CommerceComponentsDataInterface {
|
|
142
|
-
menuContext:
|
|
110
|
+
menuContext: "store-front" | "kiosk";
|
|
143
111
|
location?: Dispensary;
|
|
144
112
|
user?: User;
|
|
145
113
|
cart?: Cart;
|
|
@@ -148,159 +116,6 @@ interface CommerceComponentsDataInterface {
|
|
|
148
116
|
}
|
|
149
117
|
```
|
|
150
118
|
|
|
151
|
-
#### `Dispensary`
|
|
152
|
-
|
|
153
|
-
Complete dispensary information including location, hours, and capabilities.
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
interface Dispensary {
|
|
157
|
-
id: string;
|
|
158
|
-
status: string;
|
|
159
|
-
name: string;
|
|
160
|
-
cname: string; // URL-friendly name
|
|
161
|
-
chain: string;
|
|
162
|
-
phone: string;
|
|
163
|
-
email: string;
|
|
164
|
-
hours: DispensaryHoursSettings;
|
|
165
|
-
orderTypes: DispensaryOrderTypes;
|
|
166
|
-
images: {
|
|
167
|
-
logo: string;
|
|
168
|
-
};
|
|
169
|
-
address: DispensaryAddress;
|
|
170
|
-
links: {
|
|
171
|
-
website: string;
|
|
172
|
-
storeFrontRoot: string;
|
|
173
|
-
};
|
|
174
|
-
orderTypesConfig: OrderTypesConfigV2;
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
#### `Product`
|
|
179
|
-
|
|
180
|
-
Product information with essential e-commerce data.
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
interface Product {
|
|
184
|
-
id: string;
|
|
185
|
-
name: string;
|
|
186
|
-
cname: string; // URL-friendly name
|
|
187
|
-
price: number;
|
|
188
|
-
image: string;
|
|
189
|
-
description: string;
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
#### `User`
|
|
194
|
-
|
|
195
|
-
Authenticated user information.
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
interface User {
|
|
199
|
-
email: string;
|
|
200
|
-
firstName: string;
|
|
201
|
-
lastName: string;
|
|
202
|
-
birthday: string;
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
#### `Cart`
|
|
207
|
-
|
|
208
|
-
Current shopping cart state and totals.
|
|
209
|
-
|
|
210
|
-
```typescript
|
|
211
|
-
interface Cart {
|
|
212
|
-
items: CartItem[];
|
|
213
|
-
total: number;
|
|
214
|
-
subtotal: number;
|
|
215
|
-
tax: number;
|
|
216
|
-
discount: number;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
interface CartItem {
|
|
220
|
-
productId: string;
|
|
221
|
-
name: string;
|
|
222
|
-
price: number;
|
|
223
|
-
quantity: number;
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### Category & Brand Types
|
|
228
|
-
|
|
229
|
-
#### `Category`
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
interface Category {
|
|
233
|
-
id: string;
|
|
234
|
-
name: string;
|
|
235
|
-
cname: string;
|
|
236
|
-
}
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
#### `Brand`
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
interface Brand {
|
|
243
|
-
id: string;
|
|
244
|
-
name: string;
|
|
245
|
-
cname: string;
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
#### `Collection`
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
interface Collection {
|
|
253
|
-
id: string;
|
|
254
|
-
name: string;
|
|
255
|
-
cname: string;
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### Business Hours & Order Types
|
|
260
|
-
|
|
261
|
-
#### `DispensaryHoursSettings`
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
interface DispensaryHoursSettings {
|
|
265
|
-
inStorePickup?: HoursSettingsForOrderType;
|
|
266
|
-
curbsidePickup?: HoursSettingsForOrderType;
|
|
267
|
-
driveThruPickup?: HoursSettingsForOrderType;
|
|
268
|
-
delivery?: HoursSettingsForOrderType;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
interface HoursSettingsForOrderType {
|
|
272
|
-
enabled: boolean;
|
|
273
|
-
effectiveHours?: {
|
|
274
|
-
Monday?: DayHours;
|
|
275
|
-
Tuesday?: DayHours;
|
|
276
|
-
Wednesday?: DayHours;
|
|
277
|
-
Thursday?: DayHours;
|
|
278
|
-
Friday?: DayHours;
|
|
279
|
-
Saturday?: DayHours;
|
|
280
|
-
Sunday?: DayHours;
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
interface DayHours {
|
|
285
|
-
active?: boolean;
|
|
286
|
-
start?: string; // "09:00"
|
|
287
|
-
end?: string; // "21:00"
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
#### `DispensaryOrderTypes`
|
|
292
|
-
|
|
293
|
-
```typescript
|
|
294
|
-
interface DispensaryOrderTypes {
|
|
295
|
-
pickup: boolean;
|
|
296
|
-
inStorePickup: boolean;
|
|
297
|
-
curbsidePickup: boolean;
|
|
298
|
-
driveThruPickup: boolean;
|
|
299
|
-
delivery: boolean;
|
|
300
|
-
kiosk: boolean;
|
|
301
|
-
}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
119
|
## Extension Components
|
|
305
120
|
|
|
306
121
|
### `RemoteBoundaryComponent`
|
|
@@ -318,7 +133,7 @@ type RemoteBoundaryComponent = React.FC & {
|
|
|
318
133
|
```tsx
|
|
319
134
|
const MyCustomHeader: RemoteBoundaryComponent = () => {
|
|
320
135
|
const { location, user, actions } = useDataBridge();
|
|
321
|
-
|
|
136
|
+
|
|
322
137
|
return (
|
|
323
138
|
<header>
|
|
324
139
|
<h1>{location?.name}</h1>
|
|
@@ -331,75 +146,31 @@ const MyCustomHeader: RemoteBoundaryComponent = () => {
|
|
|
331
146
|
);
|
|
332
147
|
};
|
|
333
148
|
|
|
334
|
-
MyCustomHeader.DataBridgeVersion =
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### `RemoteModuleRegistry`
|
|
338
|
-
|
|
339
|
-
Registry for different types of extension components.
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
342
|
-
interface RemoteModuleRegistry {
|
|
343
|
-
RouteablePages?: RoutablePageRegistryEntry[];
|
|
344
|
-
StoreFrontHeader?: RemoteBoundaryComponent;
|
|
345
|
-
StoreFrontNavigation?: RemoteBoundaryComponent;
|
|
346
|
-
StoreFrontFooter?: RemoteBoundaryComponent;
|
|
347
|
-
StoreFrontHero?: RemoteBoundaryComponent;
|
|
348
|
-
ProductDetailsPrimary?: RemoteBoundaryComponent;
|
|
349
|
-
ProductDetailsSecondary?: RemoteBoundaryComponent;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
interface RoutablePageRegistryEntry {
|
|
353
|
-
path: string;
|
|
354
|
-
component: RemoteBoundaryComponent;
|
|
355
|
-
}
|
|
149
|
+
MyCustomHeader.DataBridgeVersion = "0.2";
|
|
356
150
|
```
|
|
357
151
|
|
|
358
152
|
## Actions API
|
|
359
153
|
|
|
360
154
|
The actions object provides pre-built functions for common e-commerce operations.
|
|
361
155
|
|
|
362
|
-
|
|
156
|
+
It can be used to route:
|
|
363
157
|
|
|
364
158
|
```typescript
|
|
365
159
|
const { actions } = useDataBridge();
|
|
366
160
|
|
|
367
|
-
//
|
|
368
|
-
actions.goToInfoPage(); // Navigate to store info
|
|
369
|
-
actions.goToStoreFront(); // Navigate to store front
|
|
370
|
-
actions.goToStoreLocator(); // Navigate to store locator
|
|
371
|
-
actions.goToStoreBrowser(); // Navigate to store browser
|
|
372
|
-
|
|
373
|
-
// Authentication
|
|
374
|
-
actions.goToLogin(); // Navigate to login page
|
|
375
|
-
actions.goToRegister(); // Navigate to registration
|
|
161
|
+
actions.goToLogin(); // Navigate to login page
|
|
376
162
|
|
|
377
163
|
// Product & category navigation
|
|
378
|
-
actions.goToProductDetails(
|
|
379
|
-
actions.goToCategory('flower'); // Navigate to category page
|
|
380
|
-
actions.goToBrand('brand-456'); // Navigate to brand page
|
|
381
|
-
actions.goToStore('store-789', 'dispensary-name'); // Navigate to specific store
|
|
382
|
-
actions.goToProductList('category-id', 'category-name'); // Navigate to product list
|
|
383
|
-
actions.goToBrandList('brand-id', 'brand-name'); // Navigate to brand list
|
|
384
|
-
|
|
385
|
-
// Checkout
|
|
386
|
-
actions.goToCheckout(); // Navigate to checkout
|
|
164
|
+
actions.goToProductDetails("product-123"); // Navigate to product details
|
|
387
165
|
```
|
|
388
166
|
|
|
389
|
-
|
|
167
|
+
Or to update state:
|
|
390
168
|
|
|
391
169
|
```typescript
|
|
392
170
|
const { actions } = useDataBridge();
|
|
393
171
|
|
|
394
|
-
//
|
|
395
|
-
actions.
|
|
396
|
-
actions.hideCart(); // Hide cart sidebar/modal
|
|
397
|
-
|
|
398
|
-
// Cart management
|
|
399
|
-
actions.addToCart('product-123', 2); // Add 2 items to cart
|
|
400
|
-
actions.removeFromCart('product-123'); // Remove product from cart
|
|
401
|
-
actions.updateCartItem('product-123', 5); // Update quantity to 5
|
|
402
|
-
actions.clearCart(); // Remove all items from cart
|
|
172
|
+
actions.addToCart("product-123", 2); // Add 2 items to cart
|
|
173
|
+
actions.clearCart(); // Remove all items from cart
|
|
403
174
|
```
|
|
404
175
|
|
|
405
176
|
## Data Loaders
|
|
@@ -410,293 +181,15 @@ Async functions for loading platform data. All loaders return promises that reso
|
|
|
410
181
|
const { dataLoaders } = useDataBridge();
|
|
411
182
|
|
|
412
183
|
// Location data
|
|
413
|
-
const locations = await dataLoaders.getAllLocations();
|
|
414
|
-
const currentLocations = await dataLoaders.locations();
|
|
415
|
-
|
|
416
|
-
// Catalog data
|
|
417
|
-
const categories = await dataLoaders.categories(); // Product categories
|
|
418
|
-
const brands = await dataLoaders.brands(); // Available brands
|
|
419
|
-
const products = await dataLoaders.products(); // Product catalog
|
|
420
|
-
const collections = await dataLoaders.collections(); // Product collections
|
|
184
|
+
const locations = await dataLoaders.getAllLocations(); // All dispensary locations
|
|
185
|
+
const currentLocations = await dataLoaders.locations(); // Current context locations
|
|
421
186
|
```
|
|
422
187
|
|
|
423
188
|
**Note:** All data loaders return empty arrays (`[]`) when no data is available.
|
|
424
189
|
|
|
425
|
-
## Examples
|
|
426
|
-
|
|
427
|
-
### Complete Product Catalog Component
|
|
428
|
-
|
|
429
|
-
```tsx
|
|
430
|
-
import React from 'react';
|
|
431
|
-
import { useDataBridge, useAsyncLoader, RemoteBoundaryComponent } from '@dutchie/ecommerce-extensions-sdk';
|
|
432
|
-
|
|
433
|
-
const ProductCatalog: RemoteBoundaryComponent = () => {
|
|
434
|
-
const { dataLoaders, actions, cart } = useDataBridge();
|
|
435
|
-
|
|
436
|
-
const { data: products, isLoading: productsLoading } = useAsyncLoader(dataLoaders.products);
|
|
437
|
-
const { data: categories, isLoading: categoriesLoading } = useAsyncLoader(dataLoaders.categories);
|
|
438
|
-
|
|
439
|
-
const [selectedCategory, setSelectedCategory] = React.useState<string>('');
|
|
440
|
-
|
|
441
|
-
const filteredProducts = selectedCategory
|
|
442
|
-
? products?.filter(product => /* filter logic */)
|
|
443
|
-
: products || [];
|
|
444
|
-
|
|
445
|
-
if (productsLoading || categoriesLoading) {
|
|
446
|
-
return <div className="loading">Loading catalog...</div>;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return (
|
|
450
|
-
<div className="product-catalog">
|
|
451
|
-
<div className="filters">
|
|
452
|
-
<select
|
|
453
|
-
value={selectedCategory}
|
|
454
|
-
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
455
|
-
>
|
|
456
|
-
<option value="">All Categories</option>
|
|
457
|
-
{categories?.map(category => (
|
|
458
|
-
<option key={category.id} value={category.id}>
|
|
459
|
-
{category.name}
|
|
460
|
-
</option>
|
|
461
|
-
))}
|
|
462
|
-
</select>
|
|
463
|
-
</div>
|
|
464
|
-
|
|
465
|
-
<div className="product-grid">
|
|
466
|
-
{filteredProducts.map(product => (
|
|
467
|
-
<div key={product.id} className="product-card">
|
|
468
|
-
<img src={product.image} alt={product.name} />
|
|
469
|
-
<h3>{product.name}</h3>
|
|
470
|
-
<p>${product.price.toFixed(2)}</p>
|
|
471
|
-
<p>{product.description}</p>
|
|
472
|
-
|
|
473
|
-
<div className="product-actions">
|
|
474
|
-
<button
|
|
475
|
-
onClick={() => actions.goToProductDetails(product.id)}
|
|
476
|
-
>
|
|
477
|
-
View Details
|
|
478
|
-
</button>
|
|
479
|
-
|
|
480
|
-
<button
|
|
481
|
-
onClick={() => actions.addToCart(product.id, 1)}
|
|
482
|
-
>
|
|
483
|
-
Add to Cart
|
|
484
|
-
</button>
|
|
485
|
-
</div>
|
|
486
|
-
</div>
|
|
487
|
-
))}
|
|
488
|
-
</div>
|
|
489
|
-
|
|
490
|
-
{cart && cart.items.length > 0 && (
|
|
491
|
-
<div className="cart-summary">
|
|
492
|
-
<p>Cart: {cart.items.length} items - ${cart.total.toFixed(2)}</p>
|
|
493
|
-
<button onClick={actions.showCart}>View Cart</button>
|
|
494
|
-
</div>
|
|
495
|
-
)}
|
|
496
|
-
</div>
|
|
497
|
-
);
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
ProductCatalog.DataBridgeVersion = '0.2';
|
|
501
|
-
export default ProductCatalog;
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### Custom Store Locator
|
|
505
|
-
|
|
506
|
-
```tsx
|
|
507
|
-
import React from 'react';
|
|
508
|
-
import { useDataBridge, useAsyncLoader, RemoteBoundaryComponent } from '@dutchie/ecommerce-extensions-sdk';
|
|
509
|
-
|
|
510
|
-
const StoreLocator: RemoteBoundaryComponent = () => {
|
|
511
|
-
const { dataLoaders, actions, location: currentLocation } = useDataBridge();
|
|
512
|
-
const { data: locations, isLoading } = useAsyncLoader(dataLoaders.getAllLocations);
|
|
513
|
-
|
|
514
|
-
if (isLoading) return <div>Loading locations...</div>;
|
|
515
|
-
|
|
516
|
-
const formatHours = (hours: any) => {
|
|
517
|
-
if (!hours?.effectiveHours) return 'Hours not available';
|
|
518
|
-
|
|
519
|
-
const today = new Date().toLocaleDateString('en-US', { weekday: 'long' });
|
|
520
|
-
const todayHours = hours.effectiveHours[today];
|
|
521
|
-
|
|
522
|
-
if (!todayHours?.active) return 'Closed today';
|
|
523
|
-
return `${todayHours.start} - ${todayHours.end}`;
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
return (
|
|
527
|
-
<div className="store-locator">
|
|
528
|
-
<h2>Find a Store</h2>
|
|
529
|
-
|
|
530
|
-
{locations?.map(store => (
|
|
531
|
-
<div
|
|
532
|
-
key={store.id}
|
|
533
|
-
className={`store-card ${store.id === currentLocation?.id ? 'current' : ''}`}
|
|
534
|
-
>
|
|
535
|
-
<div className="store-header">
|
|
536
|
-
<img src={store.images.logo} alt={`${store.name} logo`} />
|
|
537
|
-
<div>
|
|
538
|
-
<h3>{store.name}</h3>
|
|
539
|
-
<p className="chain">{store.chain}</p>
|
|
540
|
-
</div>
|
|
541
|
-
</div>
|
|
542
|
-
|
|
543
|
-
<div className="store-details">
|
|
544
|
-
<div className="address">
|
|
545
|
-
<p>{store.address.street1}</p>
|
|
546
|
-
{store.address.street2 && <p>{store.address.street2}</p>}
|
|
547
|
-
<p>{store.address.city}, {store.address.stateAbbreviation} {store.address.zip}</p>
|
|
548
|
-
</div>
|
|
549
|
-
|
|
550
|
-
<div className="contact">
|
|
551
|
-
<p>📞 {store.phone}</p>
|
|
552
|
-
<p>✉️ {store.email}</p>
|
|
553
|
-
</div>
|
|
554
|
-
|
|
555
|
-
<div className="hours">
|
|
556
|
-
<p><strong>Today:</strong> {formatHours(store.hours.inStorePickup)}</p>
|
|
557
|
-
</div>
|
|
558
|
-
|
|
559
|
-
<div className="services">
|
|
560
|
-
{store.orderTypes.inStorePickup && <span className="service">In-Store Pickup</span>}
|
|
561
|
-
{store.orderTypes.curbsidePickup && <span className="service">Curbside</span>}
|
|
562
|
-
{store.orderTypes.delivery && <span className="service">Delivery</span>}
|
|
563
|
-
</div>
|
|
564
|
-
</div>
|
|
565
|
-
|
|
566
|
-
<div className="store-actions">
|
|
567
|
-
<button onClick={() => actions.goToStore(store.id, store.cname)}>
|
|
568
|
-
Visit Store
|
|
569
|
-
</button>
|
|
570
|
-
{store.links.website && (
|
|
571
|
-
<a href={store.links.website} target="_blank" rel="noopener noreferrer">
|
|
572
|
-
Website
|
|
573
|
-
</a>
|
|
574
|
-
)}
|
|
575
|
-
</div>
|
|
576
|
-
</div>
|
|
577
|
-
))}
|
|
578
|
-
</div>
|
|
579
|
-
);
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
StoreLocator.DataBridgeVersion = '0.2';
|
|
583
|
-
export default StoreLocator;
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
### Enhanced Cart Component
|
|
587
|
-
|
|
588
|
-
```tsx
|
|
589
|
-
import React from 'react';
|
|
590
|
-
import { useDataBridge, RemoteBoundaryComponent } from '@dutchie/ecommerce-extensions-sdk';
|
|
591
|
-
|
|
592
|
-
const EnhancedCart: RemoteBoundaryComponent = () => {
|
|
593
|
-
const { cart, actions, location } = useDataBridge();
|
|
594
|
-
|
|
595
|
-
if (!cart || cart.items.length === 0) {
|
|
596
|
-
return (
|
|
597
|
-
<div className="empty-cart">
|
|
598
|
-
<h3>Your cart is empty</h3>
|
|
599
|
-
<button onClick={actions.goToStoreFront}>
|
|
600
|
-
Continue Shopping
|
|
601
|
-
</button>
|
|
602
|
-
</div>
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
return (
|
|
607
|
-
<div className="enhanced-cart">
|
|
608
|
-
<div className="cart-header">
|
|
609
|
-
<h2>Your Cart</h2>
|
|
610
|
-
<p>Order from {location?.name}</p>
|
|
611
|
-
</div>
|
|
612
|
-
|
|
613
|
-
<div className="cart-items">
|
|
614
|
-
{cart.items.map(item => (
|
|
615
|
-
<div key={item.productId} className="cart-item">
|
|
616
|
-
<div className="item-details">
|
|
617
|
-
<h4>{item.name}</h4>
|
|
618
|
-
<p className="price">${item.price.toFixed(2)} each</p>
|
|
619
|
-
</div>
|
|
620
|
-
|
|
621
|
-
<div className="quantity-controls">
|
|
622
|
-
<button
|
|
623
|
-
onClick={() => actions.updateCartItem(item.productId, item.quantity - 1)}
|
|
624
|
-
disabled={item.quantity <= 1}
|
|
625
|
-
>
|
|
626
|
-
-
|
|
627
|
-
</button>
|
|
628
|
-
<span className="quantity">{item.quantity}</span>
|
|
629
|
-
<button
|
|
630
|
-
onClick={() => actions.updateCartItem(item.productId, item.quantity + 1)}
|
|
631
|
-
>
|
|
632
|
-
+
|
|
633
|
-
</button>
|
|
634
|
-
</div>
|
|
635
|
-
|
|
636
|
-
<div className="item-total">
|
|
637
|
-
${(item.price * item.quantity).toFixed(2)}
|
|
638
|
-
</div>
|
|
639
|
-
|
|
640
|
-
<button
|
|
641
|
-
className="remove-item"
|
|
642
|
-
onClick={() => actions.removeFromCart(item.productId)}
|
|
643
|
-
>
|
|
644
|
-
Remove
|
|
645
|
-
</button>
|
|
646
|
-
</div>
|
|
647
|
-
))}
|
|
648
|
-
</div>
|
|
649
|
-
|
|
650
|
-
<div className="cart-summary">
|
|
651
|
-
<div className="summary-line">
|
|
652
|
-
<span>Subtotal:</span>
|
|
653
|
-
<span>${cart.subtotal.toFixed(2)}</span>
|
|
654
|
-
</div>
|
|
655
|
-
|
|
656
|
-
{cart.discount > 0 && (
|
|
657
|
-
<div className="summary-line discount">
|
|
658
|
-
<span>Discount:</span>
|
|
659
|
-
<span>-${cart.discount.toFixed(2)}</span>
|
|
660
|
-
</div>
|
|
661
|
-
)}
|
|
662
|
-
|
|
663
|
-
<div className="summary-line">
|
|
664
|
-
<span>Tax:</span>
|
|
665
|
-
<span>${cart.tax.toFixed(2)}</span>
|
|
666
|
-
</div>
|
|
667
|
-
|
|
668
|
-
<div className="summary-line total">
|
|
669
|
-
<span><strong>Total:</strong></span>
|
|
670
|
-
<span><strong>${cart.total.toFixed(2)}</strong></span>
|
|
671
|
-
</div>
|
|
672
|
-
</div>
|
|
673
|
-
|
|
674
|
-
<div className="cart-actions">
|
|
675
|
-
<button
|
|
676
|
-
className="clear-cart"
|
|
677
|
-
onClick={actions.clearCart}
|
|
678
|
-
>
|
|
679
|
-
Clear Cart
|
|
680
|
-
</button>
|
|
681
|
-
|
|
682
|
-
<button
|
|
683
|
-
className="checkout-btn"
|
|
684
|
-
onClick={actions.goToCheckout}
|
|
685
|
-
>
|
|
686
|
-
Proceed to Checkout
|
|
687
|
-
</button>
|
|
688
|
-
</div>
|
|
689
|
-
</div>
|
|
690
|
-
);
|
|
691
|
-
};
|
|
692
|
-
|
|
693
|
-
EnhancedCart.DataBridgeVersion = '0.2';
|
|
694
|
-
export default EnhancedCart;
|
|
695
|
-
```
|
|
696
|
-
|
|
697
190
|
## Best Practices
|
|
698
191
|
|
|
699
|
-
###
|
|
192
|
+
### Error Handling
|
|
700
193
|
|
|
701
194
|
Always wrap `useDataBridge()` calls in error boundaries and handle loading states:
|
|
702
195
|
|
|
@@ -705,10 +198,10 @@ const SafeComponent = () => {
|
|
|
705
198
|
try {
|
|
706
199
|
const { dataLoaders } = useDataBridge();
|
|
707
200
|
const { data, isLoading } = useAsyncLoader(dataLoaders.products);
|
|
708
|
-
|
|
201
|
+
|
|
709
202
|
if (isLoading) return <LoadingSpinner />;
|
|
710
203
|
if (!data) return <ErrorMessage message="Failed to load products" />;
|
|
711
|
-
|
|
204
|
+
|
|
712
205
|
return <ProductList products={data} />;
|
|
713
206
|
} catch (error) {
|
|
714
207
|
return <ErrorFallback error={error} />;
|
|
@@ -716,38 +209,16 @@ const SafeComponent = () => {
|
|
|
716
209
|
};
|
|
717
210
|
```
|
|
718
211
|
|
|
719
|
-
###
|
|
720
|
-
|
|
721
|
-
Use React.memo and useMemo for expensive operations:
|
|
722
|
-
|
|
723
|
-
```tsx
|
|
724
|
-
const ProductCard = React.memo(({ product, onAddToCart }) => {
|
|
725
|
-
const formattedPrice = React.useMemo(() =>
|
|
726
|
-
new Intl.NumberFormat('en-US', {
|
|
727
|
-
style: 'currency',
|
|
728
|
-
currency: 'USD'
|
|
729
|
-
}).format(product.price),
|
|
730
|
-
[product.price]
|
|
731
|
-
);
|
|
732
|
-
|
|
733
|
-
return (
|
|
734
|
-
<div className="product-card">
|
|
735
|
-
<h3>{product.name}</h3>
|
|
736
|
-
<p>{formattedPrice}</p>
|
|
737
|
-
<button onClick={() => onAddToCart(product.id, 1)}>
|
|
738
|
-
Add to Cart
|
|
739
|
-
</button>
|
|
740
|
-
</div>
|
|
741
|
-
);
|
|
742
|
-
});
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
### 3. TypeScript Best Practices
|
|
212
|
+
### TypeScript Best Practices
|
|
746
213
|
|
|
747
214
|
Leverage the provided types for better development experience:
|
|
748
215
|
|
|
749
216
|
```tsx
|
|
750
|
-
import {
|
|
217
|
+
import {
|
|
218
|
+
Product,
|
|
219
|
+
Category,
|
|
220
|
+
useDataBridge,
|
|
221
|
+
} from "@dutchie/ecommerce-extensions-sdk";
|
|
751
222
|
|
|
752
223
|
interface ProductFilterProps {
|
|
753
224
|
products: Product[];
|
|
@@ -755,23 +226,23 @@ interface ProductFilterProps {
|
|
|
755
226
|
onFilterChange: (categoryId: string) => void;
|
|
756
227
|
}
|
|
757
228
|
|
|
758
|
-
const ProductFilter: React.FC<ProductFilterProps> = ({
|
|
759
|
-
products,
|
|
760
|
-
categories,
|
|
761
|
-
onFilterChange
|
|
229
|
+
const ProductFilter: React.FC<ProductFilterProps> = ({
|
|
230
|
+
products,
|
|
231
|
+
categories,
|
|
232
|
+
onFilterChange,
|
|
762
233
|
}) => {
|
|
763
234
|
// Implementation
|
|
764
235
|
};
|
|
765
236
|
```
|
|
766
237
|
|
|
767
|
-
###
|
|
238
|
+
### Context Usage
|
|
768
239
|
|
|
769
240
|
Always check for data availability before using:
|
|
770
241
|
|
|
771
242
|
```tsx
|
|
772
243
|
const UserProfile = () => {
|
|
773
244
|
const { user, actions } = useDataBridge();
|
|
774
|
-
|
|
245
|
+
|
|
775
246
|
if (!user) {
|
|
776
247
|
return (
|
|
777
248
|
<div className="login-prompt">
|
|
@@ -780,7 +251,7 @@ const UserProfile = () => {
|
|
|
780
251
|
</div>
|
|
781
252
|
);
|
|
782
253
|
}
|
|
783
|
-
|
|
254
|
+
|
|
784
255
|
return (
|
|
785
256
|
<div className="user-profile">
|
|
786
257
|
<h2>Welcome, {user.firstName}!</h2>
|
|
@@ -790,49 +261,16 @@ const UserProfile = () => {
|
|
|
790
261
|
};
|
|
791
262
|
```
|
|
792
263
|
|
|
793
|
-
## Development
|
|
794
|
-
|
|
795
|
-
### Setup
|
|
796
|
-
|
|
797
|
-
```bash
|
|
798
|
-
# Install dependencies
|
|
799
|
-
pnpm install
|
|
800
|
-
|
|
801
|
-
# Start development mode
|
|
802
|
-
pnpm dev
|
|
803
|
-
|
|
804
|
-
# Build for production
|
|
805
|
-
pnpm build
|
|
806
|
-
|
|
807
|
-
# Run tests
|
|
808
|
-
pnpm test
|
|
809
|
-
|
|
810
|
-
# Format code
|
|
811
|
-
pnpm format
|
|
812
|
-
|
|
813
|
-
# Lint and fix
|
|
814
|
-
pnpm check
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
### Building Extensions
|
|
818
|
-
|
|
819
|
-
1. Create your extension component
|
|
820
|
-
2. Set the `DataBridgeVersion` property
|
|
821
|
-
3. Export your component
|
|
822
|
-
4. Build and deploy using the Dutchie Pro Deployment platform
|
|
823
|
-
|
|
824
264
|
### Testing
|
|
825
265
|
|
|
826
|
-
The SDK includes utilities for testing your extensions:
|
|
827
|
-
|
|
828
266
|
```tsx
|
|
829
|
-
import { render, screen } from
|
|
830
|
-
import { DataBridgeContext } from
|
|
831
|
-
import MyExtension from
|
|
267
|
+
import { render, screen } from "@testing-library/react";
|
|
268
|
+
import { DataBridgeContext } from "@dutchie/ecommerce-extensions-sdk";
|
|
269
|
+
import MyExtension from "./MyExtension";
|
|
832
270
|
|
|
833
271
|
const mockDataBridge = {
|
|
834
|
-
menuContext:
|
|
835
|
-
location: { id:
|
|
272
|
+
menuContext: "store-front" as const,
|
|
273
|
+
location: { id: "1", name: "Test Dispensary" },
|
|
836
274
|
dataLoaders: {
|
|
837
275
|
products: jest.fn().mockResolvedValue([]),
|
|
838
276
|
// ... other loaders
|
|
@@ -840,20 +278,54 @@ const mockDataBridge = {
|
|
|
840
278
|
actions: {
|
|
841
279
|
addToCart: jest.fn(),
|
|
842
280
|
// ... other actions
|
|
843
|
-
}
|
|
281
|
+
},
|
|
844
282
|
};
|
|
845
283
|
|
|
846
|
-
test(
|
|
284
|
+
test("renders extension correctly", () => {
|
|
847
285
|
render(
|
|
848
286
|
<DataBridgeContext.Provider value={mockDataBridge}>
|
|
849
287
|
<MyExtension />
|
|
850
288
|
</DataBridgeContext.Provider>
|
|
851
289
|
);
|
|
852
|
-
|
|
853
|
-
expect(screen.getByText(
|
|
290
|
+
|
|
291
|
+
expect(screen.getByText("Test Dispensary")).toBeInTheDocument();
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Performance Optimization
|
|
296
|
+
|
|
297
|
+
Use `React.memo` and `useMemo` for expensive operations:
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
const ProductCard = React.memo(({ product, onAddToCart }) => {
|
|
301
|
+
const formattedPrice = React.useMemo(
|
|
302
|
+
() =>
|
|
303
|
+
new Intl.NumberFormat("en-US", {
|
|
304
|
+
style: "currency",
|
|
305
|
+
currency: "USD",
|
|
306
|
+
}).format(product.price),
|
|
307
|
+
[product.price]
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<div className="product-card">
|
|
312
|
+
<h3>{product.name}</h3>
|
|
313
|
+
<p>{formattedPrice}</p>
|
|
314
|
+
<button onClick={() => onAddToCart(product.id, 1)}>Add to Cart</button>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
854
317
|
});
|
|
855
318
|
```
|
|
856
319
|
|
|
320
|
+
## Core Concepts
|
|
321
|
+
|
|
322
|
+
The Dutchie Ecommerce Extensions SDK is built around several key concepts:
|
|
323
|
+
|
|
324
|
+
1. **Data Bridge**: A unified interface for accessing dispensary data, user information, and cart state
|
|
325
|
+
2. **Actions**: Pre-built navigation and cart management functions
|
|
326
|
+
3. **Remote Components**: Extension components that integrate seamlessly with the Dutchie platform
|
|
327
|
+
4. **Context-Driven Architecture**: React context provides data and actions throughout your extension
|
|
328
|
+
|
|
857
329
|
## Support
|
|
858
330
|
|
|
859
331
|
For technical support and questions about the Dutchie Ecommerce Extensions SDK:
|
|
@@ -861,8 +333,3 @@ For technical support and questions about the Dutchie Ecommerce Extensions SDK:
|
|
|
861
333
|
- 📧 Contact your Dutchie agency partner representative
|
|
862
334
|
- 📚 Refer to the Dutchie Pro platform documentation
|
|
863
335
|
- 🐛 Report issues through the official Dutchie support channels
|
|
864
|
-
|
|
865
|
-
---
|
|
866
|
-
|
|
867
|
-
**License:** MIT
|
|
868
|
-
**Node Version:** >=16.0.0
|