@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/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