@snowcone-app/sdk 0.1.14 → 0.2.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/CHANGELOG.md +13 -0
- package/README.md +70 -31
- package/dist/{chunk-6MV7TDTM.js → chunk-D5ZRGKA5.js} +31 -1
- package/dist/index.cjs +234 -1
- package/dist/index.d.cts +233 -4
- package/dist/index.d.ts +233 -4
- package/dist/index.js +200 -1
- package/dist/react.cjs +30 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{websocket-Dum3OooZ.d.cts → websocket-Poy8LZNA.d.cts} +8 -1
- package/dist/{websocket-Dum3OooZ.d.ts → websocket-Poy8LZNA.d.ts} +8 -1
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#220](https://github.com/snowcone-app/snowcone-monorepo/pull/220) [`aa318b5`](https://github.com/snowcone-app/snowcone-monorepo/commit/aa318b51a15e977ecb993c81ae30b6afcf6864b8) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Add realtime saved-design-state API: `createDesignState` (top-level export for building a design state payload) and `RenderSession.renderSavedState(placement, stateId)` (render a previously-saved design state in a realtime session). These were already in `main` and documented on developers.snowcone.app but were missing from the published `@snowcone-app/sdk@0.1.15`, so docs examples failed to compile against the npm package.
|
|
8
|
+
|
|
3
9
|
All notable changes to the Merch SDK will be documented in this file.
|
|
4
10
|
|
|
5
11
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
@@ -8,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
14
|
## [0.1.9] - 2025-10-16
|
|
9
15
|
|
|
10
16
|
### Added
|
|
17
|
+
|
|
11
18
|
- **DOM-Based URL Resolution** - Intelligent URL resolution for transformed assets
|
|
12
19
|
- Added `resolveUrlFromDOM()` to find actual rendered URLs in the DOM
|
|
13
20
|
- Automatically resolves Vercel Blob Storage URLs from v0.dev
|
|
@@ -15,18 +22,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
22
|
- Perfect for environments where image URLs are transformed at runtime
|
|
16
23
|
|
|
17
24
|
### Changed
|
|
25
|
+
|
|
18
26
|
- **Enhanced URL Normalization** - Multi-tier resolution strategy
|
|
19
27
|
1. First attempts to find actual URL in DOM (for v0/Blob Storage)
|
|
20
28
|
2. Falls back to `window.location.origin` conversion
|
|
21
29
|
3. Warns about localhost URLs for better developer experience
|
|
22
30
|
|
|
23
31
|
### Fixed
|
|
32
|
+
|
|
24
33
|
- Relative URLs now work correctly in v0.dev and Vercel Blob Storage environments
|
|
25
34
|
- Mockup generation can access transformed image URLs with hash suffixes
|
|
26
35
|
|
|
27
36
|
## [0.1.8] - 2025-10-16
|
|
28
37
|
|
|
29
38
|
### Added
|
|
39
|
+
|
|
30
40
|
- **Automatic URL Normalization** - Relative artwork URLs are now automatically converted to absolute URLs
|
|
31
41
|
- Added `normalizeImageUrl()` utility function in `utils/url.ts`
|
|
32
42
|
- Supports `/images/art.jpg`, `./art.jpg`, `../art.jpg` patterns
|
|
@@ -34,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
34
44
|
- SSR-safe with helpful warnings
|
|
35
45
|
|
|
36
46
|
### Changed
|
|
47
|
+
|
|
37
48
|
- **Design Generation** - All artwork URLs are normalized in `createDesignForPlacements()`
|
|
38
49
|
- Handles placement designs, provided images, and default URLs
|
|
39
50
|
- Single source of truth for URL normalization
|
|
@@ -42,12 +53,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
42
53
|
- Clear error messages for invalid formats
|
|
43
54
|
|
|
44
55
|
### Fixed
|
|
56
|
+
|
|
45
57
|
- Artwork URLs no longer require absolute paths - relative paths work seamlessly
|
|
46
58
|
- Better developer experience matching Next.js image patterns
|
|
47
59
|
|
|
48
60
|
## [0.1.7] - 2025-10-16
|
|
49
61
|
|
|
50
62
|
### Changed
|
|
63
|
+
|
|
51
64
|
- Version bump to maintain parity with @snowcone-app/ui 0.1.11
|
|
52
65
|
- No functional changes in this release
|
|
53
66
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
**JavaScript/TypeScript SDK for product mockups and print-on-demand**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A small, isomorphic SDK for fetching product data and building real-time
|
|
6
|
+
merchandise mockup URLs. Use it to visualize artwork on t-shirts, posters, mugs,
|
|
7
|
+
and more — in the browser or on the server.
|
|
8
|
+
|
|
9
|
+
📖 **Full documentation: [developers.snowcone.app/sdk](https://developers.snowcone.app/sdk)**
|
|
6
10
|
|
|
7
11
|
## Installation
|
|
8
12
|
|
|
@@ -33,9 +37,9 @@ const url = getMockupUrl('hoodie-black', {
|
|
|
33
37
|
// <img src={url} />
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
> The `
|
|
37
|
-
> Cloudinary's cloud name). It defaults to your `shop.id`. See
|
|
38
|
-
>
|
|
40
|
+
> The `shop` ID is your **publishable** token — public and safe to expose (like
|
|
41
|
+
> Cloudinary's cloud name). It defaults to your `shop.id`. See [Public or
|
|
42
|
+
> signed](#public-or-signed) below if you want to lock things down.
|
|
39
43
|
|
|
40
44
|
## Core Functions
|
|
41
45
|
|
|
@@ -94,7 +98,7 @@ const url = getMockupUrl(productCode: string, opts: {
|
|
|
94
98
|
asset?: string; // Single default-placement image (get-started shorthand)
|
|
95
99
|
design?: Design; // Multi-placement: { [placementKey]: Fill } — takes precedence over `asset`
|
|
96
100
|
options?: Record<string, string>; // Variant picks: { size: "m" } → opt.size=m
|
|
97
|
-
secret?: string; // Optional: per-shop secret → appends
|
|
101
|
+
secret?: string; // Optional: per-shop secret → appends a signed &signature (server only)
|
|
98
102
|
base?: string; // Optional: override the host (default img.snowcone.app)
|
|
99
103
|
width?: number; // Optional: display width
|
|
100
104
|
view?: string; // Optional: camera view / mockup scene (alias: `mockup`)
|
|
@@ -140,18 +144,16 @@ const cap = getMockupUrl('RQNU68', {
|
|
|
140
144
|
> accepted (it builds the same URL as `getMockupUrl(productCode, { asset, … })`).
|
|
141
145
|
> Prefer the code-first form above for new code.
|
|
142
146
|
|
|
143
|
-
###
|
|
147
|
+
### Public or signed
|
|
144
148
|
|
|
145
|
-
The default is open and frictionless
|
|
149
|
+
The default is open and frictionless. There's one decision to make up front:
|
|
146
150
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
| **L2** | asset allowlist | restrict which asset sources composite |
|
|
152
|
-
| **L3** | signed URLs | URL must carry a valid `&s` (HMAC of a per-shop secret) |
|
|
151
|
+
- **Public** — the Shop ID alone, safe to expose. Lock it to your **asset
|
|
152
|
+
origins** so a stolen ID can only re-render your own catalog.
|
|
153
|
+
- **Signed** — your server appends an `&signature` (HMAC of a per-shop secret),
|
|
154
|
+
so only you can mint valid URLs. The secret stays server-side.
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
To sign, pass `secret` (server-side only) and `getMockupUrl` appends the
|
|
155
157
|
verified `&signature`:
|
|
156
158
|
|
|
157
159
|
```typescript
|
|
@@ -163,25 +165,62 @@ const url = getMockupUrl('hoodie-black', {
|
|
|
163
165
|
});
|
|
164
166
|
```
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
See [Public or signed](https://developers.snowcone.app/get-started) for the full picture.
|
|
169
|
+
|
|
170
|
+
### Realtime: render a saved canvas, no pixel upload
|
|
167
171
|
|
|
168
|
-
|
|
172
|
+
`getMockupUrl` composites **one** artwork onto a product. For a full multi-layer
|
|
173
|
+
design — the kind the Snowcone canvas editor produces — use a **`RenderSession`**:
|
|
174
|
+
the browser sends a ~1 KB description of the canvas (or just a saved design's id)
|
|
175
|
+
over a WebSocket, and the server rasterizes it and **fetches the referenced assets
|
|
176
|
+
itself**. No per-placement PNG is ever uploaded, so it's fast on thin/mobile
|
|
177
|
+
clients. This is exactly how the Snowcone PDP renders mockups (see ADR-0079).
|
|
178
|
+
|
|
179
|
+
Authorize the session with a short-lived **grant**. Mint it server-side with a
|
|
180
|
+
secret API key (`sk_…`, scope `mockups:realtime`) so the key never reaches the
|
|
181
|
+
browser. Create the key — and find your publishable `shop.id` — on the
|
|
182
|
+
[API keys page](https://snowcone.app/api-keys):
|
|
169
183
|
|
|
170
184
|
```typescript
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
// YOUR backend route, e.g. POST /api/realtime/grant
|
|
186
|
+
import { mintRealtimeGrant } from '@snowcone-app/sdk';
|
|
187
|
+
|
|
188
|
+
export async function POST(req: Request) {
|
|
189
|
+
const { shop } = await req.json();
|
|
190
|
+
const grant = await mintRealtimeGrant({
|
|
191
|
+
apiKey: process.env.SNOWCONE_API_KEY!, // secret — server only
|
|
192
|
+
shop, // = shop.id (publishable)
|
|
179
193
|
});
|
|
180
|
-
|
|
181
|
-
return <img src={mockupUrl} alt="Product mockup" />;
|
|
194
|
+
return Response.json(grant); // { token, expiresAt }
|
|
182
195
|
}
|
|
183
196
|
```
|
|
184
197
|
|
|
198
|
+
In the browser, point a `RenderSession` at that proxy and render. Render a **saved**
|
|
199
|
+
design by id (no canvas JSON needed) or a live `serializeStateForServer(...)` state:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { RenderSession } from '@snowcone-app/sdk';
|
|
203
|
+
|
|
204
|
+
const session = new RenderSession({
|
|
205
|
+
shop: 'YOUR_SHOP_ID', // publishable, like Stripe pk_
|
|
206
|
+
grantUrl: '/api/realtime/grant', // your proxy above
|
|
207
|
+
product: { productId: 'BEEB77', mockupIds: ['Front'] },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
session.onMockups((results) => { img.src = results[0].imageUrl; });
|
|
211
|
+
|
|
212
|
+
// (a) render a SAVED canvas by id — ideal for fulfillment / agents
|
|
213
|
+
await session.renderSavedState('Front', 'design-state-id');
|
|
214
|
+
|
|
215
|
+
// (b) or render a live canvas state from @snowcone-app/canvas
|
|
216
|
+
// import { serializeStateForServer } from '@snowcone-app/canvas';
|
|
217
|
+
// await session.renderState('Front', serializeStateForServer(canvasState));
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
React apps can use the lower-level `useRealtimeMockup({ getToken })` hook from
|
|
221
|
+
`@snowcone-app/sdk/react` instead. Full walkthrough:
|
|
222
|
+
[Realtime server-side render](https://developers.snowcone.app/realtime).
|
|
223
|
+
|
|
185
224
|
## TypeScript Support
|
|
186
225
|
|
|
187
226
|
Full TypeScript definitions are included. Import types:
|
|
@@ -211,14 +250,14 @@ See: [@snowcone-app/ui on npm](https://www.npmjs.com/package/@snowcone-app/ui)
|
|
|
211
250
|
|
|
212
251
|
## Examples
|
|
213
252
|
|
|
214
|
-
- [
|
|
215
|
-
- [
|
|
253
|
+
- [Getting started](https://developers.snowcone.app/get-started)
|
|
254
|
+
- [Mockup URLs reference](https://developers.snowcone.app/mockups)
|
|
255
|
+
- [Live playground](https://developers.snowcone.app/playground)
|
|
216
256
|
|
|
217
257
|
## Support
|
|
218
258
|
|
|
219
|
-
- **
|
|
220
|
-
- **
|
|
221
|
-
- **Docs:** [developers.snowcone.app](https://developers.snowcone.app)
|
|
259
|
+
- **Docs:** [developers.snowcone.app/sdk](https://developers.snowcone.app/sdk)
|
|
260
|
+
- **Issues:** [GitHub Issues](https://github.com/snowcone-app/snowcone-monorepo/issues)
|
|
222
261
|
|
|
223
262
|
## License
|
|
224
263
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
// src/realtime/constants.ts
|
|
2
|
+
var REALTIME_WS_URL = "wss://cdn.snowcone.app/realtime";
|
|
3
|
+
|
|
1
4
|
// src/realtime/websocket.ts
|
|
2
5
|
var RealtimeMockupService = class {
|
|
3
|
-
constructor(wsUrl =
|
|
6
|
+
constructor(wsUrl = REALTIME_WS_URL) {
|
|
4
7
|
this.wsUrl = wsUrl;
|
|
5
8
|
}
|
|
6
9
|
ws = null;
|
|
@@ -469,6 +472,32 @@ var RealtimeMockupService = class {
|
|
|
469
472
|
}
|
|
470
473
|
return true;
|
|
471
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* Render a SAVED canvas by reference (ADR-0079 Phase 4). Sends a `stateId`
|
|
477
|
+
* instead of inline state; the server resolves it, fetches the referenced
|
|
478
|
+
* assets, and renders — nothing is uploaded and the client need not hold the
|
|
479
|
+
* canvas JSON. One-shot (no throttle). Results arrive like any other render.
|
|
480
|
+
*/
|
|
481
|
+
sendCanvasStateRef(placement, stateId) {
|
|
482
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
483
|
+
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
484
|
+
console.log(`[WS] sendCanvasStateRef BLOCKED for "${placement}": ${reason}`);
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
this.lastSentVersion = ++this.requestVersion;
|
|
488
|
+
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
489
|
+
const message = JSON.stringify({
|
|
490
|
+
type: "canvas_state",
|
|
491
|
+
placement,
|
|
492
|
+
version: this.lastSentVersion,
|
|
493
|
+
stateId
|
|
494
|
+
});
|
|
495
|
+
this.ws.send(message);
|
|
496
|
+
this.lastBlobSentAt = Date.now();
|
|
497
|
+
this.addLog(`Sent canvas stateId "${stateId}" for "${placement}" (v${this.lastSentVersion})`, "sent");
|
|
498
|
+
this.callbacks.onBlobSent?.(placement);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
472
501
|
sendCanvasStateImmediately(placement, state) {
|
|
473
502
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
474
503
|
console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
|
|
@@ -649,5 +678,6 @@ ${versionToSend}
|
|
|
649
678
|
};
|
|
650
679
|
|
|
651
680
|
export {
|
|
681
|
+
REALTIME_WS_URL,
|
|
652
682
|
RealtimeMockupService
|
|
653
683
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
DEFAULT_ARTWORK_URL: () => DEFAULT_ARTWORK_URL,
|
|
39
39
|
DEFAULT_ASPECT_RATIO: () => DEFAULT_ASPECT_RATIO,
|
|
40
40
|
DEFAULT_COLOR: () => DEFAULT_COLOR,
|
|
41
|
+
DEFAULT_GRANT_BASE: () => DEFAULT_GRANT_BASE,
|
|
41
42
|
DEFAULT_MOCKUP_BASE: () => DEFAULT_MOCKUP_BASE,
|
|
42
43
|
DEFAULT_PLACEMENT_DIMENSIONS: () => DEFAULT_PLACEMENT_DIMENSIONS,
|
|
43
44
|
Elements: () => Elements,
|
|
@@ -51,7 +52,9 @@ __export(index_exports, {
|
|
|
51
52
|
ProductLoader: () => ProductLoader,
|
|
52
53
|
ProductProps: () => ProductProps,
|
|
53
54
|
PropertyManager: () => PropertyManager,
|
|
55
|
+
REALTIME_WS_URL: () => REALTIME_WS_URL,
|
|
54
56
|
RealtimeMockupService: () => RealtimeMockupService,
|
|
57
|
+
RenderSession: () => RenderSession,
|
|
55
58
|
StandardComponents: () => StandardComponents,
|
|
56
59
|
StandardEvents: () => StandardEvents,
|
|
57
60
|
StateManager: () => StateManager,
|
|
@@ -73,6 +76,7 @@ __export(index_exports, {
|
|
|
73
76
|
createComponent: () => createComponent,
|
|
74
77
|
createContextProvider: () => createContextProvider,
|
|
75
78
|
createDesignForPlacements: () => createDesignForPlacements,
|
|
79
|
+
createDesignState: () => createDesignState,
|
|
76
80
|
createDevFetcher: () => createDevFetcher,
|
|
77
81
|
createErrorHandler: () => createErrorHandler,
|
|
78
82
|
createEventManager: () => createEventManager,
|
|
@@ -95,6 +99,7 @@ __export(index_exports, {
|
|
|
95
99
|
describeProductPrice: () => describeProductPrice,
|
|
96
100
|
describeProductTitle: () => describeProductTitle,
|
|
97
101
|
extractProductId: () => extractProductId,
|
|
102
|
+
fetchRealtimeGrant: () => fetchRealtimeGrant,
|
|
98
103
|
filterImagePlacements: () => filterImagePlacements,
|
|
99
104
|
findBestCombination: () => findBestCombination,
|
|
100
105
|
findClosestSnapPoint: () => findClosestSnapPoint,
|
|
@@ -118,6 +123,7 @@ __export(index_exports, {
|
|
|
118
123
|
isValidAlignment: () => isValidAlignment,
|
|
119
124
|
isValidTileCount: () => isValidTileCount,
|
|
120
125
|
listProducts: () => listProducts,
|
|
126
|
+
mintRealtimeGrant: () => mintRealtimeGrant,
|
|
121
127
|
mockupUrl: () => mockupUrl,
|
|
122
128
|
normalizeChoice: () => normalizeChoice,
|
|
123
129
|
prepareOptionRenderData: () => prepareOptionRenderData,
|
|
@@ -4797,9 +4803,12 @@ function createSvelteComponent(descriptor, svelte) {
|
|
|
4797
4803
|
};
|
|
4798
4804
|
}
|
|
4799
4805
|
|
|
4806
|
+
// src/realtime/constants.ts
|
|
4807
|
+
var REALTIME_WS_URL = "wss://cdn.snowcone.app/realtime";
|
|
4808
|
+
|
|
4800
4809
|
// src/realtime/websocket.ts
|
|
4801
4810
|
var RealtimeMockupService = class {
|
|
4802
|
-
constructor(wsUrl =
|
|
4811
|
+
constructor(wsUrl = REALTIME_WS_URL) {
|
|
4803
4812
|
this.wsUrl = wsUrl;
|
|
4804
4813
|
}
|
|
4805
4814
|
ws = null;
|
|
@@ -5268,6 +5277,32 @@ var RealtimeMockupService = class {
|
|
|
5268
5277
|
}
|
|
5269
5278
|
return true;
|
|
5270
5279
|
}
|
|
5280
|
+
/**
|
|
5281
|
+
* Render a SAVED canvas by reference (ADR-0079 Phase 4). Sends a `stateId`
|
|
5282
|
+
* instead of inline state; the server resolves it, fetches the referenced
|
|
5283
|
+
* assets, and renders — nothing is uploaded and the client need not hold the
|
|
5284
|
+
* canvas JSON. One-shot (no throttle). Results arrive like any other render.
|
|
5285
|
+
*/
|
|
5286
|
+
sendCanvasStateRef(placement, stateId) {
|
|
5287
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
5288
|
+
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
5289
|
+
console.log(`[WS] sendCanvasStateRef BLOCKED for "${placement}": ${reason}`);
|
|
5290
|
+
return false;
|
|
5291
|
+
}
|
|
5292
|
+
this.lastSentVersion = ++this.requestVersion;
|
|
5293
|
+
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
5294
|
+
const message = JSON.stringify({
|
|
5295
|
+
type: "canvas_state",
|
|
5296
|
+
placement,
|
|
5297
|
+
version: this.lastSentVersion,
|
|
5298
|
+
stateId
|
|
5299
|
+
});
|
|
5300
|
+
this.ws.send(message);
|
|
5301
|
+
this.lastBlobSentAt = Date.now();
|
|
5302
|
+
this.addLog(`Sent canvas stateId "${stateId}" for "${placement}" (v${this.lastSentVersion})`, "sent");
|
|
5303
|
+
this.callbacks.onBlobSent?.(placement);
|
|
5304
|
+
return true;
|
|
5305
|
+
}
|
|
5271
5306
|
sendCanvasStateImmediately(placement, state) {
|
|
5272
5307
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
5273
5308
|
console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
|
|
@@ -5447,6 +5482,198 @@ ${versionToSend}
|
|
|
5447
5482
|
}
|
|
5448
5483
|
};
|
|
5449
5484
|
|
|
5485
|
+
// src/realtime/grant.ts
|
|
5486
|
+
var DEFAULT_GRANT_BASE = "https://api.snowcone.app";
|
|
5487
|
+
async function mintRealtimeGrant(opts) {
|
|
5488
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
5489
|
+
const res = await f(`${opts.base ?? DEFAULT_GRANT_BASE}/realtime/grant`, {
|
|
5490
|
+
method: "POST",
|
|
5491
|
+
headers: {
|
|
5492
|
+
"Content-Type": "application/json",
|
|
5493
|
+
Authorization: `Bearer ${opts.apiKey}`
|
|
5494
|
+
},
|
|
5495
|
+
body: JSON.stringify({ shop: opts.shop })
|
|
5496
|
+
});
|
|
5497
|
+
if (!res.ok) {
|
|
5498
|
+
const detail = await res.text().catch(() => "");
|
|
5499
|
+
throw new Error(`realtime grant failed: ${res.status}${detail ? ` ${detail}` : ""}`);
|
|
5500
|
+
}
|
|
5501
|
+
return await res.json();
|
|
5502
|
+
}
|
|
5503
|
+
async function fetchRealtimeGrant(grantUrl, shop, fetchImpl) {
|
|
5504
|
+
const f = fetchImpl ?? globalThis.fetch;
|
|
5505
|
+
const res = await f(grantUrl, {
|
|
5506
|
+
method: "POST",
|
|
5507
|
+
headers: { "Content-Type": "application/json" },
|
|
5508
|
+
body: JSON.stringify({ shop })
|
|
5509
|
+
});
|
|
5510
|
+
if (!res.ok) throw new Error(`realtime grant failed: ${res.status}`);
|
|
5511
|
+
return await res.json();
|
|
5512
|
+
}
|
|
5513
|
+
|
|
5514
|
+
// src/realtime/session.ts
|
|
5515
|
+
var RenderSession = class {
|
|
5516
|
+
svc;
|
|
5517
|
+
opts;
|
|
5518
|
+
product;
|
|
5519
|
+
mockupCb = null;
|
|
5520
|
+
errorCb = null;
|
|
5521
|
+
ready = null;
|
|
5522
|
+
constructor(opts) {
|
|
5523
|
+
if (!opts.shop) throw new Error("RenderSession: `shop` is required");
|
|
5524
|
+
if (!opts.getToken && !opts.grantUrl) {
|
|
5525
|
+
throw new Error("RenderSession: provide `grantUrl` or `getToken` to authorize the session");
|
|
5526
|
+
}
|
|
5527
|
+
this.opts = opts;
|
|
5528
|
+
this.product = opts.product ?? null;
|
|
5529
|
+
this.svc = new RealtimeMockupService(opts.wsUrl ?? REALTIME_WS_URL);
|
|
5530
|
+
const getToken = opts.getToken ?? (() => fetchRealtimeGrant(opts.grantUrl, opts.shop, opts.fetch));
|
|
5531
|
+
this.svc.setTokenProvider(getToken);
|
|
5532
|
+
}
|
|
5533
|
+
/** Register a callback fired whenever rendered mockup URLs update. */
|
|
5534
|
+
onMockups(cb) {
|
|
5535
|
+
this.mockupCb = cb;
|
|
5536
|
+
return this;
|
|
5537
|
+
}
|
|
5538
|
+
/** Register an error callback (server/transport errors). */
|
|
5539
|
+
onError(cb) {
|
|
5540
|
+
this.errorCb = cb;
|
|
5541
|
+
return this;
|
|
5542
|
+
}
|
|
5543
|
+
/** Set or update the product. Sends config immediately if already connected. */
|
|
5544
|
+
setProduct(product) {
|
|
5545
|
+
this.product = product;
|
|
5546
|
+
this.svc.sendConfig(this.toConfig(product));
|
|
5547
|
+
}
|
|
5548
|
+
/**
|
|
5549
|
+
* Connect, authorize, and configure. Resolves once the session is ready to
|
|
5550
|
+
* accept {@link RenderSession.renderState}. Idempotent.
|
|
5551
|
+
*/
|
|
5552
|
+
connect() {
|
|
5553
|
+
if (this.ready) return this.ready;
|
|
5554
|
+
if (!this.product) {
|
|
5555
|
+
return Promise.reject(
|
|
5556
|
+
new Error("RenderSession: set a product (via options.product or setProduct) before connect()")
|
|
5557
|
+
);
|
|
5558
|
+
}
|
|
5559
|
+
const product = this.product;
|
|
5560
|
+
this.ready = new Promise((resolve, reject) => {
|
|
5561
|
+
let settled = false;
|
|
5562
|
+
this.svc.setCallbacks({
|
|
5563
|
+
onConnected: () => {
|
|
5564
|
+
this.svc.sendConfig(this.toConfig(product));
|
|
5565
|
+
},
|
|
5566
|
+
onConfigReceived: () => {
|
|
5567
|
+
if (!settled) {
|
|
5568
|
+
settled = true;
|
|
5569
|
+
resolve();
|
|
5570
|
+
}
|
|
5571
|
+
},
|
|
5572
|
+
onMockupRendered: () => {
|
|
5573
|
+
this.mockupCb?.(this.svc.getState().mockupResults);
|
|
5574
|
+
},
|
|
5575
|
+
onAllMockupsRendered: (results) => {
|
|
5576
|
+
this.mockupCb?.(results);
|
|
5577
|
+
},
|
|
5578
|
+
onError: (error) => {
|
|
5579
|
+
this.errorCb?.(error);
|
|
5580
|
+
if (!settled) {
|
|
5581
|
+
settled = true;
|
|
5582
|
+
reject(new Error(error));
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
});
|
|
5586
|
+
this.svc.sendConfig(this.toConfig(product));
|
|
5587
|
+
this.svc.connect();
|
|
5588
|
+
});
|
|
5589
|
+
return this.ready;
|
|
5590
|
+
}
|
|
5591
|
+
/**
|
|
5592
|
+
* Render a canvas state for a placement. The server rasterizes it and fetches
|
|
5593
|
+
* the assets referenced inside — no pixels are uploaded. Results arrive via
|
|
5594
|
+
* {@link RenderSession.onMockups}. Safe to call repeatedly (e.g. on every
|
|
5595
|
+
* canvas edit); pass `throttleMs` to debounce during live dragging.
|
|
5596
|
+
*
|
|
5597
|
+
* `placement` is the print-area NAME (e.g. `'Front'`, the product's
|
|
5598
|
+
* `placements[].label`) — a different id from the product's `mockupIds`, and it
|
|
5599
|
+
* must match an artboard in `state`. The server renders only once it has a state
|
|
5600
|
+
* for EVERY required placement, so on a multi-placement product call this once
|
|
5601
|
+
* per placement; a missing one surfaces as an `incomplete_canvas_placements`
|
|
5602
|
+
* error on {@link RenderSession.onError}.
|
|
5603
|
+
*
|
|
5604
|
+
* `state` is the flattened `serializeStateForServer()` shape — NOT the
|
|
5605
|
+
* un-flattened shape `createDesignState` takes.
|
|
5606
|
+
*/
|
|
5607
|
+
async renderState(placement, state, throttleMs = 0) {
|
|
5608
|
+
await this.connect();
|
|
5609
|
+
this.svc.sendCanvasState(placement, state, this.product?.mockupIds.length ?? 1, throttleMs);
|
|
5610
|
+
}
|
|
5611
|
+
/**
|
|
5612
|
+
* Render a SAVED canvas by reference (ADR-0079 Phase 4). The server resolves
|
|
5613
|
+
* the `stateId`, fetches the referenced assets, and renders — no pixels are
|
|
5614
|
+
* uploaded and you don't need to hold the canvas JSON. Ideal for re-rendering
|
|
5615
|
+
* a stored design (fulfillment, agents, server-to-server). Results arrive via
|
|
5616
|
+
* {@link RenderSession.onMockups}.
|
|
5617
|
+
*/
|
|
5618
|
+
async renderSavedState(placement, stateId) {
|
|
5619
|
+
await this.connect();
|
|
5620
|
+
this.svc.sendCanvasStateRef(placement, stateId);
|
|
5621
|
+
}
|
|
5622
|
+
/** Update only the mockup ids to render (reuses the current state). */
|
|
5623
|
+
updateMockupIds(mockupIds) {
|
|
5624
|
+
if (this.product) this.product = { ...this.product, mockupIds };
|
|
5625
|
+
this.svc.updateMockupIds(mockupIds);
|
|
5626
|
+
}
|
|
5627
|
+
/** Current rendered results. */
|
|
5628
|
+
getMockups() {
|
|
5629
|
+
return this.svc.getState().mockupResults;
|
|
5630
|
+
}
|
|
5631
|
+
/** Close the WebSocket and stop auto-renew. */
|
|
5632
|
+
close() {
|
|
5633
|
+
this.svc.disconnect();
|
|
5634
|
+
this.ready = null;
|
|
5635
|
+
}
|
|
5636
|
+
/** Escape hatch: the underlying low-level service. */
|
|
5637
|
+
get service() {
|
|
5638
|
+
return this.svc;
|
|
5639
|
+
}
|
|
5640
|
+
toConfig(product) {
|
|
5641
|
+
return {
|
|
5642
|
+
productId: product.productId,
|
|
5643
|
+
mockupIds: product.mockupIds,
|
|
5644
|
+
// shop comes from the grant token server-side; sent for completeness.
|
|
5645
|
+
shop: this.opts.shop,
|
|
5646
|
+
// WebSocketConfig requires these; the server treats empty variant as "none".
|
|
5647
|
+
variantId: product.variantId ?? "",
|
|
5648
|
+
width: product.width ?? 1e3,
|
|
5649
|
+
...product.placementSettings ? { placementSettings: product.placementSettings } : {}
|
|
5650
|
+
};
|
|
5651
|
+
}
|
|
5652
|
+
};
|
|
5653
|
+
|
|
5654
|
+
// src/realtime/design-state.ts
|
|
5655
|
+
var TRANSPARENT_1X1_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
5656
|
+
async function createDesignState(input) {
|
|
5657
|
+
const f = input.fetch ?? globalThis.fetch;
|
|
5658
|
+
const res = await f(`${input.base ?? DEFAULT_GRANT_BASE}/snowcone/design-states`, {
|
|
5659
|
+
method: "POST",
|
|
5660
|
+
headers: { "Content-Type": "application/json" },
|
|
5661
|
+
body: JSON.stringify({
|
|
5662
|
+
productId: input.productId,
|
|
5663
|
+
stateJson: JSON.stringify(input.state),
|
|
5664
|
+
previewBase64: input.previewBase64 ?? TRANSPARENT_1X1_PNG
|
|
5665
|
+
})
|
|
5666
|
+
});
|
|
5667
|
+
if (!res.ok) {
|
|
5668
|
+
const detail = await res.text().catch(() => "");
|
|
5669
|
+
throw new Error(`createDesignState failed: ${res.status}${detail ? ` ${detail}` : ""}`);
|
|
5670
|
+
}
|
|
5671
|
+
const body = await res.json();
|
|
5672
|
+
const ds = body?.designState;
|
|
5673
|
+
if (!ds?.id) throw new Error("createDesignState: response missing designState.id");
|
|
5674
|
+
return { stateId: ds.id, stateUrl: ds.stateUrl ?? "", previewUrl: ds.previewUrl ?? "" };
|
|
5675
|
+
}
|
|
5676
|
+
|
|
5450
5677
|
// src/index.ts
|
|
5451
5678
|
function getFetcher(config) {
|
|
5452
5679
|
return config?.fetcher || globalThis.fetch.bind(globalThis);
|
|
@@ -5534,6 +5761,7 @@ function createClient(config) {
|
|
|
5534
5761
|
DEFAULT_ARTWORK_URL,
|
|
5535
5762
|
DEFAULT_ASPECT_RATIO,
|
|
5536
5763
|
DEFAULT_COLOR,
|
|
5764
|
+
DEFAULT_GRANT_BASE,
|
|
5537
5765
|
DEFAULT_MOCKUP_BASE,
|
|
5538
5766
|
DEFAULT_PLACEMENT_DIMENSIONS,
|
|
5539
5767
|
Elements,
|
|
@@ -5547,7 +5775,9 @@ function createClient(config) {
|
|
|
5547
5775
|
ProductLoader,
|
|
5548
5776
|
ProductProps,
|
|
5549
5777
|
PropertyManager,
|
|
5778
|
+
REALTIME_WS_URL,
|
|
5550
5779
|
RealtimeMockupService,
|
|
5780
|
+
RenderSession,
|
|
5551
5781
|
StandardComponents,
|
|
5552
5782
|
StandardEvents,
|
|
5553
5783
|
StateManager,
|
|
@@ -5569,6 +5799,7 @@ function createClient(config) {
|
|
|
5569
5799
|
createComponent,
|
|
5570
5800
|
createContextProvider,
|
|
5571
5801
|
createDesignForPlacements,
|
|
5802
|
+
createDesignState,
|
|
5572
5803
|
createDevFetcher,
|
|
5573
5804
|
createErrorHandler,
|
|
5574
5805
|
createEventManager,
|
|
@@ -5591,6 +5822,7 @@ function createClient(config) {
|
|
|
5591
5822
|
describeProductPrice,
|
|
5592
5823
|
describeProductTitle,
|
|
5593
5824
|
extractProductId,
|
|
5825
|
+
fetchRealtimeGrant,
|
|
5594
5826
|
filterImagePlacements,
|
|
5595
5827
|
findBestCombination,
|
|
5596
5828
|
findClosestSnapPoint,
|
|
@@ -5614,6 +5846,7 @@ function createClient(config) {
|
|
|
5614
5846
|
isValidAlignment,
|
|
5615
5847
|
isValidTileCount,
|
|
5616
5848
|
listProducts,
|
|
5849
|
+
mintRealtimeGrant,
|
|
5617
5850
|
mockupUrl,
|
|
5618
5851
|
normalizeChoice,
|
|
5619
5852
|
prepareOptionRenderData,
|