@ekomerc/storefront 0.1.0 → 0.1.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 +237 -10
- package/dist/README.md +679 -0
- package/dist/errors.d.cts +74 -0
- package/dist/errors.d.ts +74 -80
- package/dist/index.cjs +2840 -1810
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1493 -0
- package/dist/index.d.ts +1493 -952
- package/dist/index.js +2841 -1811
- package/dist/index.js.map +1 -1
- package/dist/package.json +45 -0
- package/dist/types.d.cts +656 -0
- package/dist/types.d.ts +656 -1
- package/package.json +12 -5
package/dist/README.md
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
# @ekomerc/storefront
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for building headless storefronts with ekomerc. Provides typed helpers for products, collections, cart, and checkout operations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @ekomerc/storefront
|
|
9
|
+
# or
|
|
10
|
+
npm install @ekomerc/storefront
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createStorefrontClient } from "@ekomerc/storefront";
|
|
17
|
+
|
|
18
|
+
const client = createStorefrontClient({
|
|
19
|
+
endpoint: "https://api.yourstore.com/graphql",
|
|
20
|
+
apiKey: "sfk_your_api_key",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Fetch products
|
|
24
|
+
const result = await client.products.list({ first: 10, filter: { search: "shirt" } });
|
|
25
|
+
if (result.isOk()) {
|
|
26
|
+
console.log(result.value.items);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Add to cart (variantId is the GID from product queries, e.g. variant.id)
|
|
30
|
+
await client.cart.create();
|
|
31
|
+
const addResult = await client.cart.addItem(variant.id, 1);
|
|
32
|
+
if (addResult.isOk()) {
|
|
33
|
+
console.log("Added to cart:", addResult.value);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
interface StorefrontClientConfig {
|
|
41
|
+
endpoint: string; // Full Storefront GraphQL URL (absolute)
|
|
42
|
+
apiKey: string; // API key (sfk_...)
|
|
43
|
+
storage?: StorageAdapter; // Cart token storage (default: auto-detect)
|
|
44
|
+
cacheTTL?: number; // Query cache TTL in ms (default: 5 minutes)
|
|
45
|
+
trackingAttributionTTL?: number; // Landing attribution TTL in ms (default: 30 days)
|
|
46
|
+
tracking?: StorefrontTrackingRuntimeConfig; // Unified tracking runtime config
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Tracking Runtime Configuration
|
|
51
|
+
|
|
52
|
+
`tracking` wires up the unified tracking runtime — consent signaling, GTM adapter dispatch, and optional diagnostics.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
interface StorefrontTrackingRuntimeConfig {
|
|
56
|
+
// Controls whether browser adapters may dispatch when consent is "unknown".
|
|
57
|
+
// Defaults to true when not set.
|
|
58
|
+
dispatchOnUnknownConsent?: boolean;
|
|
59
|
+
|
|
60
|
+
// Called before every event to get the current consent state.
|
|
61
|
+
// Return "granted", "denied", or "unknown". Defaults to "unknown".
|
|
62
|
+
resolveConsentState?: () => AnalyticsConsentState | Promise<AnalyticsConsentState>;
|
|
63
|
+
|
|
64
|
+
// Browser adapters to forward events to (GTM-only supported path in v1).
|
|
65
|
+
adapters?: AnalyticsBrowserAdapter[];
|
|
66
|
+
|
|
67
|
+
// Optional diagnostic observer — called after every ingest/adapter/consent operation.
|
|
68
|
+
// Never throws; errors inside are swallowed.
|
|
69
|
+
onDiagnostic?: (diagnostic: AnalyticsDiagnostic) => void;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Consent states:** `"granted"` | `"denied"` | `"unknown"`
|
|
74
|
+
|
|
75
|
+
- `dispatchOnUnknownConsent: false`: events forwarded only when consent is `"granted"`.
|
|
76
|
+
- `dispatchOnUnknownConsent: true` (default): events forwarded unless consent is `"denied"`.
|
|
77
|
+
|
|
78
|
+
Example with a consent management platform:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const client = createStorefrontClient({
|
|
82
|
+
endpoint: "https://api.yourstore.com/graphql",
|
|
83
|
+
apiKey: "sfk_...",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await client.init({
|
|
87
|
+
analytics: {
|
|
88
|
+
dispatchOnUnknownConsent: true,
|
|
89
|
+
resolveConsentState: () => myCmp.getConsentState(), // "granted" | "denied" | "unknown"
|
|
90
|
+
adapters: [gtmAdapter].filter(Boolean),
|
|
91
|
+
onDiagnostic: (d) => console.debug("[tracking]", d),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The SDK persists landing attribution (`utm_*`, `gclid`, `gbraid`, `wbraid`, `fbclid`, `ttclid`) in `localStorage` under `ekomerc_tracking_attribution` and expires it on read. Fresh URL params always overwrite the stored snapshot.
|
|
97
|
+
|
|
98
|
+
Call `client.init()` once on storefront bootstrap. It loads the store analytics config from the storefront API, captures landing attribution, and removes known attribution params from the address bar by default after capture. If you need to keep them in the URL, pass:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
await client.init({
|
|
102
|
+
analytics: {
|
|
103
|
+
stripUrl: false,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### GTM-First Browser Model (v1)
|
|
109
|
+
|
|
110
|
+
- SDK remains the single browser event producer.
|
|
111
|
+
- First-party ingest (`/analytics/ingest`) remains authoritative for Ekomerc analytics/audit.
|
|
112
|
+
- Browser-side vendor routing is GTM-only in this phase.
|
|
113
|
+
- Backend provider forwarding is env-gated and defaults to disabled (`TRACKING_PROVIDER_FORWARDING_ENABLED=false`).
|
|
114
|
+
- Hosted/default storefront GTM support is deferred (follow-up TODO).
|
|
115
|
+
|
|
116
|
+
Custom storefront setup in this phase:
|
|
117
|
+
|
|
118
|
+
1. Install the GTM snippet in storefront code (merchant-owned, outside SDK).
|
|
119
|
+
2. Import `packages/sdk/storefront/src/gtm-recipes/ekomerc-gtm-browser-recipe-v1.export-v2.json` into GTM.
|
|
120
|
+
3. Fill provider IDs/settings in GTM and publish manually.
|
|
121
|
+
|
|
122
|
+
Anti-duplication checklist (required):
|
|
123
|
+
|
|
124
|
+
1. Disable GA4 Enhanced Measurement in the GA4 data stream used by this GTM setup.
|
|
125
|
+
2. Disable Meta Automatic Event Setup in Meta Events Manager for the pixel used by this setup.
|
|
126
|
+
3. Disable TikTok automatic event setup or equivalent automatic browser-event features for this setup.
|
|
127
|
+
|
|
128
|
+
### Custom Endpoint URLs
|
|
129
|
+
|
|
130
|
+
`endpoint` is used exactly as provided. The SDK sends `POST` requests directly to that URL and does not rewrite hosts or paths.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const client = createStorefrontClient({
|
|
134
|
+
endpoint: "https://edge.example.com/custom/storefront/v1/graphql",
|
|
135
|
+
apiKey: "sfk_test_placeholder",
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Supported examples:
|
|
140
|
+
- `https://api.example.com/graphql`
|
|
141
|
+
- `https://edge.example.com/custom/storefront/v1/graphql`
|
|
142
|
+
- `https://staging.example.net/gql/storefront`
|
|
143
|
+
|
|
144
|
+
### Auth And Request Headers
|
|
145
|
+
|
|
146
|
+
For each request the SDK sends:
|
|
147
|
+
- `content-type: application/json`
|
|
148
|
+
- `x-storefront-key: <apiKey>` (always)
|
|
149
|
+
- `x-cart-token: <cartToken>` (when cart token exists)
|
|
150
|
+
- `authorization: Bearer <customerToken>` (when customer token exists)
|
|
151
|
+
|
|
152
|
+
### Storage Adapters
|
|
153
|
+
|
|
154
|
+
The SDK stores the cart token to persist the cart across page loads.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import {
|
|
158
|
+
createStorefrontClient,
|
|
159
|
+
createLocalStorageAdapter, // Browser localStorage
|
|
160
|
+
createMemoryAdapter, // In-memory (SSR/testing)
|
|
161
|
+
createDefaultAdapter, // Auto-detects environment
|
|
162
|
+
} from "@ekomerc/storefront";
|
|
163
|
+
|
|
164
|
+
// Browser (default)
|
|
165
|
+
const client = createStorefrontClient({
|
|
166
|
+
endpoint: "https://api.yourstore.com/graphql",
|
|
167
|
+
apiKey: "sfk_...",
|
|
168
|
+
// Uses localStorage by default in browser
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// SSR / Server-side
|
|
172
|
+
const client = createStorefrontClient({
|
|
173
|
+
endpoint: "https://api.yourstore.com/graphql",
|
|
174
|
+
apiKey: "sfk_...",
|
|
175
|
+
storage: createMemoryAdapter(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Custom storage
|
|
179
|
+
const client = createStorefrontClient({
|
|
180
|
+
endpoint: "https://api.yourstore.com/graphql",
|
|
181
|
+
apiKey: "sfk_...",
|
|
182
|
+
storage: {
|
|
183
|
+
get: (key) => sessionStorage.getItem(key),
|
|
184
|
+
set: (key, value) => sessionStorage.setItem(key, value),
|
|
185
|
+
remove: (key) => sessionStorage.removeItem(key),
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## API Reference
|
|
191
|
+
|
|
192
|
+
All methods return `Promise<Result<T, StorefrontError>>` from [neverthrow](https://github.com/supermacro/neverthrow).
|
|
193
|
+
|
|
194
|
+
### Products
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// List products with pagination
|
|
198
|
+
const result = await client.products.list({
|
|
199
|
+
first: 10, // Number of products (default: 20)
|
|
200
|
+
after: cursor, // Pagination cursor
|
|
201
|
+
filter: { search: "shirt" }, // Search query
|
|
202
|
+
sort: "BEST_SELLING"
|
|
203
|
+
});
|
|
204
|
+
// Returns: { items: Product[], pageInfo: PageInfo }
|
|
205
|
+
|
|
206
|
+
// Get single product by handle or GID
|
|
207
|
+
const result = await client.products.get("blue-shirt");
|
|
208
|
+
const result = await client.products.get("UHJvZHVjdDoxMjM="); // GID (global ID)
|
|
209
|
+
|
|
210
|
+
// Batch fetch by handles
|
|
211
|
+
const result = await client.products.getByHandles(["shirt-1", "shirt-2"]);
|
|
212
|
+
// Returns: (Product | null)[]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Each `Product` includes a `detailSections: DetailSection[]` field containing structured sections for use in product detail pages. Each section has a `sectionType` (`"RICH_TEXT"` | `"BULLET_LIST"` | `"TABLE"`), `displayIntent` (`"ACCORDION"` | `"SPECIFICATIONS"` | `"BOTH"`), `title`, `position`, and type-specific `content`.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
if (result.isOk()) {
|
|
219
|
+
for (const section of result.value.detailSections) {
|
|
220
|
+
if (section.sectionType === "RICH_TEXT") {
|
|
221
|
+
console.log(section.content.html);
|
|
222
|
+
} else if (section.sectionType === "BULLET_LIST") {
|
|
223
|
+
console.log(section.content.items);
|
|
224
|
+
} else if (section.sectionType === "TABLE") {
|
|
225
|
+
console.log(section.content.rows); // [string, string][]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Collections
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// List collections
|
|
235
|
+
const result = await client.collections.list({
|
|
236
|
+
first: 10,
|
|
237
|
+
after: cursor,
|
|
238
|
+
search: "summer"
|
|
239
|
+
});
|
|
240
|
+
// Returns: { items: Collection[], pageInfo: PageInfo }
|
|
241
|
+
|
|
242
|
+
// Get single collection
|
|
243
|
+
const result = await client.collections.get("summer-collection");
|
|
244
|
+
|
|
245
|
+
// Get products in a collection
|
|
246
|
+
const result = await client.collections.getProducts("summer-collection", {
|
|
247
|
+
first: 20,
|
|
248
|
+
after: cursor,
|
|
249
|
+
});
|
|
250
|
+
// Returns: { items: Product[], pageInfo: PageInfo }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Cart
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// Get current cart (from stored token)
|
|
257
|
+
const result = await client.cart.get();
|
|
258
|
+
// Returns: Cart | null
|
|
259
|
+
|
|
260
|
+
// Create new cart
|
|
261
|
+
const result = await client.cart.create();
|
|
262
|
+
// Returns: Cart
|
|
263
|
+
|
|
264
|
+
// Add item to cart
|
|
265
|
+
// variantId must be a GID (global ID) — use variant.id from product queries
|
|
266
|
+
const result = await client.cart.addItem(variantId, quantity);
|
|
267
|
+
// Returns: Cart
|
|
268
|
+
|
|
269
|
+
// Update item quantity
|
|
270
|
+
// variantId must be a GID (global ID) — use variant.id from product queries
|
|
271
|
+
const result = await client.cart.updateItem(variantId, newQuantity);
|
|
272
|
+
// Returns: Cart
|
|
273
|
+
|
|
274
|
+
// Remove item from cart
|
|
275
|
+
// variantId must be a GID (global ID) — use variant.id from product queries
|
|
276
|
+
const result = await client.cart.removeItem(variantId);
|
|
277
|
+
// Returns: Cart
|
|
278
|
+
|
|
279
|
+
// Clear all items
|
|
280
|
+
const result = await client.cart.clear();
|
|
281
|
+
// Returns: Cart
|
|
282
|
+
|
|
283
|
+
// Apply a promo code
|
|
284
|
+
const result = await client.cart.applyPromoCode("SAVE10");
|
|
285
|
+
if (result.isErr()) {
|
|
286
|
+
// ValidationError with reason (e.g. "Invalid or expired code")
|
|
287
|
+
}
|
|
288
|
+
// Returns: Cart (with appliedPromoCode populated)
|
|
289
|
+
|
|
290
|
+
// Remove applied promo code
|
|
291
|
+
const result = await client.cart.removePromoCode();
|
|
292
|
+
// Returns: Cart (with appliedPromoCode cleared)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Note: `variantId` in `cart.addItem`, `cart.updateItem`, and `cart.removeItem` must be a GID (global ID), not a raw UUID. Use variant IDs returned by product queries.
|
|
296
|
+
|
|
297
|
+
### Pricing Display
|
|
298
|
+
|
|
299
|
+
Product variants include sale and quantity pricing information:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const result = await client.products.get("my-product");
|
|
303
|
+
if (result.isOk()) {
|
|
304
|
+
const variant = result.value.variants[0];
|
|
305
|
+
|
|
306
|
+
// Sale price detection
|
|
307
|
+
if (variant.isOnSale) {
|
|
308
|
+
console.log(`Was: ${variant.compareAtPrice}, Now: ${variant.price}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Quantity tier pricing
|
|
312
|
+
for (const tier of variant.quantityPricing) {
|
|
313
|
+
console.log(`Buy ${tier.minQuantity}+ at ${tier.price}/unit`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Cart items expose the resolved effective unit price (considering sale + quantity tier):
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
const cart = (await client.cart.get()).unwrapOr(null);
|
|
322
|
+
if (cart) {
|
|
323
|
+
for (const item of cart.items) {
|
|
324
|
+
console.log(`Base price: ${item.price.amount}`);
|
|
325
|
+
console.log(`Effective price: ${item.effectiveUnitPrice.amount}`);
|
|
326
|
+
console.log(`Line total: ${item.totalPrice.amount}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Discount information
|
|
330
|
+
console.log(`Discount: ${cart.discountTotal.amount}`);
|
|
331
|
+
if (cart.appliedPromoCode) {
|
|
332
|
+
console.log(`Promo: ${cart.appliedPromoCode.code} (-${cart.appliedPromoCode.discountAmount})`);
|
|
333
|
+
}
|
|
334
|
+
for (const discount of cart.appliedDiscounts) {
|
|
335
|
+
console.log(`${discount.description}: -${discount.discountAmount}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Checkout
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// Start checkout (transitions cart to checkout state)
|
|
344
|
+
const result = await client.checkout.start();
|
|
345
|
+
// Returns: Cart (with status: "checkout")
|
|
346
|
+
|
|
347
|
+
// Update checkout info
|
|
348
|
+
const result = await client.checkout.update({
|
|
349
|
+
email: "customer@example.com",
|
|
350
|
+
phone: "+381...",
|
|
351
|
+
shippingAddress: {
|
|
352
|
+
firstName: "John",
|
|
353
|
+
lastName: "Doe",
|
|
354
|
+
address1: "123 Main St",
|
|
355
|
+
city: "Belgrade",
|
|
356
|
+
country: "Serbia",
|
|
357
|
+
countryCode: "RS",
|
|
358
|
+
zip: "11000",
|
|
359
|
+
},
|
|
360
|
+
billingAddress: { ... },
|
|
361
|
+
notes: "Leave at door",
|
|
362
|
+
emailMarketingConsent: true,
|
|
363
|
+
});
|
|
364
|
+
// Returns: Cart
|
|
365
|
+
|
|
366
|
+
// Complete checkout (creates order)
|
|
367
|
+
const result = await client.checkout.complete();
|
|
368
|
+
// Returns: Order
|
|
369
|
+
|
|
370
|
+
// Abandon checkout (returns cart to active state)
|
|
371
|
+
const result = await client.checkout.abandon();
|
|
372
|
+
// Returns: Cart
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Analytics
|
|
376
|
+
|
|
377
|
+
`client.analytics.track()` sends a first-party event to the storefront ingest endpoint and, when GTM adapter is configured, mirrors supported events to GTM `dataLayer`.
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// Page view
|
|
381
|
+
await client.analytics.track("page_view");
|
|
382
|
+
|
|
383
|
+
// Product view (productId can be a raw UUID, GID, or base64-encoded GID)
|
|
384
|
+
await client.analytics.track("product_view", { productId: product.id, variantId: variant.id });
|
|
385
|
+
|
|
386
|
+
// Collection view
|
|
387
|
+
await client.analytics.track("collection_view", { collectionId: collection.id });
|
|
388
|
+
|
|
389
|
+
// Search
|
|
390
|
+
await client.analytics.track("search_performed", { query: "shirt", resultsCount: 42 });
|
|
391
|
+
|
|
392
|
+
// Cart events
|
|
393
|
+
await client.analytics.track("add_to_cart", { cartId: cart.id, quantity: 1, itemsCount: 3, cartValue: 29.99 });
|
|
394
|
+
await client.analytics.track("remove_from_cart", { cartId: cart.id, quantity: 1, itemsCount: 2, cartValue: 19.99 });
|
|
395
|
+
|
|
396
|
+
// Checkout funnel
|
|
397
|
+
await client.analytics.track("checkout_started", { cartId: cart.id });
|
|
398
|
+
await client.analytics.track("checkout_step_completed", { cartId: cart.id, step: "contact" }); // "contact" | "shipping" | "payment" | "review"
|
|
399
|
+
await client.analytics.track("checkout_completed", { orderId: order.id, cartId: cart.id, orderTotal: 49.99 });
|
|
400
|
+
|
|
401
|
+
// Custom event — sent as analytics.custom with eventName in properties
|
|
402
|
+
await client.analytics.track("homepage.banner_clicked", { position: 0 });
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
All preset events also accept an optional `AnalyticsContext` as the last argument to override path, referrer, UTM params, consent state, or visitor/session IDs.
|
|
406
|
+
|
|
407
|
+
Important purchase behavior:
|
|
408
|
+
|
|
409
|
+
- `track("checkout_completed", ...)` is ingest-only.
|
|
410
|
+
- Browser purchase event (`ekomerc.purchase`) is emitted only on successful `checkout.complete()` using server-confirmed `purchaseTracking`.
|
|
411
|
+
- `checkout.complete()` API remains `Result<Order, StorefrontError>` (non-breaking).
|
|
412
|
+
|
|
413
|
+
### Cache
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// Clear the query cache
|
|
417
|
+
client.cache.clear();
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Cart Token Management
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// Get current cart token
|
|
424
|
+
const token = client.getCartToken();
|
|
425
|
+
|
|
426
|
+
// Set cart token manually
|
|
427
|
+
client.setCartToken("cart-token-uuid");
|
|
428
|
+
|
|
429
|
+
// Clear cart token
|
|
430
|
+
client.clearCartToken();
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Store
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
const result = await client.store.get();
|
|
437
|
+
if (result.isOk()) {
|
|
438
|
+
const store = result.value;
|
|
439
|
+
// store.analytics — derived analytics availability and GTM config for this store
|
|
440
|
+
console.log(store.analytics);
|
|
441
|
+
// {
|
|
442
|
+
// enabled: true,
|
|
443
|
+
// dispatchOnUnknownConsent: true,
|
|
444
|
+
// gtm: { enabled: true, containerId: "GTM-XXXXXX" },
|
|
445
|
+
// }
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Use `store.analytics` to decide whether GTM forwarding should be active at runtime:
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
import {
|
|
453
|
+
createGtmBrowserAdapter,
|
|
454
|
+
} from "@ekomerc/storefront";
|
|
455
|
+
|
|
456
|
+
const storeResult = await client.store.get();
|
|
457
|
+
if (storeResult.isOk()) {
|
|
458
|
+
const { analytics } = storeResult.value;
|
|
459
|
+
const adapters = [
|
|
460
|
+
createGtmBrowserAdapter(analytics.gtm),
|
|
461
|
+
].filter(Boolean);
|
|
462
|
+
|
|
463
|
+
console.log(adapters, analytics.dispatchOnUnknownConsent);
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Browser Adapters
|
|
468
|
+
|
|
469
|
+
GTM is the supported first-party browser adapter in this phase. The adapter returns `null` when GTM is disabled or container ID is missing.
|
|
470
|
+
|
|
471
|
+
### GTM (`createGtmBrowserAdapter`)
|
|
472
|
+
|
|
473
|
+
Pushes namespaced Ekomerc events to `window.dataLayer` (`ekomerc.*`) and updates GTM Consent Mode via `gtag("consent", "update", ...)`.
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
import { createGtmBrowserAdapter } from "@ekomerc/storefront";
|
|
477
|
+
|
|
478
|
+
const gtmAdapter = createGtmBrowserAdapter(
|
|
479
|
+
store.browserTrackingConfig.gtm, // { enabled, containerId }
|
|
480
|
+
{ dataLayerName: "dataLayer" }, // optional — defaults to "dataLayer"
|
|
481
|
+
);
|
|
482
|
+
// Returns null if disabled or containerId is missing
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
GTM mapping example: `page_view` -> `ekomerc.page_view`, `product_view` -> `ekomerc.view_item`, `add_to_cart` -> `ekomerc.add_to_cart`, `checkout_started` -> `ekomerc.begin_checkout`, `search_performed` -> `ekomerc.search`.
|
|
486
|
+
`checkout_step_completed` and `analytics.custom` remain ingest-only in this phase.
|
|
487
|
+
|
|
488
|
+
### Custom Adapters
|
|
489
|
+
|
|
490
|
+
Implement `AnalyticsBrowserAdapter` to add your own integration:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
import type { AnalyticsBrowserAdapter } from "@ekomerc/storefront";
|
|
494
|
+
|
|
495
|
+
const myAdapter: AnalyticsBrowserAdapter = {
|
|
496
|
+
provider: "gtm", // reuse an existing provider key
|
|
497
|
+
dispatch({ event, consent }) {
|
|
498
|
+
if (event.eventType === "analytics.page_view") {
|
|
499
|
+
myAnalytics.page(event.context.path);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
updateConsent(consent) {
|
|
503
|
+
myAnalytics.setConsent(consent.consentState === "granted");
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Error Handling
|
|
509
|
+
|
|
510
|
+
The SDK uses Result types for error handling. Errors are typed and categorized:
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
import {
|
|
514
|
+
StorefrontError, // Base error class
|
|
515
|
+
NetworkError, // Network/fetch failures
|
|
516
|
+
AuthError, // Invalid/revoked API key
|
|
517
|
+
ValidationError, // Validation errors from mutations
|
|
518
|
+
NotFoundError, // Resource not found
|
|
519
|
+
StateError, // Invalid cart state transition
|
|
520
|
+
GraphQLError, // Unexpected GraphQL errors
|
|
521
|
+
} from "@ekomerc/storefront";
|
|
522
|
+
|
|
523
|
+
const result = await client.products.get("nonexistent");
|
|
524
|
+
|
|
525
|
+
if (result.isErr()) {
|
|
526
|
+
const error = result.error;
|
|
527
|
+
|
|
528
|
+
if (error instanceof NotFoundError) {
|
|
529
|
+
console.log("Product not found");
|
|
530
|
+
} else if (error instanceof AuthError) {
|
|
531
|
+
console.log("Invalid API key");
|
|
532
|
+
} else if (error instanceof StateError) {
|
|
533
|
+
console.log(`Cart state error: ${error.state}`);
|
|
534
|
+
} else if (error instanceof ValidationError) {
|
|
535
|
+
console.log("Validation errors:", error.userErrors);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Or use pattern matching
|
|
540
|
+
result.match(
|
|
541
|
+
(product) => console.log("Found:", product.title),
|
|
542
|
+
(error) => console.error("Error:", error.message)
|
|
543
|
+
);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Troubleshooting Endpoint/Auth Issues
|
|
547
|
+
|
|
548
|
+
1. `AuthError` (`401`/`403`)
|
|
549
|
+
`x-storefront-key` is missing, invalid, revoked, or scoped for a different store. Rotate/regenerate the key and verify the exact value loaded in your runtime env.
|
|
550
|
+
|
|
551
|
+
2. Browser CORS failure
|
|
552
|
+
If browser console shows CORS errors, your API origin must allow your storefront origin and headers:
|
|
553
|
+
`content-type, x-storefront-key, x-cart-token, authorization`.
|
|
554
|
+
|
|
555
|
+
3. Malformed endpoint URL
|
|
556
|
+
Always use a full absolute URL like `https://api.example.com/storefront`. The SDK does not validate the URL at initialization — requests will fail at runtime if the endpoint is malformed.
|
|
557
|
+
|
|
558
|
+
### Cart State Errors
|
|
559
|
+
|
|
560
|
+
Cart mutations (add/update/remove/clear) work on `active` carts. If the cart is in `checkout` state, the backend automatically reactivates it (releases reservations, reverts to `active`) before applying the mutation. This does not mark the cart as abandoned. Attempting to modify a `converted` or `expired` cart returns a `StateError`:
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
const result = await client.cart.addItem(variantId, 1);
|
|
564
|
+
|
|
565
|
+
if (result.isErr() && result.error instanceof StateError) {
|
|
566
|
+
console.log(`Cannot modify cart: ${result.error.message}`);
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## Types
|
|
571
|
+
|
|
572
|
+
All types are exported for TypeScript users:
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
import type {
|
|
576
|
+
// Client
|
|
577
|
+
StorefrontClient,
|
|
578
|
+
StorefrontClientConfig,
|
|
579
|
+
StorageAdapter,
|
|
580
|
+
|
|
581
|
+
// Products
|
|
582
|
+
Product,
|
|
583
|
+
ProductVariant,
|
|
584
|
+
ProductOption,
|
|
585
|
+
ProductImage,
|
|
586
|
+
QuantityPricingTier,
|
|
587
|
+
DetailSection,
|
|
588
|
+
RichTextDetailSection,
|
|
589
|
+
BulletListDetailSection,
|
|
590
|
+
TableDetailSection,
|
|
591
|
+
DisplayIntent,
|
|
592
|
+
|
|
593
|
+
// Collections
|
|
594
|
+
Collection,
|
|
595
|
+
|
|
596
|
+
// Cart
|
|
597
|
+
Cart,
|
|
598
|
+
CartItem,
|
|
599
|
+
CartStatus,
|
|
600
|
+
AppliedPromoCode,
|
|
601
|
+
AppliedDiscount,
|
|
602
|
+
|
|
603
|
+
// Checkout
|
|
604
|
+
CheckoutData,
|
|
605
|
+
Address,
|
|
606
|
+
Order,
|
|
607
|
+
|
|
608
|
+
// Pagination
|
|
609
|
+
PageInfo,
|
|
610
|
+
PaginatedResult,
|
|
611
|
+
|
|
612
|
+
// Common
|
|
613
|
+
Store,
|
|
614
|
+
BrowserTrackingConfig,
|
|
615
|
+
GtmBrowserTrackingConfig,
|
|
616
|
+
|
|
617
|
+
// Tracking runtime
|
|
618
|
+
StorefrontTrackingRuntimeConfig,
|
|
619
|
+
AnalyticsBrowserAdapter,
|
|
620
|
+
AnalyticsConsentState,
|
|
621
|
+
AnalyticsDispatchOnUnknownConsent,
|
|
622
|
+
AnalyticsDiagnostic,
|
|
623
|
+
AnalyticsContext,
|
|
624
|
+
AnalyticsIngestResponse,
|
|
625
|
+
|
|
626
|
+
// Browser adapter types
|
|
627
|
+
GtmBrowserAdapterOptions,
|
|
628
|
+
GtmDataLayerEvent,
|
|
629
|
+
GtmConsentModeUpdate,
|
|
630
|
+
} from "@ekomerc/storefront";
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
## SSR / Server-Side Rendering
|
|
634
|
+
|
|
635
|
+
For SSR frameworks (Next.js, Remix, etc.), use the memory adapter on the server:
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
import {
|
|
639
|
+
createStorefrontClient,
|
|
640
|
+
createMemoryAdapter,
|
|
641
|
+
createLocalStorageAdapter,
|
|
642
|
+
} from "@ekomerc/storefront";
|
|
643
|
+
|
|
644
|
+
const client = createStorefrontClient({
|
|
645
|
+
endpoint: process.env.STOREFRONT_API_URL,
|
|
646
|
+
apiKey: process.env.STOREFRONT_API_KEY,
|
|
647
|
+
storage: typeof window === "undefined"
|
|
648
|
+
? createMemoryAdapter()
|
|
649
|
+
: createLocalStorageAdapter(),
|
|
650
|
+
});
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
## React Integration
|
|
654
|
+
|
|
655
|
+
For React applications, use the `@ekomerc/storefront-react` package which provides hooks and context:
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
import { StorefrontProvider, useProducts, useCart } from "@ekomerc/storefront-react";
|
|
659
|
+
|
|
660
|
+
function App() {
|
|
661
|
+
return (
|
|
662
|
+
<StorefrontProvider client={client}>
|
|
663
|
+
<Shop />
|
|
664
|
+
</StorefrontProvider>
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function Shop() {
|
|
669
|
+
const { products, loading, error } = useProducts();
|
|
670
|
+
const { cart, addItem } = useCart();
|
|
671
|
+
// ...
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
See the `@ekomerc/storefront-react` package for full documentation.
|
|
676
|
+
|
|
677
|
+
## License
|
|
678
|
+
|
|
679
|
+
MIT
|