@thefittingroom/shop-ui 5.0.19 → 5.0.20

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.
Files changed (2) hide show
  1. package/dist/index.js +89 -143
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -41030,18 +41030,6 @@ class FirestoreManager {
41030
41030
  });
41031
41031
  return unsubscribe;
41032
41032
  }
41033
- // listenToSubDoc subscribes to <parent>/<parentID>/<sub>/<subID>. Used for
41034
- // VTO compositions which live at users/{uid}/vto_compositions/{token}.
41035
- listenToSubDoc(parent, parentID, sub, subID, callback) {
41036
- const docRef = doc(this.firestore, parent, parentID, sub, subID);
41037
- return onSnapshot(docRef, (snap) => {
41038
- if (snap.exists()) {
41039
- callback(snap.data());
41040
- } else {
41041
- callback(null);
41042
- }
41043
- });
41044
- }
41045
41033
  async queryDocs(collectionName, constraints) {
41046
41034
  const q2 = query(this.collection(collectionName), ...constraints);
41047
41035
  return getDocs(q2);
@@ -41239,7 +41227,8 @@ async function execApiRequest(params) {
41239
41227
  useToken,
41240
41228
  method,
41241
41229
  endpoint,
41242
- body
41230
+ body,
41231
+ timeoutMs
41243
41232
  } = params;
41244
41233
  const url = `${baseUrl}${endpoint}`;
41245
41234
  if (useCache && responseCache[url]) {
@@ -41261,9 +41250,28 @@ async function execApiRequest(params) {
41261
41250
  if (body) {
41262
41251
  options.body = JSON.stringify(body);
41263
41252
  }
41264
- const response = await fetch(url, options);
41253
+ let timeoutHandle;
41254
+ if (timeoutMs && timeoutMs > 0) {
41255
+ const controller = new AbortController();
41256
+ options.signal = controller.signal;
41257
+ timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
41258
+ }
41259
+ let response;
41260
+ try {
41261
+ response = await fetch(url, options);
41262
+ } finally {
41263
+ if (timeoutHandle) clearTimeout(timeoutHandle);
41264
+ }
41265
41265
  if (!response.ok) {
41266
- throw new Error(`API request failed with status ${response.status}`);
41266
+ let detail = "";
41267
+ try {
41268
+ const errBody = await response.json();
41269
+ if (errBody && typeof errBody.error === "string") {
41270
+ detail = errBody.error;
41271
+ }
41272
+ } catch {
41273
+ }
41274
+ throw new Error(detail || `API request failed with status ${response.status}`);
41267
41275
  }
41268
41276
  let data;
41269
41277
  if (response.status === 204) {
@@ -41302,15 +41310,33 @@ async function getStyleCategoryGroups() {
41302
41310
  endpoint: "/v1/style-category-groups"
41303
41311
  });
41304
41312
  }
41305
- async function requestVto(items) {
41306
- return await execApiRequest({
41313
+ const inFlightVtoRequests = /* @__PURE__ */ new Map();
41314
+ function canonicalVtoKey(items) {
41315
+ const normalized = items.map((i) => ({
41316
+ csa: i.colorway_size_asset_id,
41317
+ untucked: !!i.untucked
41318
+ })).sort((a, b) => a.csa - b.csa || Number(a.untucked) - Number(b.untucked));
41319
+ return JSON.stringify(normalized);
41320
+ }
41321
+ function requestVto(items) {
41322
+ const key = canonicalVtoKey(items);
41323
+ const existing = inFlightVtoRequests.get(key);
41324
+ if (existing) {
41325
+ return existing;
41326
+ }
41327
+ const promise = execApiRequest({
41307
41328
  useToken: true,
41308
41329
  method: "POST",
41309
41330
  endpoint: "/v1/vto-compositions",
41310
41331
  body: {
41311
41332
  items
41312
- }
41333
+ },
41334
+ timeoutMs: getStaticData().config.api.vtoTimeoutMs
41335
+ }).finally(() => {
41336
+ inFlightVtoRequests.delete(key);
41313
41337
  });
41338
+ inFlightVtoRequests.set(key, promise);
41339
+ return promise;
41314
41340
  }
41315
41341
  const recordCache = {};
41316
41342
  async function getStyleByExternalId(brandId, externalId) {
@@ -42969,67 +42995,44 @@ function TryOnView({
42969
42995
  ] }) })
42970
42996
  ] });
42971
42997
  }
42972
- const logger$a = getLogger("overlays/fitting-room/use-vto-subscriptions");
42998
+ const logger$a = getLogger("overlays/fitting-room/use-vto-requests");
42973
42999
  const NON_PRIORITY_REQUEST_DELAY_MS = 500;
42974
43000
  function outfitKey(items) {
42975
43001
  return items.map((i) => `${i.colorway_size_asset_id}:${i.untucked ? 1 : 0}`).sort().join("|");
42976
43002
  }
42977
- function useVtoSubscriptions() {
42978
- const compositionTokensRef = reactExports.useRef(/* @__PURE__ */ new Map());
42979
- const subscriptionsRef = reactExports.useRef(/* @__PURE__ */ new Map());
42980
- const [vtoDocsByToken, setVtoDocsByToken] = reactExports.useState({});
43003
+ function useVtoRequests() {
43004
+ const [framesByKey, setFramesByKey] = reactExports.useState({});
43005
+ const requestedKeysRef = reactExports.useRef(/* @__PURE__ */ new Set());
42981
43006
  const lastPriorityTimeRef = reactExports.useRef(null);
42982
43007
  const [lastError, setLastError] = reactExports.useState(null);
42983
43008
  const clearError = reactExports.useCallback(() => setLastError(null), []);
42984
- const subscribe = reactExports.useCallback((token2, key) => {
42985
- if (subscriptionsRef.current.has(token2)) return;
42986
- const uid = getAuthManager().getAuthUser()?.uid;
42987
- if (!uid) {
42988
- logger$a.logWarn("subscribe skipped: no uid", {
42989
- token: token2,
42990
- key
42991
- });
42992
- return;
42993
- }
42994
- const unsub = getFirestoreManager().listenToSubDoc("users", uid, "vto_compositions", token2, (data) => {
42995
- if (data?.frames?.length) {
42996
- logger$a.logDebug("VTO frames ready", {
42997
- key,
42998
- token: token2,
42999
- count: data.frames.length
43000
- });
43001
- }
43002
- if (data?.error) {
43003
- setLastError(new Error(data.error));
43004
- }
43005
- setVtoDocsByToken((prev2) => ({
43006
- ...prev2,
43007
- [token2]: data ?? {}
43008
- }));
43009
- });
43010
- subscriptionsRef.current.set(token2, unsub);
43011
- }, []);
43012
43009
  const request = reactExports.useCallback((items, priority) => {
43013
43010
  if (items.length === 0) return;
43014
43011
  const key = outfitKey(items);
43015
- if (compositionTokensRef.current.has(key)) return;
43012
+ if (requestedKeysRef.current.has(key)) return;
43016
43013
  const exec = () => {
43017
- compositionTokensRef.current.set(key, "");
43014
+ requestedKeysRef.current.add(key);
43018
43015
  logger$a.logDebug("Requesting VTO composition", {
43019
43016
  key,
43020
43017
  items,
43021
43018
  priority
43022
43019
  });
43023
43020
  requestVto(items).then((resp) => {
43024
- compositionTokensRef.current.set(key, resp.token);
43025
- subscribe(resp.token, key);
43021
+ logger$a.logDebug("VTO frames ready", {
43022
+ key,
43023
+ count: resp.frames.length
43024
+ });
43025
+ setFramesByKey((prev2) => ({
43026
+ ...prev2,
43027
+ [key]: resp.frames
43028
+ }));
43026
43029
  }).catch((error) => {
43027
43030
  logger$a.logError("VTO request failed", {
43028
43031
  error,
43029
43032
  items,
43030
43033
  key
43031
43034
  });
43032
- compositionTokensRef.current.delete(key);
43035
+ requestedKeysRef.current.delete(key);
43033
43036
  setLastError(error instanceof Error ? error : new Error(String(error)));
43034
43037
  });
43035
43038
  };
@@ -43050,31 +43053,15 @@ function useVtoSubscriptions() {
43050
43053
  } else {
43051
43054
  exec();
43052
43055
  }
43053
- }, [subscribe]);
43054
- reactExports.useEffect(() => {
43055
- return () => {
43056
- subscriptionsRef.current.forEach((unsub) => {
43057
- try {
43058
- unsub();
43059
- } catch (e) {
43060
- logger$a.logError("Error unsubscribing", {
43061
- error: e
43062
- });
43063
- }
43064
- });
43065
- subscriptionsRef.current.clear();
43066
- };
43067
43056
  }, []);
43068
43057
  const framesForOutfit = reactExports.useCallback((items) => {
43069
43058
  if (items.length === 0) return null;
43070
43059
  const key = outfitKey(items);
43071
- const token2 = compositionTokensRef.current.get(key);
43072
- if (!token2) return null;
43073
- const doc2 = vtoDocsByToken[token2];
43074
- if (!doc2?.frames?.length) return null;
43060
+ const frames = framesByKey[key];
43061
+ if (!frames || frames.length === 0) return null;
43075
43062
  const baseUrl2 = getStaticData().config.frames.baseUrl;
43076
- return doc2.frames.map((u) => applyFrameBaseUrl(u, baseUrl2));
43077
- }, [vtoDocsByToken]);
43063
+ return frames.map((u) => applyFrameBaseUrl(u, baseUrl2));
43064
+ }, [framesByKey]);
43078
43065
  return {
43079
43066
  request,
43080
43067
  framesForOutfit,
@@ -43131,7 +43118,7 @@ function FittingRoomOverlay() {
43131
43118
  framesForOutfit,
43132
43119
  lastError: vtoError,
43133
43120
  clearError: clearVtoError
43134
- } = useVtoSubscriptions();
43121
+ } = useVtoRequests();
43135
43122
  reactExports.useEffect(() => {
43136
43123
  const savedScrollY = window.scrollY;
43137
43124
  if (savedScrollY > 0) window.scrollTo(0, 0);
@@ -44155,9 +44142,8 @@ function VtoSingleOverlay() {
44155
44142
  const [selectedSizeLabel, setSelectedSizeLabel] = reactExports.useState(null);
44156
44143
  const [selectedColorLabel, setSelectedColorLabel] = reactExports.useState(null);
44157
44144
  const [modalStyle, setModalStyle] = reactExports.useState({});
44158
- const compositionTokensRef = reactExports.useRef(/* @__PURE__ */ new Map());
44159
- const [vtoDocsByToken, setVtoDocsByToken] = reactExports.useState({});
44160
- const subscriptionsRef = reactExports.useRef(/* @__PURE__ */ new Map());
44145
+ const [framesByKey, setFramesByKey] = reactExports.useState({});
44146
+ const requestedKeysRef = reactExports.useRef(/* @__PURE__ */ new Set());
44161
44147
  const lastPriorityVtoRequestTimeRef = reactExports.useRef(null);
44162
44148
  reactExports.useEffect(() => {
44163
44149
  if (!userIsLoggedIn) {
@@ -44332,7 +44318,7 @@ function VtoSingleOverlay() {
44332
44318
  const requestVto$1 = reactExports.useCallback((sizeColorRecord, priority) => {
44333
44319
  const csaId = sizeColorRecord.colorwaySizeAssetId;
44334
44320
  const key = compositionKey(csaId);
44335
- if (compositionTokensRef.current.has(key)) {
44321
+ if (requestedKeysRef.current.has(key)) {
44336
44322
  return;
44337
44323
  }
44338
44324
  function executeRequest() {
@@ -44341,19 +44327,23 @@ function VtoSingleOverlay() {
44341
44327
  priority,
44342
44328
  sizeColorRecord
44343
44329
  });
44344
- compositionTokensRef.current.set(key, "");
44330
+ requestedKeysRef.current.add(key);
44345
44331
  requestVto([{
44346
44332
  colorway_size_asset_id: csaId,
44347
44333
  untucked: false
44348
44334
  }]).then((resp) => {
44349
- compositionTokensRef.current.set(key, resp.token);
44350
- subscribeToCompositionToken(resp.token, sizeColorRecord.sku);
44335
+ logger$6.timerEnd(`requestVto_${sizeColorRecord.sku}`);
44336
+ logger$6.logTimer(`requestVto_${sizeColorRecord.sku}`, `{{ts}} - VTO data is loaded for sku: ${sizeColorRecord.sku}`);
44337
+ setFramesByKey((prev2) => ({
44338
+ ...prev2,
44339
+ [key]: resp.frames
44340
+ }));
44351
44341
  }).catch((error) => {
44352
44342
  logger$6.logError(`Error requesting VTO for sku: ${sizeColorRecord.sku}`, {
44353
44343
  error,
44354
44344
  sizeColorRecord
44355
44345
  });
44356
- compositionTokensRef.current.delete(key);
44346
+ requestedKeysRef.current.delete(key);
44357
44347
  });
44358
44348
  }
44359
44349
  {
@@ -44377,45 +44367,6 @@ function VtoSingleOverlay() {
44377
44367
  }
44378
44368
  executeRequest();
44379
44369
  }, []);
44380
- const subscribeToCompositionToken = reactExports.useCallback((token2, sku) => {
44381
- if (subscriptionsRef.current.has(token2)) {
44382
- return;
44383
- }
44384
- const authManager2 = getAuthManager();
44385
- const uid = authManager2.getAuthUser()?.uid;
44386
- if (!uid) {
44387
- logger$6.logWarn(`subscribe to vto composition skipped: no uid`, {
44388
- token: token2,
44389
- sku
44390
- });
44391
- return;
44392
- }
44393
- const unsub = getFirestoreManager().listenToSubDoc("users", uid, "vto_compositions", token2, (data) => {
44394
- if (data && data.frames && data.frames.length > 0) {
44395
- logger$6.timerEnd(`requestVto_${sku}`);
44396
- logger$6.logTimer(`requestVto_${sku}`, `{{ts}} - VTO data is loaded for sku: ${sku}`);
44397
- }
44398
- setVtoDocsByToken((prev2) => ({
44399
- ...prev2,
44400
- [token2]: data ?? {}
44401
- }));
44402
- });
44403
- subscriptionsRef.current.set(token2, unsub);
44404
- }, []);
44405
- reactExports.useEffect(() => {
44406
- return () => {
44407
- subscriptionsRef.current.forEach((unsub) => {
44408
- try {
44409
- unsub();
44410
- } catch (e) {
44411
- logger$6.logError("Error unsubscribing from vto composition", {
44412
- error: e
44413
- });
44414
- }
44415
- });
44416
- subscriptionsRef.current.clear();
44417
- };
44418
- }, []);
44419
44370
  reactExports.useEffect(() => {
44420
44371
  if (selectedColorSizeRecord) {
44421
44372
  requestVto$1(selectedColorSizeRecord, true);
@@ -44435,32 +44386,24 @@ function VtoSingleOverlay() {
44435
44386
  }
44436
44387
  }
44437
44388
  }, [requestVto$1, vtoProductData, selectedColorLabel]);
44438
- const vtoData = reactExports.useMemo(() => {
44389
+ const frameUrls = reactExports.useMemo(() => {
44439
44390
  if (!selectedColorSizeRecord) {
44440
44391
  return null;
44441
44392
  }
44442
44393
  const key = compositionKey(selectedColorSizeRecord.colorwaySizeAssetId);
44443
- const token2 = compositionTokensRef.current.get(key);
44444
- if (!token2) {
44445
- return null;
44446
- }
44447
- const doc2 = vtoDocsByToken[token2];
44448
- if (!doc2 || !doc2.frames || doc2.frames.length === 0) {
44394
+ const frames = framesByKey[key];
44395
+ if (!frames || frames.length === 0) {
44449
44396
  return null;
44450
44397
  }
44451
44398
  logger$6.logDebug(`{{ts}} - Displaying VTO for sku: ${selectedColorSizeRecord.sku}`);
44452
- return doc2;
44453
- }, [selectedColorSizeRecord, vtoDocsByToken]);
44454
- const frameUrls = reactExports.useMemo(() => {
44455
- if (!vtoData?.frames) return null;
44456
44399
  const baseUrl2 = getStaticData().config.frames.baseUrl;
44457
- const rewritten = vtoData.frames.map((u) => applyFrameBaseUrl(u, baseUrl2));
44400
+ const rewritten = frames.map((u) => applyFrameBaseUrl(u, baseUrl2));
44458
44401
  rewritten.forEach((url) => {
44459
44402
  const img = new Image();
44460
44403
  img.src = url;
44461
44404
  });
44462
44405
  return rewritten;
44463
- }, [vtoData]);
44406
+ }, [selectedColorSizeRecord, framesByKey]);
44464
44407
  const handleSignOutClick = reactExports.useCallback(() => {
44465
44408
  closeOverlay();
44466
44409
  const authManager2 = getAuthManager();
@@ -45802,9 +45745,9 @@ const SHARED_CONFIG = {
45802
45745
  appGooglePlayUrl: "https://play.google.com/store/apps/details?id=com.thefittingroom.marketplace"
45803
45746
  },
45804
45747
  build: {
45805
- version: `${"5.0.19"}`,
45806
- commitHash: `${"46d41c2"}`,
45807
- date: `${"2026-05-15T19:28:45.079Z"}`
45748
+ version: `${"5.0.20"}`,
45749
+ commitHash: `${"ec50778"}`,
45750
+ date: `${"2026-05-16T14:14:47.264Z"}`
45808
45751
  }
45809
45752
  };
45810
45753
  const CONFIGS = {
@@ -45822,7 +45765,8 @@ const CONFIGS = {
45822
45765
  measurementId: "G-B7GDQ1Y9LL"
45823
45766
  },
45824
45767
  api: {
45825
- baseUrl: "https://tfr.dev.thefittingroom.xyz"
45768
+ baseUrl: "https://tfr.dev.thefittingroom.xyz",
45769
+ vtoTimeoutMs: 12e4
45826
45770
  },
45827
45771
  asset: {
45828
45772
  baseUrl: "https://assets.dev.thefittingroom.xyz/shop-sdk/assets/v5"
@@ -45849,7 +45793,8 @@ const CONFIGS = {
45849
45793
  measurementId: "G-XH9VV5N6EW"
45850
45794
  },
45851
45795
  api: {
45852
- baseUrl: "https://tfr.p.thefittingroom.xyz"
45796
+ baseUrl: "https://tfr.p.thefittingroom.xyz",
45797
+ vtoTimeoutMs: 12e4
45853
45798
  },
45854
45799
  asset: {
45855
45800
  baseUrl: "https://assets.p.thefittingroom.xyz/shop-sdk/assets/v5"
@@ -45876,7 +45821,8 @@ const CONFIGS = {
45876
45821
  measurementId: "G-B7GDQ1Y9LL"
45877
45822
  },
45878
45823
  api: {
45879
- baseUrl: "https://minecraftbadapple.com/api"
45824
+ baseUrl: "https://minecraftbadapple.com/api",
45825
+ vtoTimeoutMs: 12e4
45880
45826
  },
45881
45827
  asset: {
45882
45828
  baseUrl: "http://minecraftbadapple.com/s3/tfr-assets-dev/shop-sdk/assets/v5"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thefittingroom/shop-ui",
3
- "version": "5.0.19",
3
+ "version": "5.0.20",
4
4
  "description": "the fitting room UI library",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",