PageSourceSearch

https://www.maryruthorganics.com/apps/bubblehouse/bubblehouse.js

js maryruthorganics.com collected 2026-09-24 17:51:09 UTC 130,299 bytes, 4,222 lines download raw bytes

1(function() {
2let Bubblehouse = window.Bubblehouse = Object.assign(window.Bubblehouse ?? {}, {
3  actionQueryParam: "bh-action",
4  verbose: location.search.match(/bh-verbose/),
5  root: "https://app.bubblehouse.com/s/shopify-maryruth-organics",
6  data: {"ppc":0.01,"ppcs":0.01,"prm":"down","tester":true,"popups":{"History1":{"style":{"dialogStyle":"width: 80vw; height: 80vh; max-width: 700px; max-height: 950px; padding: 0; border: none; border-radius: 0; overflow: hidden; box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.2)"},"trigger":{"type":"manual"}},"Unredeemed1":{"style":{"dialogStyle":"width: 80vw; height: 80vh; max-width: 700px; max-height: 950px; padding: 0; border: none; border-radius: 0; overflow: hidden; box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.2)"},"trigger":{"type":"manual"}},"message":{"style":{"dialogStyle":"width: min(calc(100vw - 20px), 500px); height: 80vh; max-width: 500px; max-height: 300px; padding: 0; border: none; border-radius: 10px; overflow: hidden; box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.2)"},"trigger":{"type":"manual"}}},"popupMode":"outer/inner","popupUrl":"https://www.maryruthorganics.com/apps/bubblehouse/blocks/POPUPNAME","callUrl":"https://app.bubblehouse.com/api/v2023061/shopify-maryruth-organics/FUNCNAME","cartErrorMessage":"We couldn’t update your cart. Please try again.","cartCouponErrorMessage":"We couldn’t update your cart. Click the coupon code on your rewards page to retry, or enter this code at checkout:","userPointBalance":0,"userPointBalanceStr":"0","userPointBalanceValue":"0.000000","userPointBalanceValueStr":"$0.00","userPointBalanceAvail":0,"userPointBalanceAvailStr":"0","userPointBalanceAvailValue":"0.000000","userPointBalanceAvailValueStr":"$0.00","userTierName":""},
7  isInner: false,
8  isAdmin: false,
9  extra: {"actionProxyPaths":["/apps/bubblehouse"],"actionShopSlugs":["shopify-maryruth-organics"],"iframeStyle":""}
10});
11try {
12  Bubblehouse.data.authToken = Bubblehouse.data.authToken || localStorage.getItem('bubblehouse-auth-token');
13} catch {
14  if (Bubblehouse.verbose) console.log('[Bubblehouse] bubblelogin relay source=bootstrap storage=blocked');
15}
16Bubblehouse.isLoggedIn = !!Bubblehouse.data.authToken;
17
18/* blockClientDispatcher.js */
19;(function() {
20window.addEventListener('message', handleMessage);
21Bubblehouse.handleBlockClientCall = handleBlockClientCall;
22
23const sandboxedAdminPreviewFrameCalls = new Set(['Resize1', 'Title1']);
24
25function handleMessage(ev) {
26  let data = ev.data;
27  if (!data) return;
28  Bubblehouse.handleBlockClientCall(data, ev)
29}
30
31function handleBlockClientCall(data, ev = null) {
32  let call = data.bubblehouseBlockClientCall;
33  if (!call) return;
34
35  const adminSourceFrame = adminMessageSourceFrame(ev);
36  if (adminSourceFrame === null) {
37    console.warn(`[Bubblehouse] BlockClient.${call} ignored from unknown admin message source`);
38    return
39  }
40  if (isSandboxedAdminPreviewFrame(adminSourceFrame) && !sandboxedAdminPreviewFrameCalls.has(call)) {
41    console.warn(`[Bubblehouse] BlockClient.${call} ignored from sandboxed admin preview frame`);
42    return
43  }
44
45  if (call === "RunCustomJS") {
46    if (Bubblehouse.verbose) console.log(`[Bubblehouse] BlockClient.${call}:`, data);
47    Bubblehouse.handleRunCustomJS(data, ev);
48    return
49  }
50
51  let instance = data.instance;
52
53  let el = (instance === '#self' ? document : document.getElementById(instance))
54  if (el) {
55    if (Bubblehouse.verbose) console.log(`[Bubblehouse] BlockClient.${call} from ${instance}:`, data);
56    let detail = ev ? Object.assign({}, data, { sourceWindow: ev.source, sourceOrigin: ev.origin }) : data;
57    el.dispatchEvent(new CustomEvent('bhclient:' + call, { detail, bubbles: true }));
58  } else {
59    console.warn(`[Bubblehouse] invalid call BlockClient.${call} from unknown instance ${instance}:`, data);
60  }
61}
62
63function isSandboxedAdminPreviewFrame(frame) {
64  return frame?.hasAttribute('sandbox') && frame.getAttribute('data-block-preview-frame') === 'true'
65}
66
67function adminMessageSourceFrame(ev) {
68  if (!Bubblehouse.isAdmin || !ev || !ev.source) return undefined;
69  for (const frame of document.querySelectorAll('iframe')) {
70    if (frame.contentWindow === ev.source) {
71      return frame
72    }
73  }
74  return null
75}
76})() || true;
77
78/* handleRunCustomJSStub.js */
79;(function() {
80Bubblehouse.handleRunCustomJS = function handleRunCustomJS(data, ev = null) {
81  console.error('[Bubblehouse] BlockClient.RunCustomJS: JS exec disabled');
82};
83})() || true;
84
85/* flashCommands.js */
86;(function() {
87Bubblehouse.processFlashCommands = processFlashCommands;
88
89function processFlashCommands(flash, options = {}) {
90  options = options || {}
91  const commands = Array.isArray(flash?.c) ? flash.c : Array.isArray(flash?.commands) ? flash.commands : []
92  for (const command of commands) {
93    processFlashCommand(command, options)
94  }
95}
96
97function processFlashCommand(command, options) {
98  const name = command?.n || command?.name
99  const params = command?.p || command?.params || {}
100  const instance = currentBlockClientInstance(options)
101
102  let message
103  switch (name) {
104    case 'ApplyCoupon':
105      if (!params.code) {
106        return
107      }
108      message = {
109        bubblehouseBlockClientCall: 'ApplyDiscountCode1',
110        instance,
111        code: params.code,
112      }
113      break
114
115    case 'AddProductToCart1':
116      if (!params.variantId || !params.discountCode) {
117        return
118      }
119      message = {
120        bubblehouseBlockClientCall: 'AddProductToCart1',
121        instance,
122        variantId: params.variantId,
123        quantity: 1,
124        discountCode: params.discountCode,
125        cartUrl: params.cartUrl || null,
126      }
127      break
128
129    default:
130      return
131  }
132
133  if (typeof Bubblehouse.sendBlockClientCall === 'function') {
134    Bubblehouse.sendBlockClientCall(message)
135  } else if (window.parent !== window) {
136    window.parent.postMessage(message, '*')
137  } else {
138    console.log(`[Bubblehouse] Block Client call ignored because of no viable transport: ${message.bubblehouseBlockClientCall}`)
139  }
140}
141
142function currentBlockClientInstance(options) {
143  return options.instance ||
144    instanceFromURL() ||
145    dataValue(document.body, 'instance') ||
146    firstAttributeValue('data-resizeparentiframe-frame-name-value') ||
147    firstAttributeValue('data-blockclient-instance-value') ||
148    '#self'
149}
150
151function instanceFromURL() {
152  return new URL(window.location.href).searchParams.get('instance') || ''
153}
154
155function dataValue(el, key) {
156  if (!el) {
157    return ''
158  }
159  return el.dataset?.[key] || el.getAttribute?.(`data-${key}`) || ''
160}
161
162function firstAttributeValue(name) {
163  for (const el of document.querySelectorAll('*')) {
164    if (el.hasAttribute?.(name)) {
165      return el.getAttribute(name) || ''
166    }
167  }
168  return ''
169}
170})() || true;
171
172/* shopify/marketQueryParams.js */
173const marketQueryParamNames = [
174  "market",
175  "market_domain",
176  "market_path",
177  "external_path",
178  "currency",
179  "locale",
180  "country",
181]
182
183function shopifyRootPath() {
184  let root = window.Shopify?.routes?.root
185  if (!root) {
186    return ""
187  }
188  try {
189    return new URL(root, window.location.href).pathname
190  } catch {
191    return root
192  }
193}
194
195function collectCurrentMarketQueryParams() {
196  let pageUrl = new URL(window.location.href)
197  let queryParams = {}
198
199  for (let name of marketQueryParamNames) {
200    let value = pageUrl.searchParams.get(name)
201    if (value) {
202      queryParams[name] = value
203    }
204  }
205
206  if (window.Shopify) {
207    if (!queryParams.market_domain && window.location.host) {
208      queryParams.market_domain = window.location.host
209    }
210    if (!queryParams.market_path) {
211      let rootPath = shopifyRootPath()
212      if (rootPath) {
213        queryParams.market_path = rootPath
214      }
215    }
216    if (!queryParams.external_path && window.location.pathname) {
217      queryParams.external_path = window.location.pathname
218    }
219    if (!queryParams.currency && window.Shopify.currency?.active) {
220      queryParams.currency = window.Shopify.currency.active
221    }
222    if (!queryParams.locale) {
223      let locale = (window.Shopify.locale || "").trim()
224      if (locale) {
225        queryParams.locale = locale
226      }
227    }
228    if (!queryParams.country && window.Shopify.country) {
229      queryParams.country = window.Shopify.country
230    }
231  }
232
233  return queryParams
234}
235
236
237/* util/bubblehouseFrames.js */
238function collectBubblehouseFrames(root) {
239  return collectBubblehouseDocumentFrames(root).concat(collectBubblehouseBlockFrames(root))
240}
241
242function collectBubblehouseDocumentFrames(root) {
243  try {
244    return Array.from(root.querySelectorAll('iframe'))
245  } catch {
246    return []
247  }
248}
249
250function collectBubblehouseBlockFrames(root) {
251  let result = []
252  for (let host of collectBubblehouseBlockHosts(root)) {
253    if (host.shadowRoot) {
254      result.push(...collectBubblehouseDocumentFrames(host.shadowRoot))
255    }
256  }
257  return result
258}
259
260function collectBubblehouseBlockHosts(root) {
261  try {
262    return Array.from(root.querySelectorAll('bubblehouse-block'))
263  } catch {
264    return []
265  }
266}
267
268function bubblehouseShopSlug(url) {
269  let parts = url.pathname.split('/').filter(Boolean)
270  if (parts[0] === 's' && parts[1] && isBubblehousePageKind(parts[2]) && parts[3]) {
271    return parts[1]
272  }
273  if (isBubblehousePageKind(parts[0]) && parts[1]) {
274    if (/^v\d+$/.test(parts[1])) {
275      return parts[2] && parts[3] ? parts[2] : ''
276    }
277    return parts[2] ? parts[1] : ''
278  }
279  return ''
280}
281
282function isBubblehouseAppBlockOrPageURL(url) {
283  return bubblehouseShopSlug(url) !== ''
284}
285
286function isBubblehouseAuthFrameURL(url, appOrigin, pageOrigin) {
287  return isBubblehouseAppFrameURL(url, appOrigin) ||
288    isShopifyBubblehousePageOrBlockURL(url, pageOrigin)
289}
290
291function isBubblehouseActionFrameURL(url, appOrigin, pageOrigin, shopSlugs, proxyPaths) {
292  let appFrame = bubblehouseActionAppFrame(url);
293  return (url.origin === appOrigin &&
294      appFrame !== null &&
295      shopSlugs.includes(appFrame.shopSlug) &&
296      isBubblehouseActionPageName(appFrame.pageName)) ||
297    (url.origin === pageOrigin &&
298      isBubblehouseActionPageName(bubblehouseActionProxyPageName(url, proxyPaths)))
299}
300
301function isBubblehouseAppFrameURL(url, appOrigin) {
302  return appOrigin !== "" &&
303    url.origin === appOrigin &&
304    isBubblehouseAppBlockOrPageURL(url)
305}
306
307function isShopifyBubblehousePageOrBlockURL(url, pageOrigin) {
308  if (url.origin !== pageOrigin) {
309    return false
310  }
311  let parts = url.pathname.split('/').filter(Boolean)
312  return parts[0] === "apps" &&
313    parts[1] === "bubblehouse" &&
314    !!parts[2] &&
315    parts[3] === "content"
316}
317
318function isBubblehousePageKind(kind) {
319  return kind === 'blocks' || kind === 'pages'
320}
321
322function bubblehouseAppOrigin() {
323  for (let value of [Bubblehouse?.data?.callUrl, Bubblehouse?.root]) {
324    if (!value) {
325      continue
326    }
327    try {
328      return new URL(value, location.href).origin
329    } catch {
330    }
331  }
332  return ""
333}
334
335function bubblehouseActionAppFrame(url) {
336  let parts = url.pathname.split('/').filter(Boolean)
337  if (parts.length === 4 && parts[0] === 's' && parts[1] && parts[2] === 'blocks' && parts[3]) {
338    return { shopSlug: parts[1], pageName: parts[3] }
339  }
340  if (parts.length === 4 && parts[0] === 'blocks' && /^v\d+$/.test(parts[1]) && parts[2] && parts[3]) {
341    return { shopSlug: parts[2], pageName: parts[3] }
342  }
343  return null
344}
345
346function bubblehouseActionProxyPageName(url, proxyPaths) {
347  let parts = url.pathname.split('/').filter(Boolean)
348  for (let proxyPath of proxyPaths) {
349    let proxyParts;
350    try {
351      proxyParts = new URL(proxyPath, url.origin).pathname.split('/').filter(Boolean)
352    } catch {
353      continue
354    }
355    if (proxyParts.length === 0) {
356      continue
357    }
358    let proxyStart = parts.length - proxyParts.length - 2;
359    if (proxyStart < 0 || parts[parts.length - 1] !== 'content') {
360      continue
361    }
362    if (proxyParts.every((part, index) => parts[proxyStart + index] === part)) {
363      return parts[parts.length - 2] || ''
364    }
365  }
366  return ''
367}
368
369function isBubblehouseActionPageName(name) {
370  return name === 'rewards' || name === 'Rewards7'
371}
372
373function bubblehouseRootShopSlug(root, baseURL) {
374  try {
375    let parts = new URL(root, baseURL).pathname.split('/').filter(Boolean)
376    return parts[0] === 's' && parts[1] ? parts[1] : ''
377  } catch {
378    return ''
379  }
380}
381
382
383/* util/nextId.js */
384let nextId = Bubblehouse.nextId = (function () {
385  let lastId = 0;
386
387  return function nextId() {
388    lastId++;
389    return lastId;
390  }
391})()
392
393
394/* util/simpleglob.js */
395const GlobStar_URL = '[^/]*';
396
397// Provides simple glob-like match function with the following rules:
398// - ** matches any sequence of symbols
399// - *  matches pattern specified by opts.star
400// - ?  matches any symbol
401function glob(pattern, haystack, opts = { star: GlobStar_URL, whole: true }) {
402  pattern = pattern.replace(/([*]{1,2}|[?])/g, (match) => {
403    switch (match) {
404      case '**': return '.*';
405      case '*':  return opts.star;
406      case '?':  return '.';
407    }
408  });
409
410  if (opts.whole) {
411    pattern = '^' + pattern + '$';
412  }
413
414  pattern = new RegExp(pattern, 'g');
415
416  return haystack.match(pattern) != null;
417}
418
419
420/* Popup.js */
421const enabledPopups = Bubblehouse.data.popups || {};
422const processedPopupRequestURLs = new Set();
423
424
425function popupUrlWithQuery(blockName, queryParams) {
426  let url = new URL(
427    Bubblehouse.data.popupUrl ? Bubblehouse.data.popupUrl.replace('POPUPNAME', blockName) : `${Bubblehouse.root}/blocks/${blockName}`,
428    location.href,
429  )
430  for (let [key, value] of Object.entries(queryParams)) {
431    url.searchParams.set(key, value)
432  }
433  return url.toString()
434}
435class Popup {
436  static defaultDialogStyle = `
437    display: flex;
438    position: fixed;
439    z-index: 1000;
440  `
441
442  constructor(blockName, popupStyle, params, clientY = null, authToken = null) {
443    this.blockName = blockName;
444    this.clientY = clientY;
445
446    let instance = this.instance = `bubblehouse-${this.blockName}-${nextId()}`;
447
448    let dialog = document.createElement('dialog');
449    dialog.id = instance + '-dialog';
450    dialog.style = Popup.defaultDialogStyle + popupStyle.dialogStyle;
451
452    dialog.addEventListener('click', this._handleDialogClick.bind(this))
453    dialog.addEventListener('close', this._handleDialogClose.bind(this))
454
455    let queryParams = { 'popup': true };
456    queryParams.instance = instance;
457
458    let popupUrl = Bubblehouse.data.popupUrl ? Bubblehouse.data.popupUrl.replace('POPUPNAME', this.blockName) : `${Bubblehouse.root}/blocks/${this.blockName}`;
459
460    let auth = authToken || Bubblehouse.data.authToken || authTokenFromQueryString() || authTokenFromExistingBubblehouseFrame(popupUrl);
461    if (auth) {
462      queryParams.auth = auth
463    }
464
465    if (params && Object.keys(params).length > 0) {
466      queryParams.params = JSON.stringify(params)
467    }
468
469    Object.assign(queryParams, collectCurrentMarketQueryParams())
470
471    let blockFrame = this.blockFrame = document.createElement('iframe');
472    blockFrame.id = instance;
473    blockFrame.src = popupUrlWithQuery(this.blockName, queryParams)
474    blockFrame.style = `width: 100%; height: 100%; border: 0; outline: 0; box-shadow: none;`;
475    blockFrame.setAttribute('sandbox', 'allow-top-navigation allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin');
476    blockFrame.setAttribute('allow', 'clipboard-write');
477    blockFrame.addEventListener('bhclient:ClosePopup1', this._ClosePopup1.bind(this))
478    blockFrame.addEventListener('bhclient:Title1', this._Title1.bind(this))
479    blockFrame.addEventListener('bhclient:Resize1', this._Resize1.bind(this))
480    dialog.appendChild(blockFrame);
481
482    this._isLoaded = false
483    this._isShowingRequested = false
484    this._isShown = false
485    this._isClosed = false
486    blockFrame.addEventListener('load', this._handleLoaded.bind(this))
487
488    this.dialog = dialog;
489    this.iframe = blockFrame;
490
491    document.body.appendChild(dialog);
492  }
493
494  show() {
495    if (this._isShowingRequested) {
496      return
497    }
498    this._isShowingRequested = true
499    if (this._isLoaded) {
500      this._doShow()
501    } else {
502      if (Bubblehouse.verbose) console.log(`[Bubblehouse] Popup ${this.instance} loading, will show when finished.`);
503      setTimeout(this._showOnLoadingTimeout.bind(this), 2000)
504    }
505  }
506
507  close() {
508    if (this._isClosed) {
509      return
510    }
511    this._isClosed = true
512    if (this.dialog.open) {
513      this.dialog.close();
514    } else {
515      this._cleanup();
516    }
517  }
518
519  _handleLoaded() {
520    if (this._isLoaded) {
521      return
522    }
523    this._isLoaded = true
524    if (this._isShowingRequested && !this._isShown && !this._isClosed) {
525      this._doShow()
526    }
527  }
528
529  _showOnLoadingTimeout() {
530    if (!this._isShown && !this._isClosed) {
531      if (Bubblehouse.verbose) console.log(`[Bubblehouse] Popup ${this.instance} loading timed out, showing anyway.`);
532      this._doShow()
533    }
534  }
535
536  _doShow() {
537    if (this._isShown) {
538      return
539    }
540
541    if (Bubblehouse.verbose) console.log(`[Bubblehouse] Popup ${this.instance} showing`);
542    if (Bubblehouse.isInner) {
543      let height = this.dialog.offsetHeight;
544      if (height > 0) {
545        let top = 20;
546        if (this.clientY && this.clientY > height + top) {
547          top = this.clientY - height;
548          this.dialog.style.marginTop = top + 'px';
549        }
550      }
551    }
552    this.dialog.showModal();
553    this._isShown = true;
554  }
555
556  _ClosePopup1(ev) {
557    if (Bubblehouse.verbose) console.log(`[Bubblehouse] Popup ${this.instance} ClosePopup1`);
558    this.close();
559    if (ev.detail.reload) {
560      if (ev.detail.flash) {
561        let pageUrl = new URL(window.location.href)
562        pageUrl.searchParams.set('flash', JSON.stringify(ev.detail.flash))
563        window.location.href = pageUrl.toString()
564      } else {
565        window.location.reload()
566      }
567    }
568  }
569
570  _Resize1(ev) {
571    ev.stopPropagation()
572    this._setFrameTitleFromEvent(ev)
573  }
574
575  _Title1(ev) {
576    ev.stopPropagation()
577    this._setFrameTitleFromEvent(ev)
578  }
579
580  _setFrameTitleFromEvent(ev) {
581    if (this.blockFrame.title || ev.detail.sourceWindow !== this.blockFrame.contentWindow || typeof ev.detail.title !== 'string') {
582      return
583    }
584    let title = ev.detail.title.trim()
585    if (title) {
586      this.blockFrame.title = title
587    }
588  }
589
590  _handleDialogClick(ev) {
591    const d = this.dialog.getBoundingClientRect()
592    if (ev.clientX < d.left || ev.clientX > d.right || ev.clientY < d.top || ev.clientY > d.bottom) {
593      this.close()
594    }
595  }
596
597  _handleDialogClose(ev) {
598    if (Bubblehouse.verbose) console.log(`[Bubblehouse] Popup ${this.instance} closed`);
599    this._cleanup();
600  }
601
602  _cleanup() {
603    if (this.dialog.parentNode) {
604      document.body.removeChild(this.dialog);
605    }
606  }
607}
608
609function isSimilarPopupOpen(name) {
610  return document.querySelector(`dialog[id^="bubblehouse-${name}-"]`) !== null;
611}
612
613function isPopupAvailable(name) {
614  return !!enabledPopups[name];
615}
616
617function authTokenFromQueryString() {
618  let windowUrl = new URL(location.href)
619  return windowUrl.searchParams.get('auth') || ''
620}
621
622function authTokenFromExistingBubblehouseFrame(popupUrl) {
623  let popupURL = new URL(popupUrl, location.href)
624  let popupShop = bubblehouseShopSlug(popupURL)
625  if (!popupShop) {
626    return ''
627  }
628  for (let frame of collectBubblehouseFrames(document)) {
629    let frameUrl
630    try {
631      frameUrl = new URL(frame.src, location.href)
632    } catch {
633      continue
634    }
635    if (frameUrl.origin === popupURL.origin &&
636      frameUrl.searchParams.has('auth') &&
637      bubblehouseShopSlug(frameUrl) === popupShop) {
638      return frameUrl.searchParams.get('auth')
639    }
640  }
641  return ''
642}
643
644function openPopupHere(name, params = {}, options = {}) {
645  if (isSimilarPopupOpen(name)) {
646      return;
647  }
648
649  let popupStyle;
650  if (options.url) {
651    popupStyle = options
652  } else {
653    let popupSettings = enabledPopups[name];
654    if (!popupSettings) {
655      throw new Error(`unknown Bubblehouse popup "${name}"`)
656    }
657    popupStyle = options.legacyStyle || popupSettings.style;
658  }
659
660  let popup = new Popup(name, popupStyle, params, options.clientY || null, options.authToken || null)
661
662  if (options.handlers) {
663    for (let [key, value] of Object.entries(options.handlers)) {
664      popup.blockFrame.addEventListener('bhclient:' + key, value)
665    }
666  }
667
668  popup.show();
669  return popup;
670}
671
672function openPopup(popupName, params = {}, options = {}) {
673  let isInverted = !!options.isModeInverted;
674  let effMode, mode = Bubblehouse.data.popupMode
675  if (mode === 'api/inner') {
676    effMode = (isInverted ? 'inner' : 'api')
677  } else if (mode === 'outer/inner') {
678    effMode = (isInverted ? 'inner' : 'outer')
679  } else if (mode === 'inner/api') {
680    effMode = (!isInverted ? 'inner' : 'api')
681  } else if (mode === 'inner/outer') {
682    effMode = (!isInverted ? 'inner' : 'outer')
683  } else {
684    effMode = 'inner'
685  }
686
687  if (effMode === 'inner') {
688    console.log(`[Bubblehouse] popup(${mode} => ${effMode}): inner mode`);
689    openPopupHere(popupName, params, options);
690  } else if (effMode === 'outer') {
691    let message = { bubblehouseBlockClientCall: 'OpenInternalPopup1', instance: '#self', name: popupName, params };
692    if (Bubblehouse.data.propagateAuthTokenToOuterFrame) {
693      message.authToken = Bubblehouse.data.authToken;
694    }
695    console.log(`[Bubblehouse] popup(${mode} => ${effMode}): calling ${message.bubblehouseBlockClientCall}:`, message)
696    window.parent.postMessage(message, '*')
697  } else if (effMode === 'api') {
698    let popupSettings = enabledPopups[popupName];
699    if (!popupSettings) {
700      throw new Error(`unknown Bubblehouse popup "${popupName}"`)
701    }
702    let message = { bubblehouseBlockClientCall: 'OpenPopup1', instance: '#self', name: popupName, params, url: Bubblehouse.data.popupUrl.replace('POPUPNAME', popupName), dialog: popupSettings.style.dialogStyle };
703    if (Bubblehouse.data.propagateAuthTokenToOuterFrame) {
704      message.authToken = Bubblehouse.data.authToken;
705    }
706    console.log(`[Bubblehouse] popup(${mode} => ${effMode}): calling ${message.bubblehouseBlockClientCall}:`, message)
707    window.parent.postMessage(message, '*')
708  }
709}
710
711function processPopupRequestURL(rawURL, cleanupURL = null) {
712  let pageUrl = new URL(rawURL, window.location.href)
713  if (pageUrl.searchParams.has('bh-popup')) {
714    let requestURL = pageUrl.toString()
715    if (processedPopupRequestURLs.has(requestURL)) {
716      return true
717    }
718    processedPopupRequestURLs.add(requestURL)
719
720    let req = parsePopupRequest(pageUrl.searchParams.get('bh-popup'))
721    if (cleanupURL && !pageUrl.searchParams.has('bh-popup-keep')) {
722      pageUrl.searchParams.delete('bh-popup')
723      cleanupURL(pageUrl)
724    }
725    Bubblehouse.processFlashCommands(req.params?.flash)
726    openPopup(req.name, req.params)
727    return true;
728  }
729
730  return false;
731}
732
733function processRequestedPopup() {
734  return processPopupRequestURL(window.location.href, (pageUrl) => window.history.replaceState({}, '', pageUrl.toString()));
735}
736
737function processTurboFrameRequestedPopup(ev) {
738  let frame = ev.target
739  if (!frame || frame.tagName !== 'TURBO-FRAME') {
740    return
741  }
742
743  let src = frame.getAttribute('src')
744  if (!src) {
745    return
746  }
747
748  processPopupRequestURL(src)
749}
750
751function parsePopupRequest(str) {
752  let idx = str.indexOf(':');
753  if (idx < 0) {
754    return { name: str.trim(), params: {} };
755  }
756
757  let name = str.slice(0, idx).trim();
758  let raw  = str.slice(idx + 1).trim();
759  return { name, params: JSON.parse(raw) };
760}
761
762
763function processPageVisitPopups() {
764  let pageUrl = new URL(window.location.href);
765  if (pageUrl.searchParams.has("popup")) return;
766  if (Bubblehouse.isAdmin) return;
767  Object.entries(enabledPopups).forEach(([name, opts]) => {
768    if (opts.trigger.type === "page_visit") {
769      const matches = opts.trigger.criteria.some(({ url }) => {
770        if (!url) return false;
771        return glob(url, pageUrl.pathname);
772      });
773
774      if (matches) {
775        setTimeout(() => openPopupHere(name), opts.trigger.delay_secs * 1000);
776      }
777    }
778  });
779}
780
781function processPostPurchasePopups() {
782  let pageUrl = new URL(window.location.href);
783  if (!glob("**/checkouts/**/thank?you", pageUrl.pathname)) {
784    return false;
785  }
786  return Object.entries(enabledPopups).some(([name, opts]) => {
787    if (opts.trigger.type === "post_purchase") {
788      setTimeout(() => openPopupHere(name), opts.trigger.delay_secs * 1000);
789      return true;
790    } else {
791      return false;
792    }
793  });
794}
795
796function handleOpenInternalPopup1(ev) {
797  openPopupHere(ev.detail.name, ev.detail.params, {
798    clientY: ev.detail.clientY,
799    authToken: ev.detail.authToken,
800  })
801}
802
803function handleOpenApiPopup1(ev) {
804  openPopupHere(ev.detail.name, ev.detail.params, ev.detail)
805}
806
807const processors = [processRequestedPopup, processPostPurchasePopups, processPageVisitPopups];
808
809processors.some((f) => f());
810
811document.addEventListener('bhclient:OpenInternalPopup1', handleOpenInternalPopup1);
812document.addEventListener('bhclient:OpenPopup1', handleOpenApiPopup1);
813document.addEventListener('turbo:frame-load', processTurboFrameRequestedPopup);
814
815Bubblehouse.openPopup = openPopup;
816Bubblehouse.openPopupHere = openPopupHere;
817Bubblehouse.isPopupAvailable = isPopupAvailable;
818
819
820/* util/onDOMContentLoaded.js */
821function onDOMContentLoaded(func) {
822  if (document.readyState === "loading") {
823    document.addEventListener("DOMContentLoaded", func);
824  } else {
825    func();
826  }
827}
828
829
830/* nonshopify/bubblelogin-authtoken.js */
831const BUBBLEHOUSE_AUTH_TOKEN_STORAGE_KEY = "bubblehouse-auth-token";
832const BUBBLEHOUSE_HASH_TOKEN_PREFIX = "#bh-token=";
833
834let bubblehouseAuthTokenObserver = null;
835
836processBubblehouseLoginCredentials("load");
837onDOMContentLoaded(() => processBubblehouseLoginCredentials("domcontentloaded"));
838window.addEventListener("hashchange", () => {
839  if (location.hash.startsWith(BUBBLEHOUSE_HASH_TOKEN_PREFIX)) {
840    processBubblehouseLoginCredentials("hashchange");
841  }
842});
843
844function processBubblehouseLoginCredentials(source) {
845  let stats = bubblehouseLoginStats(source);
846  let hashToken = readBubblehouseHashToken(stats);
847  if (hashToken !== null) {
848    storeBubblehouseAuthToken(hashToken, stats);
849    clearBubblehouseLoginHash(stats);
850    Bubblehouse.data.authToken = hashToken;
851  } else if (!Bubblehouse.data.authToken) {
852    Bubblehouse.data.authToken = readStoredBubblehouseAuthToken(stats);
853  }
854
855  Bubblehouse.isLoggedIn = !!Bubblehouse.data.authToken;
856  let foundAuthFrame = applyBubblehouseAuthToken(Bubblehouse.data.authToken, stats);
857  if (!foundAuthFrame) {
858    observeLateBubblehouseAuthFrames(Bubblehouse.data.authToken, stats);
859  }
860  logBubblehouseLoginStats(stats);
861}
862
863function bubblehouseLoginStats(source) {
864  return {
865    source,
866    hash: "absent",
867    storage: "unchanged",
868    history: "unchanged",
869    candidates: 0,
870    matched: 0,
871    reloaded: 0,
872    skippedAuth: 0,
873    skippedShape: 0,
874    components: 0,
875    componentUpdates: 0,
876    componentSkips: 0,
877    componentUnavailable: 0,
878    errors: 0,
879  }
880}
881
882function readBubblehouseHashToken(stats) {
883  if (!location.hash.startsWith(BUBBLEHOUSE_HASH_TOKEN_PREFIX)) {
884    return null;
885  }
886  let rawToken = location.hash.slice(BUBBLEHOUSE_HASH_TOKEN_PREFIX.length);
887  stats.hash = rawToken ? "present" : "empty";
888  try {
889    return decodeURIComponent(rawToken);
890  } catch {
891    stats.errors++;
892    stats.hash = "decode-error";
893    return rawToken;
894  }
895}
896
897function storeBubblehouseAuthToken(token, stats) {
898  try {
899    if (token) {
900      localStorage.setItem(BUBBLEHOUSE_AUTH_TOKEN_STORAGE_KEY, token);
901      stats.storage = "stored";
902    } else if (localStorage.removeItem) {
903      localStorage.removeItem(BUBBLEHOUSE_AUTH_TOKEN_STORAGE_KEY);
904      stats.storage = "cleared";
905    } else {
906      localStorage.setItem(BUBBLEHOUSE_AUTH_TOKEN_STORAGE_KEY, "");
907      stats.storage = "cleared";
908    }
909  } catch {
910    stats.storage = "blocked";
911    stats.errors++;
912  }
913}
914
915function readStoredBubblehouseAuthToken(stats) {
916  try {
917    return localStorage.getItem(BUBBLEHOUSE_AUTH_TOKEN_STORAGE_KEY) || "";
918  } catch {
919    stats.storage = "blocked";
920    stats.errors++;
921    return "";
922  }
923}
924
925function clearBubblehouseLoginHash(stats) {
926  try {
927    let url = new URL(location.href);
928    url.hash = "";
929    history.replaceState(null, "", url.toString());
930    stats.history = "cleared";
931  } catch {
932    stats.history = "blocked";
933    stats.errors++;
934  }
935}
936
937function applyBubblehouseAuthToken(token, stats) {
938  return applyBubblehouseAuthTokenToNode(token, stats, document)
939}
940
941function applyBubblehouseAuthTokenToNode(token, stats, node) {
942  if (!token) {
943    return false
944  }
945  let foundAuthFrame = applyBubblehouseAuthTokenToStaticFrames(token, stats, node);
946  applyBubblehouseAuthTokenToBlockHosts(token, stats, node);
947  return foundAuthFrame
948}
949
950function applyBubblehouseAuthTokenToStaticFrames(token, stats, root) {
951  let appOrigin = bubblehouseAppOrigin();
952  let pageOrigin = location.origin;
953  let foundAuthFrame = false;
954  for (let frame of collectBubblehouseDocumentFramesIncludingRoot(root)) {
955    stats.candidates++;
956    let frameUrl;
957    try {
958      frameUrl = new URL(frame.src, location.href);
959    } catch {
960      stats.errors++;
961      continue
962    }
963    if (!isBubblehouseAuthFrameURL(frameUrl, appOrigin, pageOrigin)) {
964      stats.skippedShape++;
965      continue
966    }
967    foundAuthFrame = true;
968    stats.matched++;
969    if (frameUrl.searchParams.has("auth")) {
970      stats.skippedAuth++;
971      continue
972    }
973    reloadBubblehouseFrameWithAuth(frame, frameUrl, token, stats);
974  }
975  return foundAuthFrame
976}
977
978function applyBubblehouseAuthTokenToBlockHosts(token, stats, root) {
979  for (let host of collectBubblehouseBlockHostsIncludingRoot(root)) {
980    stats.components++;
981    try {
982      if (typeof host.applyBubblehouseAuthToken !== "function") {
983        stats.componentUnavailable++;
984      } else if (host.applyBubblehouseAuthToken(token)) {
985        stats.componentUpdates++;
986      } else {
987        stats.componentSkips++;
988      }
989    } catch {
990      stats.errors++;
991    }
992  }
993}
994
995function observeLateBubblehouseAuthFrames(token, stats) {
996  if (!token || bubblehouseAuthTokenObserver || typeof MutationObserver !== "function") {
997    return
998  }
999  let root = document.body || document.documentElement;
1000  if (!root) {
1001    return
1002  }
1003  try {
1004    // Some headless storefronts load this relay before React inserts the rewards iframe.
1005    // Safari cannot fall back to the app-domain cookie, so the late iframe still needs the hash token.
1006    bubblehouseAuthTokenObserver = new MutationObserver((mutations) => {
1007      let mutationStats = bubblehouseLoginStats("mutation");
1008      for (let mutation of mutations) {
1009        for (let node of mutation.addedNodes || []) {
1010          applyBubblehouseAuthTokenToNode(Bubblehouse.data.authToken, mutationStats, node);
1011        }
1012      }
1013      logBubblehouseLoginStats(mutationStats);
1014    });
1015    bubblehouseAuthTokenObserver.observe(root, { childList: true, subtree: true });
1016  } catch {
1017    stats.errors++;
1018  }
1019}
1020
1021function collectBubblehouseDocumentFramesIncludingRoot(root) {
1022  let frames = collectBubblehouseDocumentFrames(root);
1023  if (root && root.tagName && root.tagName.toLowerCase() === "iframe") {
1024    frames.unshift(root);
1025  }
1026  return frames
1027}
1028
1029function collectBubblehouseBlockHostsIncludingRoot(root) {
1030  let hosts = collectBubblehouseBlockHosts(root);
1031  if (root && root.tagName && root.tagName.toLowerCase() === "bubblehouse-block") {
1032    hosts.unshift(root);
1033  }
1034  return hosts
1035}
1036
1037function reloadBubblehouseFrameWithAuth(frame, frameUrl, token, stats) {
1038  try {
1039    if (typeof Bubblehouse.prepareFrameURLForAuthReload === "function") {
1040      Bubblehouse.prepareFrameURLForAuthReload(frameUrl);
1041    }
1042    frameUrl.searchParams.set("auth", token);
1043    let newFrame = frame.cloneNode();
1044    newFrame.src = frameUrl.toString();
1045    let parent = frame.parentNode;
1046    parent.insertBefore(newFrame, frame);
1047    parent.removeChild(frame);
1048    stats.reloaded++;
1049  } catch {
1050    stats.errors++;
1051  }
1052}
1053
1054function logBubblehouseLoginStats(stats) {
1055  if (!Bubblehouse.verbose) {
1056    return
1057  }
1058  console.log(
1059    `[Bubblehouse] bubblelogin relay source=${stats.source} hash=${stats.hash} storage=${stats.storage} history=${stats.history} ` +
1060    `frames candidates=${stats.candidates} matched=${stats.matched} reloaded=${stats.reloaded} skipped_auth=${stats.skippedAuth} skipped_shape=${stats.skippedShape} ` +
1061    `components hosts=${stats.components} updated=${stats.componentUpdates} skipped=${stats.componentSkips} unavailable=${stats.componentUnavailable} errors=${stats.errors}`,
1062  );
1063}
1064
1065
1066/* actionTokenRelay.js */
1067const BUBBLEHOUSE_ACTION_QUERY_PARAM = Bubblehouse.actionQueryParam;
1068
1069let bubblehouseActionToken = readBubblehouseActionToken();
1070let bubblehouseRejectedActionToken = "";
1071let bubblehouseActionTokenConflict = false;
1072let bubblehouseActionTokenObserver = null;
1073
1074processBubblehouseActionToken();
1075onDOMContentLoaded(processBubblehouseActionToken);
1076window.addEventListener("message", handleBubblehouseActionTokenStatus);
1077Bubblehouse.prepareFrameURLForAuthReload = prepareBubblehouseActionFrameURLForAuthReload;
1078
1079function readBubblehouseActionToken() {
1080  try {
1081    let values = new URL(location.href).searchParams.getAll(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1082    return values.length === 1 ? values[0] : "";
1083  } catch {
1084    return "";
1085  }
1086}
1087
1088function processBubblehouseActionToken() {
1089  if (!bubblehouseActionToken) {
1090    return
1091  }
1092  applyBubblehouseActionTokenToNode(document);
1093  observeLateBubblehouseActionFrames();
1094}
1095
1096function applyBubblehouseActionTokenToNode(node) {
1097  let appOrigin = bubblehouseActionAppOrigin();
1098  let shopSlugs = bubblehouseActionShopSlugs();
1099  let proxyPaths = bubblehouseActionProxyPaths();
1100  let candidates = [];
1101  for (let frame of collectBubblehouseActionFramesIncludingRoot(node)) {
1102    let frameURL;
1103    try {
1104      frameURL = new URL(frame.src, location.href);
1105    } catch {
1106      continue
1107    }
1108    if (!isBubblehouseActionFrameURL(frameURL, appOrigin, location.origin, shopSlugs, proxyPaths)) {
1109      continue
1110    }
1111    candidates.push({ frame, frameURL });
1112  }
1113  for (let { frame, frameURL } of candidates) {
1114    if (bubblehouseRejectedActionToken &&
1115        frameURL.searchParams.get(BUBBLEHOUSE_ACTION_QUERY_PARAM) === bubblehouseRejectedActionToken) {
1116      try {
1117        frameURL.searchParams.delete(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1118        frame.src = frameURL.toString();
1119      } catch {
1120      }
1121    }
1122  }
1123  if (!bubblehouseActionToken || bubblehouseActionTokenConflict) {
1124    return
1125  }
1126  let conflictingCandidates = candidates.filter(({ frameURL }) => {
1127    let values = frameURL.searchParams.getAll(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1128    return values.length > 0 && (values.length !== 1 || values[0] !== bubblehouseActionToken)
1129  });
1130  if (conflictingCandidates.length > 0) {
1131    bubblehouseActionTokenConflict = true;
1132    if (Bubblehouse.verbose) {
1133      console.log("[Bubblehouse] Action Token relay conflict; no outer token selected");
1134    }
1135    return
1136  }
1137  for (let { frame, frameURL } of candidates) {
1138    let values = frameURL.searchParams.getAll(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1139    if (values.length > 0) {
1140      continue
1141    }
1142    try {
1143      frameURL.searchParams.set(BUBBLEHOUSE_ACTION_QUERY_PARAM, bubblehouseActionToken);
1144      frame.src = frameURL.toString();
1145    } catch {
1146    }
1147  }
1148}
1149
1150function observeLateBubblehouseActionFrames() {
1151  if (bubblehouseActionTokenObserver || typeof MutationObserver !== "function") {
1152    return
1153  }
1154  let root = document.body || document.documentElement;
1155  if (!root) {
1156    return
1157  }
1158  try {
1159    bubblehouseActionTokenObserver = new MutationObserver((mutations) => {
1160      for (let mutation of mutations) {
1161        for (let node of mutation.addedNodes || []) {
1162          applyBubblehouseActionTokenToNode(node);
1163        }
1164      }
1165    });
1166    bubblehouseActionTokenObserver.observe(root, { childList: true, subtree: true });
1167  } catch {
1168  }
1169}
1170
1171function collectBubblehouseActionFramesIncludingRoot(root) {
1172  let frames = collectBubblehouseFrames(root);
1173  if (root && root.tagName && root.tagName.toLowerCase() === "iframe") {
1174    frames.unshift(root);
1175  }
1176  return frames
1177}
1178
1179function bubblehouseActionAppOrigin() {
1180  for (let value of [Bubblehouse.data.callUrl, Bubblehouse.root]) {
1181    if (!value) {
1182      continue
1183    }
1184    try {
1185      return new URL(value, location.href).origin
1186    } catch {
1187    }
1188  }
1189  return ""
1190}
1191
1192function handleBubblehouseActionTokenStatus(ev) {
1193  let status = ev.data && ev.data.bubblehouseActionTokenStatus;
1194  let acknowledgedToken = ev.data && ev.data.bubblehouseActionToken;
1195  let trustedTokenStatus = acknowledgedToken === bubblehouseActionToken ||
1196    (status === "rejected" && acknowledgedToken === "");
1197  if (!bubblehouseActionToken ||
1198      !ev.data || !trustedTokenStatus ||
1199      (bubblehouseActionTokenConflict && status !== "rejected") ||
1200      (status !== "captured" && status !== "rejected")) {
1201    return
1202  }
1203  if (!isTrustedBubblehouseActionTokenStatus(ev)) {
1204    return
1205  }
1206  let token = bubblehouseActionToken;
1207  if (status === "rejected") {
1208    bubblehouseRejectedActionToken = token;
1209    bubblehouseActionToken = "";
1210  }
1211  clearOuterBubblehouseActionToken(token);
1212}
1213
1214function isTrustedBubblehouseActionTokenStatus(ev) {
1215  let appOrigin = bubblehouseActionAppOrigin();
1216  let shopSlugs = bubblehouseActionShopSlugs();
1217  let proxyPaths = bubblehouseActionProxyPaths();
1218  for (let frame of collectBubblehouseFrames(document)) {
1219    let frameURL;
1220    try {
1221      frameURL = new URL(frame.src, location.href);
1222    } catch {
1223      continue
1224    }
1225    let actionValues = frameURL.searchParams.getAll(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1226    if (frame.contentWindow === ev.source &&
1227        frameURL.origin === ev.origin &&
1228        actionValues.length === 1 &&
1229        actionValues[0] === bubblehouseActionToken &&
1230        isBubblehouseActionFrameURL(frameURL, appOrigin, location.origin, shopSlugs, proxyPaths)) {
1231      return true
1232    }
1233  }
1234  return false
1235}
1236
1237function bubblehouseActionShopSlugs() {
1238  let configured = Bubblehouse.extra && Bubblehouse.extra.actionShopSlugs;
1239  let slugs = Array.isArray(configured) ? configured.filter((slug) => typeof slug === "string" && slug) : [];
1240  let rootSlug = bubblehouseRootShopSlug(Bubblehouse.root, location.href);
1241  if (rootSlug && !slugs.includes(rootSlug)) {
1242    slugs.push(rootSlug);
1243  }
1244  return slugs
1245}
1246
1247function bubblehouseActionProxyPaths() {
1248  let configured = Bubblehouse.extra && Bubblehouse.extra.actionProxyPaths;
1249  return Array.isArray(configured) ? configured.filter((path) => typeof path === "string" && path) : []
1250}
1251
1252function prepareBubblehouseActionFrameURLForAuthReload(url) {
1253  if (bubblehouseRejectedActionToken &&
1254      url.searchParams.get(BUBBLEHOUSE_ACTION_QUERY_PARAM) === bubblehouseRejectedActionToken) {
1255    url.searchParams.delete(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1256  }
1257}
1258
1259function clearOuterBubblehouseActionToken(token) {
1260  try {
1261    let url = new URL(location.href);
1262    if (url.searchParams.get(BUBBLEHOUSE_ACTION_QUERY_PARAM) !== token) {
1263      return
1264    }
1265    url.searchParams.delete(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1266    history.replaceState(history.state, "", url.toString());
1267  } catch {
1268  }
1269}
1270
1271
1272/* util/wantSuccessfulJson.js */
1273function wantSuccessfulJson(response) {
1274  if (response.ok) {
1275    return response.json()
1276  } else {
1277    throw new Error(`${response.url} returned HTTP ${response.status} ${response.statusText}`)
1278  }
1279}
1280
1281
1282/* util/waitAll.js */
1283function waitAll(results) {
1284  return Promise.all(results.map((v) => Promise.resolve(v)))
1285}
1286
1287
1288/* util/reportError.js */
1289function reportError(error, context = '') {
1290  console.error(`[Bubblehouse] error: ${context ? context + ': ' : ''}${error.message || error}`)
1291  if (error.stack) {
1292    console.error(error.stack)
1293  }
1294}
1295
1296function reportWarning(error, context = '') {
1297  console.warn(`[Bubblehouse] warning: ${context ? context + ': ' : ''}${error.message || error}`)
1298}
1299
1300
1301/* util/debounceAsyncWithRepeat.js */
1302function debounceAsyncWithRepeat(debugName, debounceInterval, f) {
1303  const IDLE = 0, PENDING = 1, RUNNING = 2, RUNNING_AND_PENDING = 3;
1304  let state = -1; setState(IDLE);
1305  let pendingArgs = [];
1306  return request;
1307
1308  function request(...args) {
1309    pendingArgs = args;
1310    if (state === IDLE) {
1311      schedule()
1312    } else if (state === RUNNING) {
1313      setState(RUNNING_AND_PENDING)
1314    }
1315  }
1316
1317  function schedule() {
1318    setState(PENDING);
1319    setTimeout(start, debounceInterval);
1320  }
1321
1322  function start() {
1323    let args = pendingArgs;
1324    pendingArgs = [];
1325    setState(RUNNING);
1326    let promise = f.apply(null, args);
1327    if (promise && promise.then) {
1328      promise.then(finish, finish)
1329    } else {
1330      finish()
1331    }
1332  }
1333
1334  function finish(result) {
1335    if (result instanceof Error) {
1336      console.error(`[Bubblehouse] ${debugName} failed:`, result)
1337    } else {
1338      if (Bubblehouse.verbose) console.log(`[Bubblehouse] ${debugName} finished:`, result)
1339    }
1340    if (state === RUNNING_AND_PENDING) {
1341      start();
1342    } else {
1343      setState(IDLE);
1344    }
1345  }
1346
1347  function setState(newState) {
1348    // if (Bubblehouse.verbose) console.log(`[Bubblehouse] ${debugName} state transition: ${state} => ${newState}`);
1349    state = newState;
1350  }
1351}
1352
1353
1354/* shopify/cartApi.js */
1355// updateCart = debounceAsyncWithRepeat('updateCart', 1, updateCart);
1356
1357Bubblehouse.loadCart = loadCart;
1358Bubblehouse.addToCart = addToCart;
1359Bubblehouse.updateCart = updateCart;
1360Bubblehouse.callStorefront = callStorefront;
1361Bubblehouse.addDiscountCode = addDiscountCode;
1362
1363function loadCart() {
1364  return fetch(Shopify.routes.root + "cart.js").then(wantSuccessfulJson);
1365}
1366
1367// items = [{ id: giftVariantId, quantity: 1 }]
1368function addToCart(items, { origin } = {}) {
1369  let body = { items };
1370  console.log(`[Bubblehouse] adding to cart (${origin}):`, body);
1371  return fetch(Shopify.routes.root + "cart/add.js?bubblehouse-cart-ignore=1", {
1372    method: "POST",
1373    body: JSON.stringify(body),
1374    headers: { "Content-Type": "application/json" },
1375  }).then(wantSuccessfulJson);
1376}
1377
1378// updates = { key1: quantity1, ... }
1379function updateCart(updates, { origin } = {}) {
1380  let body = { updates };
1381  console.log(`[Bubblehouse] updating cart (${origin}):`, body);
1382  return fetch(
1383    `${Shopify.routes.root}cart/update.js?bubblehouse-cart-ignore=1`,
1384    {
1385      method: "POST",
1386      body: JSON.stringify(body),
1387      headers: { "Content-Type": "application/json" },
1388    },
1389  );
1390}
1391
1392async function addDiscountCode(_cartToken, discountCode, codesToRemove = []) {
1393  const cart = await loadCart();
1394  const existingDiscountCodes = (cart.discount_codes || []).map(({ code }) => code);
1395  const discountCodes = [...new Set([
1396    ...existingDiscountCodes.filter((code) => !codesToRemove.includes(code)),
1397    discountCode,
1398  ])];
1399  if (Bubblehouse.verbose) {
1400    console.log(`[BH Debug] new discountCodes = ${JSON.stringify(discountCodes)}`)
1401  }
1402  const updated = await setDiscountCodes(discountCodes);
1403  if (!updated.discount_codes?.some(({ code, applicable }) =>
1404    code.toLowerCase() === discountCode.toLowerCase() && applicable
1405  )) {
1406    throw new Error("Shopify did not apply the discount code");
1407  }
1408}
1409
1410function setDiscountCodes(discountCodes) {
1411  const body = { discount: discountCodes.join(",") };
1412  return fetch(
1413    `${Shopify.routes.root}cart/update.js?bubblehouse-cart-ignore=1`,
1414    {
1415      method: "POST",
1416      body: JSON.stringify(body),
1417      headers: { "Content-Type": "application/json" },
1418    },
1419  ).then(wantSuccessfulJson);
1420}
1421
1422function isShopifyStorefrontApiAvailable() {
1423  return !!Bubblehouse.data.storefrontToken;
1424}
1425
1426async function callStorefront(query, variables) {
1427  let token = Bubblehouse.data.storefrontToken;
1428  if (!token) {
1429    throw new Error("misconfigured: missing storefront token");
1430  }
1431
1432  let m = query.match(/result: (\w+)/);
1433  if (!m) {
1434    throw new Error("invalid GQL query");
1435  }
1436  let callName = m[1];
1437
1438  let response = await fetch("/api/2025-01/graphql.json", {
1439    method: "POST",
1440    headers: {
1441      "Content-Type": "application/json",
1442      "X-Shopify-Storefront-Access-Token": token,
1443    },
1444    body: JSON.stringify({ query, variables }),
1445  });
1446  let data = await response.json();
1447  if (Bubblehouse.verbose) {
1448    console.log(`[Bubblehouse] callStorefront ${callName}(${JSON.stringify(variables)}) => ${JSON.stringify(data)}`)
1449}
1450  if (data?.errors) {
1451    // {"errors":[{"message":"Field 'xxcart' doesn't exist on type 'QueryRoot'","locations":[{"line":2,"column":5}],"path":["query","result"],"extensions":{"code":"undefinedField","typeName":"QueryRoot","fieldName":"xxcart"}},{"message":"Variable $cartId is declared by anonymous query but not used","locations":[{"line":1,"column":1}],"path":["query"],"extensions":{"code":"variableNotUsed","variableName":"cartId"}}]}
1452
1453    console.error(`[Bubblehouse] ${callName} GQL errors: ${JSON.stringify(data.errors)}`);
1454    throw new Error(`${callName}: GQL error: ${data.errors[0].message} (${data.errors.length})`);
1455  }
1456  let result = data.data?.result;
1457  if (result?.userErrors?.length) {
1458    console.error(`[Bubblehouse] ${callName} user errors:`, result.userErrors);
1459    throw new Error(`${callName}: GQL userErrors: ${result.userErrors}`);
1460  }
1461  return result;
1462}
1463
1464function navigateToCartOrReloadPage(cartUrl) {
1465  window.location.href = cartUrl ? cartUrl : "/cart";
1466}
1467
1468
1469/* xhrMonitor.js */
1470;(function() {
1471let origXHROpen = XMLHttpRequest.prototype.open;
1472let origFetch = window.fetch;
1473XMLHttpRequest.prototype.open = xhrOpenWrapperUnlikelyToBeRelatedToYourError;
1474window.fetch = fetchWrapperUnlikelyToBeRelatedToYourError;
1475if (Bubblehouse.verbose) console.log('[Bubblehouse] xhr monitoring enabled');
1476
1477function xhrOpenWrapperUnlikelyToBeRelatedToYourError(method, url) {
1478  if (this.bubblehouseListener)
1479    this.removeEventListener('loadend', this.bubblehouseListener);
1480  this.bubblehouseListener = handleXMLHttpRequestLoaded.bind(this, method.toUpperCase(), url)
1481  this.addEventListener('loadend', this.bubblehouseListener);
1482  origXHROpen.apply(this, arguments);
1483}
1484
1485function handleXMLHttpRequestLoaded(method, url) {
1486  // if (Bubblehouse.verbose) console.log(`[Bubblehouse] xhr finished via XMLHttpRequest: ${method} ${url}`);
1487  document.dispatchEvent(new CustomEvent('bubblehouse:xhrend', { detail: { method: method, url: url, ok: true } }));
1488}
1489
1490function fetchWrapperUnlikelyToBeRelatedToYourError(resource, options) {
1491  let method = 'GET';
1492  if (options && typeof options === 'object' && options.method && typeof options.method === 'string') {
1493    method = options.method.toUpperCase();
1494  }
1495  let promise = origFetch.apply(this, arguments);
1496  promise.then(fetchWrapperSucceeded.bind(null, method));
1497  return promise;
1498}
1499
1500function fetchWrapperSucceeded(method, response) {
1501  // if (Bubblehouse.verbose) console.log(`[Bubblehouse] xhr finished via fetch: ${method} ${response.url}`);
1502  document.dispatchEvent(new CustomEvent('bubblehouse:xhrend', { detail: { method: method, url: response.url, ok: response.ok } }));
1503}
1504})() || true;
1505
1506/* cartMonitor.js */
1507;(function() {
1508Bubblehouse.cartUpdaters = []
1509Bubblehouse.cartListeners = []
1510document.addEventListener('bubblehouse:xhrend', considerUpdatingCart);
1511document.addEventListener('bubblehouse:external-cart-update', handleExternalCartUpdate);
1512setTimeout(() => { handleCartUpdate('pageLoad', false) }, 1)
1513
1514function considerUpdatingCart(ev) {
1515  let { method, url } = ev.detail;
1516  let m
1517  if ((method === 'POST') && (m = /\/cart\/(add|update|change)/.exec(url))) {
1518    let isBubblehouseOrigin = /bubblehouse-cart-ignore/.test(url)
1519    if (Bubblehouse.verbose) console.log(`[Bubblehouse] cart update detected (from ${isBubblehouseOrigin ? 'bubblehouse' : 'outside'})`);
1520    handleCartUpdate(m[1], isBubblehouseOrigin).then(null, reportError)
1521  // } else if ((method === 'POST') && (m = /\/api\/([0-9-]+)\/graphql\.json/.exec(url))) {
1522  }
1523}
1524
1525function handleExternalCartUpdate() {
1526  handleCartUpdate('external_update', false).then(null, reportError)
1527}
1528
1529// document.dispatchEvent(new Event('bubblehouse_cart_update', { detail: { origin: url } }))
1530
1531async function handleCartUpdate(cartAction, isBubblehouseOrigin) {
1532  if (Bubblehouse.cartListeners.length === 0 && Bubblehouse.cartUpdaters.length === 0) return;
1533  let cartBody
1534  try {
1535    cartBody = await loadCart()
1536  } catch (error) {
1537    reportWarning(error, `cart fetch failed after ${cartAction}`)
1538    return
1539  }
1540  if (Bubblehouse.verbose) console.log(`[Bubblehouse] cart after ${cartAction}:`, cartBody);
1541  if (!isBubblehouseOrigin) {
1542    await waitAll(Bubblehouse.cartUpdaters.map((f) => f(cartBody, cartAction)))
1543  }
1544  await waitAll(Bubblehouse.cartListeners.map((f) => f(cartBody, cartAction)))
1545  //await rerenderIfModified(cartBody)
1546}
1547
1548function rerenderIfModified(originalCart) {
1549  return loadCart().then((newCart) => {
1550    if (JSON.stringify(originalCart.items) != JSON.stringify(newCart.items)) {
1551      if (Bubblehouse.verbose) console.log(`[Bubblehouse] cart modified after running cart updaters`);
1552      return rerenderCart();
1553    } else {
1554      if (Bubblehouse.verbose) console.log(`[Bubblehouse] cart unmodified after running cart updaters`);
1555    }
1556  })
1557}
1558})() || true;
1559
1560/* shopify/isOnCartPage.js */
1561Bubblehouse.isOnCartPage = isOnCartPage;
1562Bubblehouse.isCartVisible = isCartVisible;
1563
1564function isOnCartPage() {
1565  return /\/cart$/.test(location.pathname)
1566}
1567
1568function isCartVisible() {
1569  return isOnCartPage() || !!document.querySelector('#cart-drawer')
1570}
1571
1572
1573/* util/ignoreErr.js */
1574function ignoreErr(debugPrefix, f) {
1575  try {
1576    let result = f()
1577    if (result && result.then) {
1578      return result['catch'](function (error) {
1579        console.error(`[Bubblehouse] ${debugPrefix} failed:`, error)
1580      })
1581    } else {
1582      return result
1583    }
1584  } catch (error) {
1585    console.error(`[Bubblehouse] ${debugPrefix} failed:`, error)
1586  }
1587}
1588
1589
1590/* rerenderCart.js */
1591Bubblehouse.rerenderCart = rerenderCart;
1592
1593function rerenderCart(force) {
1594  if (!isOnCartPage() && !force) {
1595    return
1596  }
1597  location.reload();
1598  // return waitAll([
1599  //   ignoreErr('rerenderCart.shopify', () => {
1600  //     let el = document.querySelector('cart-notification');
1601  //     if (el && 'function' === typeof el.renderContents) {
1602  //       return fetch(Shopify.routes.root + 'cart?sections=cart-drawer,cart-icon-bubble', { cache: 'no-cache', credentials: 'same-origin' }).then(wantSuccessfulJson).then((body) => {
1603  //         el.renderContents({ sections: body })
1604  //       })
1605  //     }
1606  //   }),
1607  // ])
1608}
1609
1610
1611/* freeSample.js */
1612;(function() {
1613if (!Bubblehouse.data.freeSample) {
1614  return
1615}
1616let freeSampleVariantId = Bubblehouse.data.freeSample.variantId;
1617Bubblehouse.cartUpdaters.push(adjustGiftsInCart);
1618if (Bubblehouse.verbose) console.log(`[Bubblehouse] [free-sample] enabled with variant ID ${freeSampleVariantId}`)
1619
1620let minPurchaseAmountInCents = ~~(parseFloat(Bubblehouse.data.freeSample.minPurchaseAmount, 10) * 100 + 0.5)
1621
1622function adjustGiftsInCart(cart, cartAction) {
1623  let sampleItems = [], regularItems = []
1624  for (let item of cart.items) {
1625    if (item.variant_id === freeSampleVariantId) {
1626      sampleItems.push(item)
1627    } else {
1628      regularItems.push(item)
1629    }
1630  }
1631
1632  let isFreeSampleOwed = (regularItems.length > 0) && (cart.total_price >= minPurchaseAmountInCents);
1633
1634  if (Bubblehouse.verbose) console.log(`[Bubblehouse] [free-sample] cart contains ${regularItems.length} regular items and ${sampleItems.length} sample items, amount ${cart.total_price} => ${isFreeSampleOwed ? 'ELIGIBLE' : 'NOT eligible'} for free sample.`)
1635
1636  if (isFreeSampleOwed && sampleItems.length === 0) {
1637    if (cartAction === 'add' || (cartAction === 'update' && Bubblehouse.data.freeSample.autoAdd) || (cartAction === 'pageLoad' && isCartVisible())) {
1638      if (Bubblehouse.data.freeSample.autoAdd) {
1639        addGiftToCart().then(() => rerenderCart(false))
1640      } else {
1641        console.log(`[Bubblehouse] [free-sample] offering via popup`)
1642
1643        let popup
1644        popup = openPopupHere('free-sample', {}, {
1645          handlers: {
1646            'AddGiftToCart1': function (message) { doAddSampleToCart1(popup, message) },
1647          }
1648        })
1649      }
1650    } else {
1651      console.log(`[Bubblehouse] [free-sample] eligible, but not offered on ${cartAction} action`)
1652    }
1653  } else if (sampleItems.length > 0) {
1654    let firstItem = sampleItems.pop();
1655    let firstItemQuantity = (isFreeSampleOwed ? 1 : 0);
1656
1657    let updates = {};
1658    if (firstItem.quantity !== firstItemQuantity) {
1659      updates[firstItem.key] = firstItemQuantity;
1660    }
1661    for (let item of sampleItems) {
1662      updates[item.key] = 0;
1663    }
1664
1665    if (Object.keys(updates).length > 0) {
1666      if (Bubblehouse.verbose) console.log(`[Bubblehouse] [free-sample] removing from cart.`)
1667
1668      let u = updateCart(updates, { origin: 'free-sample' })
1669      console.log('u = ', u)
1670      return u.then(() => rerenderCart(false))
1671    } else {
1672      if (Bubblehouse.verbose) console.log(`[Bubblehouse] [free-sample] is up to date`)
1673    }
1674  }
1675}
1676
1677function doAddSampleToCart1(popup, message) {
1678  if (Bubblehouse.verbose) console.log(`[Bubblehouse] [free-sample] adding to cart`);
1679  addGiftToCart().then(() => {
1680    popup.close();
1681    return rerenderCart(true);
1682  })
1683}
1684
1685function addGiftToCart() {
1686  return addToCart([{ id: freeSampleVariantId, quantity: 1 }], { origin: 'free-sample' })
1687}
1688})() || true;
1689
1690/* util/observeElements.js */
1691function observeElements(selector, func) {
1692  let observer = new MutationObserver(function (mutations) {
1693    mutations.forEach(function (mutation) {
1694      for (let node of mutation.addedNodes) {
1695        if (node.nodeType === Node.ELEMENT_NODE && node.matches(selector)) {
1696          func(node)
1697        }
1698      }
1699    })
1700  })
1701  observer.observe(document, { subtree: true, childList: true })
1702
1703  for (let node of document.querySelectorAll(selector)) {
1704    func(node)
1705  }
1706}
1707
1708
1709/* shopify/productPage.js */
1710function isShopifyProductPage() {
1711  return (
1712    /\/products\/[^/]+/.test(window.location.pathname) ||
1713    window.ShopifyAnalytics?.meta?.page?.pageType === 'product' ||
1714    window.meta?.product != null
1715  )
1716}
1717
1718
1719/* inlinePanels.js */
1720;(function() {
1721observeElements('.bubblehouse-product-points-panel', populateProductPointsPanel)
1722
1723function populateProductPointsPanel(panel) {
1724  if (!panel) {
1725    return
1726  }
1727  if (!Bubblehouse.data.ppc) {
1728    return
1729  }
1730  let valueSpan = panel.querySelector('.bubblehouse-product-points-value')
1731  let amount = panel.dataset.priceCents
1732  if (Bubblehouse.verbose) console.log(`[Bubblehouse] Found inline panel, ppc = ${Bubblehouse.data.ppc}, amount = ${amount}`)
1733  if (valueSpan && amount) {
1734    let points = Math.floor(Bubblehouse.data.ppc * +amount)
1735    valueSpan.textContent = points.toFixed(0)
1736
1737    if (Bubblehouse.data.prelaunch) {
1738      return
1739    }
1740    panel.style.visibility = ''
1741    panel.style.display = ''
1742    updatePointsOnVariantChange(valueSpan)
1743  }
1744}
1745
1746async function updatePointsOnVariantChange(valueSpan) {
1747  if (!isShopifyProductPage()) {
1748    return
1749  }
1750
1751  let url = new URL(window.location.href)
1752  url.pathname += '.js'
1753
1754  let data
1755  try {
1756    let response = await fetch(url.toString())
1757    if (!response.ok) {
1758      console.error(`[Bubblehouse] error fetching product json`)
1759      return
1760    }
1761    data = await response.json()
1762  } catch {
1763    console.error(`[Bubblehouse] error fetching product json`)
1764    return
1765  }
1766
1767  if (!data || !Array.isArray(data.variants)) {
1768    console.error(`[Bubblehouse] product variants array is undefined`)
1769    return
1770  }
1771
1772  window.addEventListener('bubblehouse:variant-change', e => {
1773    let variant = data.variants.find(o => o.id == e.detail.variantId)
1774    if (!variant) {
1775      console.log(`[Bubblehouse] variant ${e.detail.variantId} not found`)
1776      return
1777    }
1778    let points = Math.floor(Bubblehouse.data.ppc * +variant.price)
1779    valueSpan.textContent = points.toFixed(0)
1780  })
1781}
1782})() || true;
1783
1784/* util/h.js */
1785function h(tagSpec, ...children) {
1786  let [tagName, ...classesAndIds] = tagSpec.match(/[#.]?[^#.]+/g)
1787  let element = document.createElement(tagName)
1788  for (let identifier of classesAndIds) {
1789    if (identifier.startsWith('#')) {
1790      element.id = identifier.slice(1)
1791      element.classList.add(identifier.slice(1))
1792    } else if (identifier.startsWith('.')) {
1793      element.classList.add(identifier.slice(1))
1794    } else {
1795      throw new Error(`invalid tag spec: "${tagSpec}" (invalid part is "${identifier}")`)
1796    }
1797  }
1798  while (children.length > 0 && children[0] && children[0].constructor === Object) {
1799    setAttrs(element, children.shift())
1800  }
1801  if (children.length === 1 && typeof children[0] === 'string') {
1802    element.textContent = children[0]
1803  } else {
1804    for (let child of children) {
1805      appendChild(element, child)
1806    }
1807  }
1808  return element;
1809}
1810
1811function setAttrs(element, attrs) {
1812  for (const [attr, value] of Object.entries(attrs)) {
1813    if (value == null || value === false) {
1814      continue
1815    } else if (attr === 'class') {
1816      adjustClasses(element, value)
1817    } else if (attr.startsWith('on')) {
1818      let event = attr.substr(2)
1819      element.addEventListener(event, value)
1820    } else if (value === true) {
1821      element.setAttribute(attr, '')
1822    } else if (value && value.constructor === Object) {
1823      if (attr === 'style') {
1824        Object.assign(element.style, value)
1825      } else {
1826        throw new TypeError(`Invalid object value for attribute "${attr}"`)
1827      }
1828    } else {
1829      element.setAttribute(attr, value.toString())
1830    }
1831  }
1832}
1833
1834// classes -> classItem | array of classes
1835// classItem -> falsy | classSpecString | {classSpecString: booleanish}
1836// classSpecString -> whitespace-separated list of classes
1837function adjustClasses(element, classes) {
1838  if (!classes) return;
1839  if (typeof classes === 'string') {
1840    element.classList.add(...classes.trim().split(/\s+/))
1841  } else if (Array.isArray(classes)) {
1842    for (let item of classes) {
1843      adjustClasses(element, item)
1844    }
1845  } else if (classes && classes.constructor === Object) {
1846    for (let [name, included] of Object.entries(classes)) {
1847      element.classList.toggle(name, !!included)
1848    }
1849  } else {
1850    throw new TypeError('Classes must be strings, objects, or false/null/undefined');
1851  }
1852}
1853
1854function appendChild(element, child) {
1855  if (child == null || child === false) return;
1856  if (child instanceof Node || typeof child === 'string') {
1857    element.append(child)
1858  } else if (typeof child === 'number') {
1859    element.append(child.toString())
1860  } else if (Array.isArray(child)) {
1861    for (let nestedChild of child) {
1862      appendChild(element, nestedChild)
1863    }
1864  } else {
1865    throw new TypeError('Children must be elements, strings, numbers, arrays, or false/null/undefined');
1866  }
1867  return child
1868}
1869
1870function assignElementRefs(host, root) {
1871  for (let child of root.children) {
1872    if (child.id) {
1873      host[child.id] = child
1874    }
1875    assignElementRefs(host, child)
1876  }
1877  return root
1878}
1879
1880
1881/* util/removeArrayElement.js */
1882function removeArrayElement(array, element) {
1883  let index = array.indexOf(element)
1884  if (index >= 0) {
1885    array.splice(index, 1)
1886  }
1887}
1888
1889
1890/* util/parseStringTemplate.js */
1891function parseStringTemplate(str, fs, fk, prefix = '{{.', suffix = '}}') {
1892  // Given "This is {{.foo}} sample {{.bar}} value", calls
1893  // - fs('This is ')
1894  // - fk('foo')
1895  // - fs(' sample ')
1896  // - fk('bar')
1897  // - fs(' value.')
1898  let remaining = str;
1899  let startIndex = 0,
1900    endIndex = 0;
1901  while (
1902    (startIndex = remaining.indexOf(prefix)) >= 0 &&
1903    (endIndex = remaining.indexOf(suffix, startIndex + prefix.length)) >= 0
1904  ) {
1905    if (startIndex > 0) {
1906      fs(remaining.substring(0, startIndex));
1907    }
1908    fk(remaining.substring(startIndex + prefix.length, endIndex));
1909    remaining = remaining.substring(endIndex + suffix.length);
1910  }
1911  if (remaining) {
1912    fs(remaining);
1913  }
1914}
1915
1916function mapStringTemplate(str, fs, fk) {
1917  let result = [];
1918  parseStringTemplate(
1919    str,
1920    (s) => {
1921      result.push(fs(s, result.length));
1922    },
1923    (s) => {
1924      result.push(fk(s, result.length));
1925    },
1926  );
1927  return result;
1928}
1929
1930function parseTags(str, { validTags }) {
1931  // Given "This is <foo>sample</foo> value", returns
1932  // - ['This is ', {tag: 'foo', body: ['sample']}, ' value']
1933  // Does not support nested tags so far.
1934
1935  var result = [];
1936  var body = result;
1937  var element = null;
1938  parseStringTemplate(str, (str) => {
1939    body.push(str);
1940  }, (rawStr) => {
1941    let innerStr = rawStr.trim();
1942    if (innerStr === '') {
1943      body.push('(!EMPTY_TAG!)');
1944      return;
1945    }
1946
1947    let isClosing = innerStr.startsWith('/');
1948    let tag = isClosing ? innerStr.substring(1).trim() : innerStr;
1949
1950    if (!validTags.includes(tag)) {
1951      body.push('<' + rawStr + '>');
1952    } else if (element != null) {
1953      if (!isClosing) {
1954        body.push('(!NESTED_TAG!)');
1955      } else if (tag === element.tag) {
1956        body = result;
1957        element = null;
1958      } else {
1959        body.push('(!WRONG_CLOSING_TAG!)');
1960      }
1961    } else if (isClosing) {
1962      body.push('(!UNOPENED_TAG!)');
1963    } else {
1964      element = { tag: tag, body: [] };
1965      body.push(element);
1966      body = element.body;
1967    }
1968  }, '<', '>');
1969  return result;
1970}
1971
1972
1973/* util/localize.js */
1974function localize(strings, key, vals = {}) {
1975  if (!strings.hasOwnProperty(key)) {
1976    throw new Error(`missing string "${key}"`);
1977  }
1978  let str = strings[key];
1979  for (let k in vals)
1980    if (vals.hasOwnProperty(k)) {
1981      str = str.replaceAll("{{." + k + "}}", "" + vals[k]);
1982    }
1983  return str;
1984}
1985
1986
1987/* vm/RedeemVM.js */
1988class RedeemVM {
1989  constructor(
1990    data,
1991    cartCost,
1992    itemCount,
1993    cartCouponCodes,
1994    desiredRedemptionAmountOrPoints,
1995    locale,
1996    overrides = {},
1997    options = { subtractProvisionalAmount: false},
1998  ) {
1999    data || (data = {});
2000    let isFreeCart =
2001      data.enableWhenFree &&
2002      cartCost != null &&
2003      itemCount != null &&
2004      cartCost == 0 &&
2005      itemCount > 0;
2006    this.data = data;
2007    this.cartCost = cartCost;
2008    this.locale = locale;
2009    this.isLoaded = data && cartCost != null; // && (cartCost > 0 || isFreeCart);
2010    this.offers = (data && data.offers) || [];
2011    this.enablePointRedemption = data && data.enablePointRedemption;
2012    console.log(`[Bubblehouse] data =`, data);
2013    console.log(
2014      `[Bubblehouse] RedeemVM: cartCost = ${cartCost}, itemCount = ${itemCount}, isLoaded = ${this.isLoaded}, isFreeCart = ${isFreeCart}`,
2015    );
2016    if (!this.isLoaded || !data.styles) {
2017      return;
2018    }
2019
2020    this.values = data.styles.v || {};
2021    this.classes = data.styles.c || {};
2022    this.stylesheetUrl = data.styles.css;
2023    this.col1 =
2024      overrides.col1 || this.resolveValue(".widget.presentation.col1.derived");
2025    this.col2 =
2026      overrides.col2 || this.resolveValue(".widget.presentation.col2.derived");
2027    if (!["amount", "points"].includes(this.col1)) {
2028      throw new Error(
2029        `<bubblehouse-redeem-panel>: internal error: col1 = ${this.col1}`,
2030      );
2031    }
2032    if (
2033      !["amount", "points", "none"].includes(this.col2) ||
2034      this.col2 === this.col1
2035    ) {
2036      throw new Error(
2037        `<bubblehouse-redeem-panel>: internal error: col2 = ${this.col2}`,
2038      );
2039    }
2040
2041    this.spendablePts = data.ptsSpendable;
2042    this.spendableAmount = this.convertPointsToAmount(this.spendablePts);
2043
2044    let maxRedeemPts = data.redeemPtsMax;
2045    if (cartCost != null) {
2046      let maxOrderAmount = cartCost;
2047      if (options.subtractProvisionalAmount && data.provisionalAmount != null) {
2048        maxOrderAmount = Math.max(0, cartCost - data.provisionalAmount);
2049        // if (data.provisionalCouponCode == null || cartCouponCodes == null || !cartCouponCodes.includes(data.provisionalCouponCode)) {
2050        // }
2051      }
2052      let maxOrderPts = Math.ceil(this.convertAmountToPoints(maxOrderAmount));
2053      maxRedeemPts = Math.min(maxRedeemPts, maxOrderPts);
2054    }
2055    let defaultRedeemPts =
2056      Number.isFinite(data.redeemPtsDefault) ? data.redeemPtsDefault : maxRedeemPts;
2057    defaultRedeemPts = Math.min(Math.max(0, defaultRedeemPts), maxRedeemPts);
2058    if (defaultRedeemPts != 0 && defaultRedeemPts < data.redeemPtsMin) {
2059      defaultRedeemPts = 0;
2060    }
2061
2062    if (
2063      desiredRedemptionAmountOrPoints == null ||
2064      !isNaN(desiredRedemptionAmountOrPoints)
2065    ) {
2066      if (this.col1 === "amount") {
2067        if (desiredRedemptionAmountOrPoints == null) {
2068          this.effectiveRedemptionPts = defaultRedeemPts;
2069        } else {
2070          this.effectiveRedemptionPts = Math.ceil(
2071            this.convertAmountToPoints(desiredRedemptionAmountOrPoints),
2072          );
2073        }
2074        this.effectiveRedemptionAmount = this.convertPointsToAmount(
2075          this.effectiveRedemptionPts,
2076        );
2077      } else if (this.col1 === "points") {
2078        this.effectiveRedemptionPts =
2079          desiredRedemptionAmountOrPoints == null
2080            ? defaultRedeemPts
2081            : desiredRedemptionAmountOrPoints;
2082        this.effectiveRedemptionAmount = this.convertPointsToAmount(
2083          this.effectiveRedemptionPts,
2084        );
2085      }
2086    } else {
2087      this.effectiveRedemptionPts = null;
2088      this.effectiveRedemptionAmount = null;
2089    }
2090
2091    this.amountError = null;
2092    let applyStatusGates =
2093      options.applyStatusGates === true ||
2094      options.applyCheckoutStatusGates === true;
2095    let applyDiscountCodeGate =
2096      options.applyDiscountCodeGate === true ||
2097      options.applyCheckoutStatusGates === true;
2098    this.redeemDisabledMessage =
2099      applyStatusGates && data.redeemDisabledMessage != null
2100        ? data.redeemDisabledMessage
2101        : "";
2102    this.isRedemptionBlockedByStatusGate =
2103      applyStatusGates &&
2104      (
2105        this.redeemDisabledMessage !== "" ||
2106        data.enabled === false ||
2107        data.enablePointRedemption === false
2108      );
2109    this.isRedemptionBlockedByDiscountCodeGate =
2110      applyDiscountCodeGate &&
2111      data.disableIfDiscountApplied === true &&
2112      (cartCouponCodes || []).length > 0;
2113    this.isRedemptionBlockedByCheckoutState =
2114      this.isRedemptionBlockedByStatusGate ||
2115      this.isRedemptionBlockedByDiscountCodeGate;
2116    if (this.effectiveRedemptionPts != null) {
2117      if (this.effectiveRedemptionPts < 0) {
2118        this.amountError = this.localize(".error.negative.string");
2119      } else if (
2120        this.effectiveRedemptionPts != 0 &&
2121        this.effectiveRedemptionPts < data.redeemPtsMin
2122      ) {
2123        let valueStr;
2124        if (this.col1 === "amount") {
2125          valueStr = this.formatBareCurrency(
2126            this.convertPointsToAmount(data.redeemPtsMin),
2127          );
2128        } else if (this.col1 === "points") {
2129          valueStr = this.formatPoints(data.redeemPtsMin);
2130        }
2131        this.amountError = this.localize(".error.below-minimum.string", {
2132          value: valueStr,
2133        });
2134      } else if (
2135        this.effectiveRedemptionPts != 0 &&
2136        this.effectiveRedemptionPts > maxRedeemPts
2137      ) {
2138        let pts;
2139        let valueStr;
2140
2141        if (maxRedeemPts === 0 && data.redeemPtsPeriodLimit > 0) {
2142          pts = data.redeemPtsPeriodLimit
2143        } else {
2144          pts = maxRedeemPts
2145        }
2146
2147        if (this.col1 === "amount") {
2148          valueStr = this.formatBareCurrency(
2149            this.convertPointsToAmount(pts),
2150          );
2151        } else if (this.col1 === "points") {
2152          valueStr = this.formatPoints(pts);
2153        }
2154        this.amountError = this.localize(".error.above-balance.string", {
2155          value: valueStr,
2156        });
2157      }
2158
2159      this.visibleRedemptionPts = null;
2160      if (this.effectiveRedemptionPts != null) {
2161        this.visibleRedemptionPts = this.effectiveRedemptionPts;
2162        if (this.resolveValue('.redeem-points.presentation.enum') === 'negative') {
2163          this.visibleRedemptionPts = -this.visibleRedemptionPts;
2164        }
2165      }
2166    }
2167
2168    this.isRedeemEnabled =
2169      this.effectiveRedemptionAmount != null &&
2170      (this.effectiveRedemptionAmount != 0 || isFreeCart) &&
2171      this.amountError == null &&
2172      !this.isRedemptionBlockedByCheckoutState;
2173    if (this.effectiveRedemptionAmount != null) {
2174      if (this.effectiveRedemptionAmount == 0 && isFreeCart) {
2175        this.redeemLabel = this.localize(".redeem-button.free.string");
2176      } else {
2177        this.redeemLabel = this.localize(".redeem-button.active.string", {
2178          amount: this.formatCurrency(this.effectiveRedemptionAmount),
2179          pts: this.formatPoints(this.effectiveRedemptionPts),
2180        });
2181      }
2182    } else {
2183      this.redeemLabel = this.localize(".redeem-button.disabled.string");
2184    }
2185
2186    // if (shopDetails.isVerbose) {
2187    //   console.log(`[Bubblehouse] spendableAmount = ${spendableAmount}, cartCost = ${cartCost}, defaultRedemptionAmount = ${defaultRedemptionAmount}, unroundedEffectiveRedemptionAmount = ${unroundedEffectiveRedemptionAmount}, effectiveRedemptionPts = ${effectiveRedemptionPts}, effectiveRedemptionAmount = ${effectiveRedemptionAmount}`)
2188    // }
2189  }
2190
2191  minimalCurrencyStep() {
2192    return Math.pow(10, -this.data.shopCurrencyPrecision);
2193  }
2194  formatCurrency(amount) {
2195    return (
2196      this.data.shopCurrencyPrefix +
2197      this.formatBareCurrency(amount) +
2198      this.data.shopCurrencySuffix
2199    );
2200  }
2201  formatPoints(pts) {
2202    if (pts < 0) {
2203      return '–' + this.formatPoints(-pts)
2204    }
2205    return new Intl.NumberFormat(this.locale).format(pts);
2206  }
2207  formatBareCurrency(amount) {
2208    let n = this.data.shopCurrencyPrecision;
2209    return new Intl.NumberFormat(this.locale, {
2210      minimumFractionDigits: n,
2211      maximumFractionDigits: n,
2212    }).format(amount);
2213  }
2214  formatCurrencyInputValue(amount) {
2215    return amount.toFixed(this.data.shopCurrencyPrecision);
2216  }
2217  convertPointsToAmount(pts) {
2218    return Math.round(pts * this.data.ptValue * 10000) / 10000;
2219  }
2220  convertAmountToPoints(amount) {
2221    return Math.round((amount / this.data.ptValue) * 10000) / 10000;
2222  }
2223
2224  resolveValue(key) {
2225    let str = this.values[key];
2226    if (typeof str === "undefined") {
2227      throw new Error(`missing value ${key}`);
2228    }
2229    return str;
2230  }
2231
2232  resolvePixelValue(key) {
2233    let value = this.resolveValue(key);
2234    if (value == null) {
2235      return null;
2236    }
2237    let m = value.match(/^(\d+)px$/);
2238    if (m) {
2239      return parseInt(m[1], 10);
2240    }
2241    return null;
2242  }
2243
2244  customEmbed(key, stringFunc, childrenFuncs) {
2245    let string = this.resolveValue(key)
2246    if (string === '') {
2247      return;
2248    }
2249    parseStringTemplate(string, stringFunc, (fieldKey, i) => {
2250      let child = childrenFuncs[fieldKey];
2251      if (child) {
2252        child();
2253      } else {
2254        stringFunc(`{{!.${fieldKey}!}}`);
2255      }
2256    });
2257  }
2258
2259  localize(key, vals) {
2260    return localize(this.values, key, vals);
2261  }
2262}
2263
2264
2265/* conditions.js */
2266function isConditionAttributeValueMatched(conditions) {
2267    if (!conditions) {
2268        return true
2269    }
2270    return conditions.split(/\s+/).every(isConditionMatched)
2271}
2272
2273function isConditionMatched(cond) {
2274    if (cond && cond[0] == '!') {
2275        return !isConditionMatched(cond.substr(1))
2276    }
2277    switch (cond) {
2278        case '': return true
2279        case 'never': return false
2280        case 'tester': return !!Bubblehouse.data.tester
2281        case 'logged_in': return !!Bubblehouse.data.loggedIn
2282        case 'widget1': return !!Bubblehouse.data.widget1
2283        case 'widget2': return !!Bubblehouse.data.widget2
2284        case 'widget3': return !!Bubblehouse.data.widget3
2285        default: console.error(`[Bubblehouse] invalid condition: if="... ${cond} ..."`); return false
2286    }
2287}
2288
2289function adjustPanelVisibility(element) {
2290    let conditions = element.getAttribute('if')
2291    if (!isConditionAttributeValueMatched(conditions)) {
2292        console.log(`[Bubblehouse] hiding <${element.tagName}> because conditions do not match (${conditions})`)
2293        if (element.style.display !== 'none') {
2294            element.style.display = 'none'
2295        }
2296        return false
2297    }
2298    if (element.style.display === 'none') {
2299        element.style.display = element.getAttribute('display') || ''
2300    }
2301    if (element.style.visibility === 'hidden') {
2302        element.style.visibility = 'visible'
2303    }
2304    if (Bubblehouse.verbose) console.log(`[Bubblehouse] showing <${element.tagName}>`)
2305    return true
2306}
2307
2308
2309/* call.js */
2310Bubblehouse.call = call;
2311
2312function call(funcName, input) {
2313  let headers = {
2314    'Content-Type': 'application/json',
2315    'Accept': 'application/json',
2316  }
2317  if (Bubblehouse.data.authToken) {
2318    headers['Authorization'] = `Bearer ${Bubblehouse.data.authToken}`
2319  }
2320  return fetch(Bubblehouse.data.callUrl.replace('FUNCNAME', funcName), {
2321    method: 'POST',
2322    body: JSON.stringify(input || {}),
2323    headers: headers,
2324  }).then(wantSuccessfulJson)    
2325}
2326
2327
2328/* tags/bubblehouse-redeem-panel.js */
2329;(function() {
2330const stylesheet = `
2331:root { display: flex; padding: 4px }
2332.hidden { display: none !important }
2333
2334.inputItem { margin-left: 4px; margin-right: 4px }
2335.inputItem:first-child { margin-left: 0 }
2336.inputItem:last-child { margin-right: 0 }
2337
2338input[type=number].nostepper {-moz-appearance: textfield }
2339input[type=number].nostepper::-webkit-inner-spin-button,
2340input[type=number].nostepper::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0 }
2341`;
2342const UTILITY_CLASSES = { hidden: true };
2343
2344const experimentalInplaceFlow = true;
2345
2346class BubblehouseRedeemPanel extends HTMLElement {
2347  constructor() {
2348    super();
2349
2350    this._onCartChanged = this._onCartChanged.bind(this);
2351    this._onCartLoaded = this._onCartLoaded.bind(this);
2352    this._onCartLoadingFailed = this._onCartLoadingFailed.bind(this);
2353    this._onWidgetDataLoaded = this._onWidgetDataLoaded.bind(this);
2354    this._onWidgetDataLoadingFailed =
2355      this._onWidgetDataLoadingFailed.bind(this);
2356    this._onRedemptionAmountChange = this._onRedemptionAmountChange.bind(this);
2357    this._onRedemptionPointsChange = this._onRedemptionPointsChange.bind(this);
2358    this.redeem = this.redeem.bind(this);
2359    this.initialized = false;
2360
2361    this.redemptionPending = false;
2362    this.redemptionError = null;
2363    this.cartData = null;
2364    this.redemptionData = null;
2365    this.desiredRedemptionAmountOrPoints = null;
2366    this.stylesheetReady = false;
2367    this.cartGeneration = 0;
2368    this.statusGeneration = 0;
2369
2370    this.attachShadow({ mode: "open" });
2371  }
2372
2373  connectedCallback() {
2374    if (!adjustPanelVisibility(this)) return;
2375    Bubblehouse.cartListeners.push(this._onCartChanged);
2376    this.reload("initial");
2377    this.render();
2378  }
2379
2380  disconnectedCallback() {
2381    removeArrayElement(Bubblehouse.cartListeners, this._onCartChanged);
2382  }
2383
2384  cartCouponCodes() {
2385    if (!this.cartData) {
2386      return null;
2387    }
2388    return (this.cartData.cart_level_discount_applications || [])
2389      .filter((da) => da.type === "discount_code")
2390      .map((da) => da.title)
2391  }
2392
2393  _collectCheckoutContext() {
2394    let context = {
2395      site: "cart",
2396      country: Shopify.country,
2397      currency: Shopify.currency && Shopify.currency.active,
2398      locale: (Shopify.locale || "").trim(),
2399      timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
2400
2401      discountCodesKnown: true,
2402      discountCodes: this.cartCouponCodes(),
2403    };
2404    if (this.cartData) {
2405      context.cartSubtotal = (this.cartData.total_price / 100).toFixed(6);
2406    }
2407    return context;
2408  }
2409
2410  reload(why) {
2411    this.reloadCart();
2412    this.reloadStatus(why);
2413  }
2414
2415  reloadCart() {
2416    let generation = ++this.cartGeneration;
2417    loadCart().then(
2418      (cartData) => this._onCartLoaded(cartData, generation),
2419      (error) => this._onCartLoadingFailed(error, generation),
2420    );
2421  }
2422
2423  reloadStatus(why) {
2424    if (!this.cartData) {
2425      return;
2426    }
2427    let generation = ++this.statusGeneration;
2428    call("CheckoutStatus1", {
2429      ...this._collectCheckoutContext(),
2430    }).then(
2431      (redemptionData) => this._onWidgetDataLoaded(redemptionData, generation),
2432      (error) => this._onWidgetDataLoadingFailed(error, generation),
2433    );
2434  }
2435
2436  render(origin) {
2437    // if (Bubblehouse.verbose) console.log(`<${this.tagName}> rendering:\n\ncartData = ${JSON.stringify(this.cartData, null, 2)}\n\nredemptionData = ${JSON.stringify(this.redemptionData, null, 2)}`);
2438
2439    let vm = (this.vm = new RedeemVM(
2440      this.redemptionData,
2441      this.cartData ? this.cartData.total_price / 100 : null,
2442      this.cartData ? this.cartData.item_count : null,
2443      this.cartCouponCodes(),
2444      this.desiredRedemptionAmountOrPoints,
2445      undefined,
2446      {},
2447      { subtractProvisionalAmount: false, applyStatusGates: true }
2448    ));
2449
2450    let isContentVisible = vm.isLoaded && !vm.data?.hide;
2451    let isStatusVisible = Boolean(vm.data?.hide && vm.data?.hideMessage);
2452    let isRootVisible = isContentVisible || isStatusVisible;
2453    let isDisabled = isStatusVisible || vm.isRedemptionBlockedByCheckoutState;
2454
2455    if (!this.initialized && vm.stylesheetUrl) {
2456      this.initialized = true;
2457      if (Bubblehouse.verbose) {
2458        console.log(
2459          `[Bubblehouse] <${this.tagName}> initialized with vm: `,
2460          vm,
2461        );
2462      }
2463
2464      let panelCells = [];
2465      if (vm.col1 === "amount") {
2466        panelCells.push(h("div#availableAmount"));
2467      } else if (vm.col1 === "points") {
2468        panelCells.push(h("div#availablePoints"));
2469      }
2470      if (vm.col2 === "points") {
2471        panelCells.push(h("div#availablePoints"));
2472      } else if (vm.col2 === "amount") {
2473        panelCells.push(h("div#availableAmount"));
2474      }
2475      if (vm.col1 === "amount") {
2476        panelCells.push(h("div#redemptionAmountCell"));
2477      } else if (vm.col1 === "points") {
2478        panelCells.push(h("div#redemptionPtsCell"));
2479      }
2480      if (vm.col2 === "points") {
2481        panelCells.push(h("div#redemptionPtsCell"));
2482      } else if (vm.col2 === "amount") {
2483        panelCells.push(h("div#redemptionAmountCell"));
2484      }
2485
2486      let logo = null;
2487      let logoUrl = vm.values[".logo.image"];
2488      if (logoUrl) {
2489        logo = h("img#logoImg", { src: logoUrl, alt: "" });
2490      }
2491
2492      let stylesheetEl = h("link", { rel: "stylesheet", href: vm.stylesheetUrl });
2493      stylesheetEl.addEventListener("load", () => {
2494        this.stylesheetReady = true;
2495        this.render("stylesheet_loaded");
2496      });
2497      this.shadowRoot.append(
2498        h("style", stylesheet),
2499        stylesheetEl,
2500        h(
2501          "div#rootPanel",
2502          { style: { display: "none" } },
2503          logo,
2504          h("div#amountsPanel", panelCells),
2505          h(`button#redeemButton`, {
2506            type: "button",
2507            onclick: this.redeem,
2508          }),
2509          h("div#statusPanel", h("span#statusMessageSpan")),
2510        ),
2511      );
2512      assignElementRefs(this, this.shadowRoot);
2513      this.setRootPanelVisible(false);
2514
2515      this.applyClass(this.rootPanel, "widget");
2516      this.applyClass(this.logoImg, "logo");
2517      this.applyClass(this.statusPanel, "message-panel");
2518      this.applyClass(this.amountsPanel, "amounts-panel");
2519      this.applyClass(this.redeemButton, "redeem-button");
2520      this.applyClass(this.availableAmount, "available-amount");
2521      this.applyClass(this.availablePoints, "available-points");
2522      this.applyClass(this.redemptionAmountCell, "redeem-amount");
2523      this.applyClass(this.redemptionPtsCell, "redeem-points");
2524
2525      this.embedChildren(
2526        this.availableAmount,
2527        ".available-amount.overall.string",
2528        {
2529          value: () => h("span#availableAmountValue"),
2530        },
2531      );
2532      this.applyClass(this.availableAmountValue, "available-amount-value");
2533
2534      this.embedChildren(
2535        this.availablePoints,
2536        ".available-points.overall.string",
2537        {
2538          value: () => h("span#availablePointsValue"),
2539        },
2540      );
2541      this.applyClass(this.availablePointsValue, "available-points-value");
2542
2543      if (vm.col1 === "amount") {
2544        this.embedChildren(
2545          this.redemptionAmountCell,
2546          ".redeem-amount.overall.string",
2547          {
2548            value: () =>
2549              h(
2550                "span#redemptionAmountValue.inputItem",
2551                {},
2552                h(
2553                  "span#redemptionAmountPrefixSpan",
2554                  vm.data.shopCurrencyPrefix,
2555                ),
2556                h("input#redemptionInput.nostepper", {
2557                  type: "number",
2558                  min: "0",
2559                  step: "" + vm.minimalCurrencyStep(),
2560                  placeholder: vm.formatBareCurrency(0),
2561                  onchange: this._onRedemptionAmountChange,
2562                  oninput: this._onRedemptionAmountChange,
2563                }),
2564                h(
2565                  "span#redemptionAmountSuffixSpan",
2566                  vm.data.shopCurrencySuffix,
2567                ),
2568              ),
2569          },
2570        );
2571        this.embedChildren(
2572          this.redemptionPtsCell,
2573          ".redeem-points.overall.string",
2574          {
2575            value: () => h("span#redemptionPtsValueSpan"),
2576          },
2577        );
2578        this.detailRedemptionCell = this.redemptionPtsCell;
2579      } else if (vm.col1 === "points") {
2580        this.embedChildren(
2581          this.redemptionPtsCell,
2582          ".redeem-points.overall.string",
2583          {
2584            value: () =>
2585              h(
2586                "span#redemptionPtsValueSpan.inputItem",
2587                h("input#redemptionInput.nostepper", {
2588                  type: "number",
2589                  min: "0",
2590                  onchange: this._onRedemptionPointsChange,
2591                  oninput: this._onRedemptionPointsChange,
2592                }),
2593              ),
2594          },
2595        );
2596        this.embedChildren(
2597          this.redemptionAmountCell,
2598          ".redeem-amount.overall.string",
2599          {
2600            value: () => h("span#redemptionAmountValue"),
2601          },
2602        );
2603        this.detailRedemptionCell = this.redemptionAmountCell;
2604      }
2605      this.applyClass(this.redemptionInput, "input-field");
2606      this.applyClass(this.redemptionAmountValue, "redeem-amount-value");
2607      this.applyClass(this.redemptionPtsValueSpan, "redeem-points-value");
2608    }
2609    if (!this.initialized) {
2610      return;
2611    }
2612
2613    this.applyClass(this.logoImg, {
2614      "logo--enabled": !isDisabled,
2615      "logo--disabled": isDisabled,
2616    });
2617    // this.applyClass(this.logoImg, , isDisabled);
2618    this.applyClass(this.rootPanel, {
2619      "widget--enabled": !isDisabled,
2620      "widget--disabled": isDisabled,
2621    });
2622    this.applyClass(this.statusPanel, {
2623      "message-panel--enabled": !isDisabled,
2624      "message-panel--disabled": isDisabled,
2625    });
2626    this.applyClass(this.amountsPanel, { hidden: !isContentVisible });
2627    this.applyClass(this.redeemButton, { hidden: !isContentVisible });
2628
2629    console.log(
2630      `[Bubblehouse] <${this.tagName}> render: isContentVisible = ${isContentVisible}, vm.data?.hide = ${vm.data?.hide}, this.amountsPanel.className = ${this.amountsPanel.className}, this.redeemButton.className = ${this.redeemButton.className}`,
2631    );
2632
2633    if (vm.data?.hide) {
2634      this._renderStatusMessage(vm.data.hideMessage, vm.data.actions);
2635      this.setRootPanelVisible(isRootVisible);
2636      return;
2637    } else if (!vm.isLoaded) {
2638      this._renderStatusMessage("");
2639      this.setRootPanelVisible(isRootVisible);
2640      return;
2641    }
2642
2643    if (this.availableAmountValue) {
2644      this.availableAmountValue.textContent = vm.formatCurrency(
2645        vm.spendableAmount,
2646      );
2647    }
2648    if (this.availablePointsValue) {
2649      this.availablePointsValue.textContent = vm.formatPoints(vm.spendablePts);
2650    }
2651    if (this.detailRedemptionCell) {
2652      this.detailRedemptionCell.classList.toggle(
2653        "hidden",
2654        vm.visibleRedemptionPts == null,
2655      );
2656    }
2657    if (this.redemptionPtsValueSpan && vm.col2 === "points") {
2658      this.redemptionPtsValueSpan.textContent =
2659        vm.visibleRedemptionPts != null
2660          ? vm.formatPoints(vm.visibleRedemptionPts)
2661          : "";
2662    }
2663    if (this.redemptionAmountValue && vm.col2 === "amount") {
2664      this.redemptionAmountValue.textContent =
2665        vm.effectiveRedemptionAmount != null
2666          ? vm.formatCurrency(vm.effectiveRedemptionAmount)
2667          : "";
2668    }
2669    if (this.shadowRoot.activeElement !== this.redemptionInput) {
2670      if (vm.col1 === "amount" && vm.effectiveRedemptionAmount != null) {
2671        this.redemptionInput.value = vm.formatBareCurrency(
2672          vm.effectiveRedemptionAmount,
2673        );
2674      } else if (vm.col1 === "points" && vm.effectiveRedemptionPts != null) {
2675        this.redemptionInput.value = "" + vm.effectiveRedemptionPts;
2676      }
2677    }
2678    this.redeemButton.textContent = (this.redemptionPending ? vm.localize(".message.redeeming.string") : vm.redeemLabel);
2679    this.redeemButton.disabled = this.redemptionPending || !vm.isRedeemEnabled;
2680
2681    this.redemptionInput.setCustomValidity(vm.amountError || "");
2682
2683    if (this.redemptionPending) {
2684      this._renderStatusMessage("");
2685    } else if (vm.redeemDisabledMessage) {
2686      this._renderStatusMessage(vm.redeemDisabledMessage);
2687    } else if (this.redemptionError) {
2688      this._renderStatusMessage(this.redemptionError);
2689    } else {
2690      this._renderStatusMessage("");
2691    }
2692    this.setRootPanelVisible(isRootVisible);
2693  }
2694
2695  redeem() {
2696    if (this.redemptionPending) {
2697      return;
2698    }
2699    this.redemptionPending = true;
2700    this.redemptionError = null;
2701    this.render();
2702
2703    this._redeemAsync()
2704      .then(() => {
2705        if (this.redemptionData.useAdvancedIntegration) {
2706          this.redemptionPending = false;
2707        } else {
2708          // don't mark as done -- we're navigating away
2709        }
2710        this.redemptionError = null;
2711        this.render();
2712      })
2713      .catch((error) => {
2714        reportError(error, "redeem");
2715        this.redemptionPending = false;
2716        this.redemptionError = error.toString();
2717        this.render();
2718      });
2719  }
2720
2721  applyClass(el, ...args) {
2722    if (!el) {
2723      return;
2724    }
2725    for (let arg of args) {
2726      if (typeof arg === 'string') {
2727        this._toggleSingleClass(el, arg, true);
2728      } else if (typeof arg === 'object') {
2729        for (let [shortClassName, isOn] of Object.entries(arg)) {
2730          this._toggleSingleClass(el, shortClassName, isOn);
2731        }
2732      }
2733    }
2734  }
2735
2736  setRootPanelVisible(isVisible) {
2737    this.rootPanel.classList.toggle("hidden", !isVisible);
2738    if (isVisible) {
2739      if (this.stylesheetReady) {
2740        this.rootPanel.style.removeProperty("display");
2741      } else {
2742        this.rootPanel.style.setProperty("display", "none");
2743      }
2744    } else {
2745      this.rootPanel.style.setProperty("display", "none", "important");
2746    }
2747  }
2748
2749  _toggleSingleClass(el, shortClassName, isOn) {
2750    if (UTILITY_CLASSES[shortClassName]) {
2751      el.classList.toggle(shortClassName, isOn);
2752    } else if (this.vm.data) {
2753      let className = this.vm.classes[shortClassName];
2754      if (!className) {
2755        throw new Error(`missing theme class ${shortClassName}`);
2756      }
2757      el.classList.toggle(className, isOn);
2758    }
2759  }
2760
2761  embedChildren(el, stringKey, childrenKeys) {
2762    if (!el) {
2763      return;
2764    }
2765    parseStringTemplate(
2766      this.vm.resolveValue(stringKey),
2767      (staticStr) => {
2768        el.append(h("span", {}, staticStr));
2769      },
2770      (fieldKey) => {
2771        let child = childrenKeys[fieldKey];
2772        if (typeof child === "undefined") {
2773          el.append(`{{!.${fieldKey}!}}`);
2774        } else {
2775          if (typeof child === "function") {
2776            child = child();
2777          }
2778          appendChild(el, child);
2779        }
2780      },
2781    );
2782    assignElementRefs(this, el);
2783  }
2784
2785  async _redeemAsync() {
2786    if (this.vm.effectiveRedemptionAmount == null) {
2787      throw new Error("Invalid amount");
2788    }
2789    if (this.vm.effectiveRedemptionAmount == 0) {
2790      if (!this.redemptionData.useAdvancedIntegration) {
2791        window.location.href = `/checkout`;
2792      }
2793      return;
2794    }
2795    let input = {
2796      amount: `${this.vm.effectiveRedemptionAmount.toFixed(6)}`,
2797      canRemove: true,
2798      ...this._collectCheckoutContext(),
2799    };
2800    let output = await call("RedeemCheckoutReward1", input);
2801    if (Bubblehouse.verbose)
2802      console.log(
2803        `[Bubblehouse] <${this.tagName}> redeem input = ${JSON.stringify(input)}, output = ${JSON.stringify(output)}`,
2804      );
2805
2806    if (output.error) {
2807      throw new Error(output.message || output.error);
2808    }
2809
2810    if (output.newStatus) {
2811      this.statusGeneration++;
2812      this.redemptionData = output.newStatus;
2813    }
2814
2815    if (output.type === "discount_code" || output.type === "gift_card") {
2816      await this._addDiscountCode({ type: output.type, code: output.code }, output.removeCodes);
2817    } else {
2818      throw new Error(
2819        `Unknown type of reward: ${output.type} (full response: ${JSON.stringify(output)})`,
2820      );
2821    }
2822    this.desiredRedemptionAmountOrPoints = null;
2823
2824    this.reloadCart();
2825  }
2826
2827  async _addDiscountCode(coupon, removeCodes) {
2828    if (isShopifyStorefrontApiAvailable()) {
2829      await addDiscountCode(this.cartData.token, coupon.code, removeCodes);
2830      this._sendToDestination(null);
2831    } else {
2832      this._sendToDestination(coupon);
2833    }
2834  }
2835
2836  _sendToDestination(coupon) {
2837    this.dispatchEvent(
2838      new CustomEvent("bubblehouse:cart-updated", {
2839        detail: {},
2840        bubbles: true,
2841      }),
2842    );
2843    if (this.redemptionData.useAdvancedIntegration) {
2844      return;
2845    }
2846    let destination = this.redemptionData.sendToCart ? "/cart" : "/checkout";
2847    if (!coupon) {
2848      window.location.href = destination;
2849    } else if (coupon.type === "discount_code" && this.redemptionData.useShopifyDiscountPath) {
2850      window.location.href = `/discount/${encodeURIComponent(coupon.code)}?redirect=${destination}`;
2851    } else {
2852      window.location.href = `${destination}?discount=${encodeURIComponent(coupon.code)}`;
2853    }
2854  }
2855
2856  _onRedemptionAmountChange(ev) {
2857    let amount = parseFloat(this.redemptionInput.value || "");
2858    if (isNaN(amount) || !isFinite(amount)) {
2859      amount = NaN; // null would mean default; NaN means custom but invalid
2860    } else {
2861      amount = this.vm.convertPointsToAmount(
2862        Math.ceil(this.vm.convertAmountToPoints(amount)),
2863      );
2864    }
2865    this.desiredRedemptionAmountOrPoints = amount;
2866    this.render();
2867    this.redemptionInput.reportValidity();
2868  }
2869
2870  _onRedemptionPointsChange(ev) {
2871    let pts = parseInt(this.redemptionInput.value || "", 10);
2872    this.desiredRedemptionAmountOrPoints = pts;
2873    this.render();
2874    this.redemptionInput.reportValidity();
2875  }
2876
2877  _renderStatusMessage(message, actions = null) {
2878    if (message) {
2879      this.statusPanel.classList.remove("hidden");
2880      if (actions) {
2881        applyContentWithActions(this.statusMessageSpan, message, actions);
2882      } else {
2883        this.statusMessageSpan.textContent = message;
2884      }
2885    } else {
2886      this.statusPanel.classList.add("hidden");
2887      this.statusMessageSpan.textContent = "";
2888    }
2889  }
2890
2891  _onCartChanged(cartData, cartAction) {
2892    this.cartGeneration++;
2893    this.cartData = cartData;
2894    this.reloadStatus("cart_updated");
2895    this.render();
2896  }
2897
2898  _onCartLoaded(cartData, generation) {
2899    if (generation !== this.cartGeneration) {
2900      return;
2901    }
2902    this.cartData = cartData;
2903    this.reloadStatus("cart_loaded");
2904    this.render();
2905  }
2906  _onCartLoadingFailed(error, generation) {
2907    if (generation !== this.cartGeneration) {
2908      return;
2909    }
2910    reportWarning(error, "cannot load cart");
2911    this.render();
2912  }
2913
2914  _onWidgetDataLoaded(redemptionData, generation) {
2915    if (generation !== this.statusGeneration) {
2916      return;
2917    }
2918    this.redemptionData = redemptionData;
2919    this.render();
2920  }
2921  _onWidgetDataLoadingFailed(error, generation) {
2922    if (generation !== this.statusGeneration) {
2923      return;
2924    }
2925    reportWarning(error, "cannot load redemption data");
2926    this.render();
2927  }
2928}
2929
2930function applyContentWithActions(el, contentStr, actions) {
2931  let validTags = Object.keys(actions).map((k) => 'bhaction:' + k);
2932  let root = parseTags(contentStr, { validTags });
2933  console.log(`[Bubblehouse] applyContentWithActions: str = ${JSON.stringify(contentStr)}, actions = ${JSON.stringify(actions)}, content = ${JSON.stringify(root, null, 2)}`)
2934
2935  while (el.firstChild) {
2936    el.removeChild(el.firstChild);
2937  }
2938
2939  appendRenderTagContent(el, root, actions);
2940}
2941
2942function appendRenderTagContent(el, content, actions) {
2943  for (let item of content) {
2944    appendRenderTagItem(el, item, actions);
2945  }
2946}
2947
2948function appendRenderTagItem(el, item, actions) {
2949  if (typeof item === 'string') {
2950    el.appendChild(document.createTextNode(item));
2951  } else {
2952    let actionName = item.tag.replace(/^bhaction:/, '')
2953    let action = actions[actionName]
2954    if (!action) {
2955      el.appendChild(document.createTextNode('(!TAG!)'));
2956      appendRenderTagContent(el, item.body, actions);
2957      return
2958    }
2959    let actionEl = document.createElement('a');
2960    if (action.jscall) {
2961      actionEl.addEventListener('click', (ev) => {
2962        if (runActionJSCall(item.tag, action.jscall)) {
2963          ev.preventDefault();
2964        }
2965      });
2966    }
2967    actionEl.setAttribute('part', 'actionlink');
2968    if (action.nav) {
2969      actionEl.href = action.nav.url;
2970      let target = actionNavigationTarget(action.nav.target);
2971      if (target) {
2972        actionEl.target = target;
2973      }
2974    }
2975    el.appendChild(actionEl);
2976    appendRenderTagContent(actionEl, item.body, actions);
2977  }
2978}
2979
2980function runActionJSCall(tag, call) {
2981  let name = call.func;
2982  let func = window[name];
2983  if (typeof func !== "function") {
2984    console.error(`[Bubblehouse] <${tag}>: global function ${name} not found`);
2985    return false;
2986  }
2987  try {
2988    func();
2989    return true;
2990  } catch (error) {
2991    reportError(error, `<${tag}>`);
2992    return false;
2993  }
2994}
2995
2996function actionNavigationTarget(target) {
2997  switch (target) {
2998  case "new":
2999  case "_blank":
3000    return "_blank";
3001  case "top":
3002  case "_top":
3003    return "_top";
3004  case "same":
3005  case "":
3006  case null:
3007  case undefined:
3008    return "";
3009  default:
3010    console.error(`[Bubblehouse] unsupported action navigation target ${target}`);
3011    return "";
3012  }
3013}
3014
3015customElements.define("bubblehouse-redeem-panel", BubblehouseRedeemPanel);
3016})() || true;
3017
3018/* variantChangeEvent.js */
3019;(function() {
3020if (isShopifyProductPage()) {
3021  window.addEventListener("load", () => {
3022    observeElements('[name="id"]', observeAndDispatchVariantChange)
3023  })
3024}
3025
3026function observeAndDispatchVariantChange(input) {
3027  let observer = new MutationObserver((mutations) => {
3028    for (const mutation of mutations) {
3029      if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
3030        window.dispatchEvent(
3031          new CustomEvent('bubblehouse:variant-change', { detail: { variantId: mutation.target.value } })
3032        )
3033      }
3034    }
3035  })
3036
3037  observer.observe(input, {
3038    attributes: true,
3039    attributeFilter: ['value'],
3040  })
3041}
3042})() || true;
3043
3044/* bigcommerce/api.js */
3045;(function() {
3046window.Bubblehouse.BigCommerce ??= {};
3047window.Bubblehouse.BigCommerce.getToken = getToken;
3048window.Bubblehouse.BigCommerce.loadCart = loadCart;
3049window.Bubblehouse.BigCommerce.createCart = createCart;
3050window.Bubblehouse.BigCommerce.addToCart = addToCart;
3051window.Bubblehouse.BigCommerce.updateCartItem = updateCartItem;
3052window.Bubblehouse.BigCommerce.deleteCartItem = deleteCartItem;
3053window.Bubblehouse.BigCommerce.deleteCart = deleteCart;
3054window.Bubblehouse.BigCommerce.getCheckout = getCheckout;
3055window.Bubblehouse.BigCommerce.applyCouponToCheckout = applyCouponToCheckout;
3056
3057async function getToken(appClientId) {
3058  return fetch(`/customer/current.jwt?app_client_id=${appClientId}`).then(
3059    (response) => response.text());
3060}
3061
3062function loadCart() {
3063  return fetch("/api/storefront/carts?include=lineItems.physicalItems.options,lineItems.digitalItems.options", {
3064    method: "GET",
3065    credentials: "same-origin",
3066  }).then(wantSuccessfulJson);
3067}
3068
3069function createCart(lineItems) {
3070  let body = { lineItems };
3071  console.log("[BH] [DEBUG] {BigCommerce} creating BigCommerce cart:", body);
3072  return fetch("/api/storefront/carts", {
3073    method: "POST",
3074    credentials: "same-origin",
3075    headers: { "Content-Type": "application/json" },
3076    body: JSON.stringify(body),
3077  }).then(wantSuccessfulJson);
3078}
3079
3080function addToCart(cartId, lineItems, { origin } = {}) {
3081  let body = { lineItems };
3082  console.log(`[BH] [DEBUG] {BigCommerce} adding to BigCommerce cart (${origin}):`, body);
3083  return fetch(`/api/storefront/carts/${cartId}/items`, {
3084    method: "POST",
3085    credentials: "same-origin",
3086    headers: { "Content-Type": "application/json" },
3087    body: JSON.stringify(body),
3088  }).then(wantSuccessfulJson);
3089}
3090
3091function updateCartItem(cartId, itemId, lineItem, { origin } = {}) {
3092  let body = { lineItem };
3093  console.log(`[BH] [DEBUG] {BigCommerce} updating BigCommerce cart item (${origin}):`, body);
3094  return fetch(`/api/storefront/carts/${cartId}/items/${itemId}`, {
3095    method: "PUT",
3096    credentials: "same-origin",
3097    headers: { "Content-Type": "application/json" },
3098    body: JSON.stringify(body),
3099  }).then(wantSuccessfulJson);
3100}
3101
3102function deleteCartItem(cartId, itemId, { origin } = {}) {
3103  console.log(`[BH] [DEBUG] {BigCommerce} deleting BigCommerce cart item (${origin}):`, itemId);
3104  return fetch(`/api/storefront/carts/${cartId}/items/${itemId}`, {
3105    method: "DELETE",
3106    credentials: "same-origin",
3107  }).then(wantSuccessfulJson);
3108}
3109
3110function deleteCart(cartId, { origin } = {}) {
3111  console.log(`[BH] [DEBUG] {BigCommerce} deleting BigCommerce cart (${origin}):`, cartId);
3112  return fetch(`/api/storefront/carts/${cartId}`, {
3113    method: "DELETE",
3114    credentials: "same-origin",
3115  }).then(wantSuccessfulJson);
3116}
3117
3118async function getCheckout(checkoutId) {
3119  return fetch(`/api/storefront/checkouts/${checkoutId}`, {
3120    method: "GET",
3121    credentials: "same-origin",
3122    headers: { "Content-Type": "application/json" },
3123  }).then(wantSuccessfulJson);
3124}
3125
3126async function applyCouponToCheckout(checkoutId, couponCode) {
3127  console.log(`[BH] [DEBUG] {BigCommerce}
3127 applying coupon to BigCommerce checkout: ${couponCode}`);
3128  let body = { couponCode };
3129  return fetch(`/api/storefront/checkouts/${checkoutId}/coupons`, {
3130    method: "POST",
3131    credentials: "same-origin",
3132    headers: { "Content-Type": "application/json" },
3133    body: JSON.stringify(body),
3134  }).then(wantSuccessfulJson);
3135}
3136})() || true;
3137
3138/* woocommerce/api.js */
3139window.Bubblehouse ??= {};
3140window.Bubblehouse.WooCommerce ??= {};
3141
3142window.Bubblehouse.WooCommerce.loadCart = async function() {
3143  return await fetch(`/wp-json/wc/store/v1/cart`, {
3144    credentials: 'include',
3145  })
3146    .then(async (response) => ({
3147      ...await response.json(),
3148      token: response.headers.get("cart-token"),
3149    }))
3150    .catch((err) => {
3151      console.error(`[BH] {WooCommerce} Unable to load cart: ${err}`);
3152      return null;
3153    });
3154}
3155
3156window.Bubblehouse.WooCommerce.applyCoupon = async function(_token, code) {
3157  // NOTE (@seletskiy): Detect version of checkout used (older vs newer WooCommerce).
3158  //                    'bh-discounts' namespace and handler is defined in bubblehouse-woocomm PHP extension.
3159  var isBlockCheckout = (
3160    typeof window.wc !== 'undefined' &&
3161    window.wc.blocksCheckout &&
3162    typeof window.wc.blocksCheckout.extensionCartUpdate === 'function'
3163  );
3164
3165  if (isBlockCheckout) {
3166    try {
3167      await window.wc.blocksCheckout.extensionCartUpdate({
3168        namespace: 'bh-discounts',
3169        data: { coupon_code: code },
3170      });
3171
3172      console.log(`[BH] [DEBUG] {applyCoupon} Block Checkout - coupon applied`);
3173    } catch (err) {
3174      console.log(`[BH] [DEBUG] {applyCoupon} Block Checkout - error: ${err}`);
3175    };
3176  } else {
3177    jQuery.ajax({
3178      type: 'POST',
3179      url: woocommerce_params.ajax_url,
3180      data: {
3181        action: 'bh_apply_coupon',
3182        coupon_code: code,
3183      },
3184      success: () => {
3185        $(document.body).trigger('update_checkout');
3186        console.log(`[BH] [DEBUG] {applyCoupon} Classic Checkout - coupon applied`)
3187      },
3188      error: (err) => {
3189        console.log(`[BH] [DEBUG] {applyCoupon} Classic Checkout - error: ${err}`)
3190      },
3191    });
3192  }
3193}
3194
3195window.Bubblehouse.WooCommerce.removeCoupon = async function(token, code) {
3196  return await fetch(`/wp-json/wc/store/v1/cart/coupons/${code}`, {
3197    method: 'DELETE',
3198    credentials: 'include',
3199    headers: {
3200      'Content-Type': 'application/json',
3201      'Cart-Token': token,
3202    },
3203  })
3204    .then((response) => response.json())
3205    .catch((err) => {
3206      console.error(`[BH] {WooCommerce} Unable to remove coupon: ${err}`);
3207      return null;
3208    });
3209}
3210
3211window.Bubblehouse.WooCommerce.removeAllCoupons = async function(token) {
3212  return await fetch(`/wp-json/wc/store/v1/cart/coupons`, {
3213    method: 'DELETE',
3214    credentials: 'include',
3215    headers: {
3216      'Content-Type': 'application/json',
3217      'Cart-Token': token,
3218    },
3219  })
3220    .then((response) => response.json())
3221    .catch((err) => {
3222      console.error(`[BH] {WooCommerce} Unable to remove all coupons: ${err}`);
3223      return null;
3224    });
3225}
3226
3227window.Bubblehouse.WooCommerce.addItem = async function(token, productId, quantity) {
3228  return await fetch(`/wp-json/wc/store/v1/cart/add-item`, {
3229    method: 'POST',
3230    credentials: 'include',
3231    headers: {
3232      'Content-Type': 'application/json',
3233      'Cart-Token': token,
3234    },
3235    body: JSON.stringify({ id: productId, quantity }),
3236  })
3237    .then((response) => response.json())
3238    .catch((err) => {
3239      console.error(`[BH] {WooCommerce} Unable to add item: ${err}`);
3240      return null;
3241    });
3242}
3243
3244window.Bubblehouse.WooCommerce.updateItem = async function(token, key, quantity) {
3245  return await fetch(`/wp-json/wc/store/v1/cart/update-item`, {
3246    method: 'POST',
3247    credentials: 'include',
3248    headers: {
3249      'Content-Type': 'application/json',
3250      'Cart-Token': token,
3251    },
3252    body: JSON.stringify({ key, quantity }),
3253  })
3254    .then((response) => response.json())
3255    .catch((err) => {
3256      console.error(`[BH] {WooCommerce} Unable to update item: ${err}`);
3257      return null;
3258    });
3259}
3260
3261
3262/* cart.js */
3263;(function() {
3264function CartApi() {
3265  const providers = {
3266    BigCommerce: {
3267      load: async () => {
3268        const carts = await Bubblehouse.BigCommerce.loadCart()
3269        if (!carts) {
3270          console.error(`[BH] {CartApi/BigCommerce} Unable to load carts.`);
3271          return null;
3272        }
3273
3274        if (carts?.length < 1) {
3275          console.warn(`[BH] {CartApi/BigCommerce} No active carts found.`);
3276          return { id: null, items: [] };
3277        }
3278
3279        const cart = carts[0];
3280
3281        return {
3282          id: cart.id,
3283          items: [
3284            ...cart.lineItems.physicalItems.map((item) => ({
3285              id: item.id,
3286              variant: { id: item.variantId },
3287              product: { id: item.productId },
3288            })),
3289          ]
3290        }
3291      },
3292
3293      add: async (cartId, { products, discount }, opts) => {
3294        if (products) {
3295          const items = products.map(({ product, quantity, variant, options }) => ({
3296            productId: product?.id,
3297            variantId: variant?.id,
3298            quantity,
3299            optionSelections: options
3300              ? Object.entries(options).map(([k, v]) => ({ optionId: k, optionValue: v }))
3301              : [],
3302          }));
3303
3304          if (cartId) {
3305            await Bubblehouse.BigCommerce.addToCart(cartId, items, opts);
3306          } else {
3307            const { id } = await Bubblehouse.BigCommerce.createCart(items);
3308            cartId = id;
3309          }
3310        }
3311
3312        if (discount) {
3313          await Bubblehouse.BigCommerce.applyCouponToCheckout(cartId, discount.code)
3314        }
3315      },
3316
3317      update: async (cartId, { items }, opts) => {
3318        return await Promise.all(items.map(({ id, variant, product, quantity }) =>
3319          Bubblehouse.BigCommerce.updateCartItem(
3320            cartId,
3321            id,
3322            {
3323              productId: product?.id,
3324              variantId: variant?.id,
3325              quantity,
3326            },
3327            opts,
3328          ),
3329        ))
3330      },
3331    },
3332
3333    Shopify: {
3334      load: async () => {
3335        const cart = await Bubblehouse.loadCart();
3336        if (!cart) {
3337          console.error(`[BH] {CartApi/Shopify} Unable to load cart.`);
3338          return null;
3339        }
3340
3341        return {
3342          id: cart.token,
3343          items: cart.items.map((item) => ({
3344            id: item.key,
3345            variant: { id: item.variant_id },
3346            product: { id: item.product_id },
3347          }))
3348        }
3349      },
3350
3351      add: async (cartId, { products, discount }, opts) => {
3352        if (products) {
3353          const items = products.map(({ variant, quantity }) => ({
3354            id: variant?.id,
3355            quantity,
3356          }));
3357
3358          await Bubblehouse.addToCart(items, opts);
3359        }
3360
3361        if (discount) {
3362          await Bubblehouse.addDiscountCode(cartId, discount.code);
3363        }
3364      },
3365
3366      update: async (_cartId, { items }, opts) => {
3367        const updates = {};
3368        items.forEach(({ id, quantity }) => {
3369          updates[id] = quantity;
3370        });
3371
3372        return await Bubblehouse.updateCart(updates, opts);
3373      },
3374    },
3375
3376    WooCommerce: {
3377      load: async () => {
3378        const cart = await Bubblehouse.WooCommerce.loadCart();
3379        if (!cart) {
3380          console.error(`[BH] {CartApi/WooCommerce} Unable to load cart.`);
3381          return null;
3382        }
3383
3384        return {
3385          id: cart.token,
3386          items: cart.items?.map((item) => ({
3387            id: item.key,
3388            variant: { id: item.id?.toString() },
3389            product: { id: item.id?.toString() },
3390          })) ?? []
3391        }
3392      },
3393
3394      add: async (token, { products, discount }, _opts) => {
3395        if (products) {
3396          for (const { product, quantity } of products) {
3397            await Bubblehouse.WooCommerce.addItem(token, product?.id, quantity);
3398          }
3399        }
3400
3401        if (discount) {
3402          await Bubblehouse.WooCommerce.applyCoupon(token, discount.code);
3403        }
3404      },
3405
3406      update: async (token, { items }, _opts) => {
3407        for (const { id, quantity } of items) {
3408          await Bubblehouse.WooCommerce.updateItem(token, id, quantity);
3409        }
3410      },
3411    }
3412  }
3413
3414  const isBigCommerce = window.BCData !== undefined;
3415  const isShopify = window.Shopify !== undefined;
3416  const isWooCommerce = window.wc !== undefined;
3417
3418  if (isBigCommerce) {
3419    console.log("[BH] {CartApi} Using BigCommerce.")
3420    return providers.BigCommerce;
3421  }
3422
3423  if (isWooCommerce) {
3424    console.log("[BH] {CartApi} Using WooCommerce.")
3425    return providers.WooCommerce;
3426  }
3427
3428  if (isShopify) {
3429    console.log("[BH] {CartApi} Using Shopify.")
3430    return providers.Shopify;
3431  }
3432
3433  const noop = () => {
3434    console.log("[BH] {CartApi} Not supported.")
3435  }
3436
3437  return { load: noop, add: noop, update: noop };
3438}
3439
3440window.Bubblehouse.CartApi = CartApi;
3441})() || true;
3442
3443/* outerFlash.js */
3444function showFlashMessage(message) {
3445  if (isPopupAvailable('message')) {
3446    openPopup('message', { message })
3447  } else {
3448    let text = message.t
3449    if (message.m === 'failure') {
3450      text = 'Error: ' + text
3451    }
3452    alert(text)
3453  }
3454}
3455
3456
3457/* outerApi.js */
3458;(function() {
3459async function handleApplyDiscountCode1(ev) {
3460  const code = ev.detail.code;
3461  if (!code) {
3462    return;
3463  }
3464
3465  const api = window.Bubblehouse.CartApi();
3466  try {
3467    const cart = await api.load();
3468    if (!cart) {
3469      console.log("[BH] {ApplyDiscountCode1} No cart info available.")
3470      return;
3471    }
3472
3473    await api.add(
3474      cart.id,
3475      {
3476        products: null,
3477        discount: { code },
3478      },
3479      { origin: "ApplyDiscountCode1" }
3480    );
3481  } catch (error) {
3482    cartErrorShow(error, code);
3483  }
3484}
3485
3486async function handleAddProductToCart1(ev) {
3487  const {
3488    variantId, productId, options, discountCode, quantity,
3489    successMessage, cartUrl, refreshOnAdd, ignoreIfAlreadyAdded,
3490  } = ev.detail;
3491  const api = window.Bubblehouse.CartApi();
3492
3493  try {
3494    const cart = await api.load();
3495    if (!cart) {
3496      console.log("[BH] {AddProductToCart1} No cart info available.")
3497      return;
3498    }
3499
3500    const alreadyAdded = ignoreIfAlreadyAdded && cart.items.some(({ variant }) => variant?.id == variantId);
3501    if (alreadyAdded && !discountCode) {
3502      console.log(`[BH] {AddProductToCart1} Already got product variant in cart.`)
3503      return;
3504    }
3505
3506    await api.add(
3507      cart.id,
3508      {
3509        products: alreadyAdded ? null : [{
3510          variant: { id: variantId },
3511          quantity,
3512          ...(productId ? { product: { id: productId } } : {}),
3513          options,
3514        }],
3515        discount: discountCode ? { code: discountCode } : null,
3516      },
3517      { origin: "AddProductToCart1" }
3518    );
3519  } catch (error) {
3520    cartErrorShow(error, discountCode);
3521    return;
3522  }
3523
3524  if (successMessage) {
3525    showFlashMessage({ t: successMessage })
3526    if (refreshOnAdd) {
3527      setTimeout(() => { window.location.reload(); }, 1000)
3528    }
3529  } else {
3530    if (cartUrl !== null) {
3531      navigateToCartOrReloadPage(cartUrl)
3532    }
3533  }
3534}
3535
3536function cartErrorShow(error, code) {
3537  console.error("[BH] Unable to update cart:", error);
3538  const message = (code ? Bubblehouse.data?.cartCouponErrorMessage : Bubblehouse.data?.cartErrorMessage) ||
3539    error?.message || String(error);
3540  showFlashMessage({ m: 'failure', t: [message, code].filter(Boolean).join('\n') });
3541}
3542
3543document.addEventListener('bhclient:AddProductToCart1', handleAddProductToCart1);
3544document.addEventListener('bhclient:ApplyDiscountCode1', handleApplyDiscountCode1);
3545})() || true;
3546
3547/* resizer.js */
3548document.addEventListener('bhclient:Resize1', function (ev) {
3549  ev.target.style.height = ev.detail.height + 'px';
3550});
3551
3552
3553/* outerMessage.js */
3554document.addEventListener('bhclient:PostOuterMessage1', handlePostOuterMessage1);
3555
3556function handlePostOuterMessage1(ev) {
3557  window.postMessage({ event: ev.detail.event, data: ev.detail.data }, '*')
3558}
3559
3560
3561/* tags/bubblehouse-value.js */
3562;(function() {
3563const VALUES = {
3564    'points': () => Bubblehouse.data.userPointBalanceStr,
3565    'points-value': () => Bubblehouse.data.userPointBalanceValueStr,
3566    'tier': () => Bubblehouse.data.userTierName,
3567}
3568
3569// <bubblehouse-value key="..."> displays the specified loyalty value (properly formatted for the current customer).
3570class BubblehouseValue extends HTMLElement {
3571    constructor() {
3572        super();
3573        const shadow = this.attachShadow({ mode: 'open' });
3574        this.valueSpan = document.createElement('span');
3575        shadow.appendChild(this.valueSpan);
3576    }
3577
3578    connectedCallback() {
3579        this.render();
3580    }
3581
3582    render() {
3583        let key = this.getAttribute('key')
3584        if (!key) {
3585            key = this.getAttribute('altkey') // don't ask, it's an attempt to debug a problem with Wypeuk
3586        }
3587        let func = VALUES[key]
3588        if (!func) {
3589            console.error(`[Bubblehouse] <bubblehouse-value> has invalid key=${JSON.stringify(key)}`)
3590        }
3591        let value = (func ? func() : 'invalid-value-key')
3592        let fallback = this.getAttribute('fallback')
3593        let text
3594        if (value == null || value === '') {
3595            text = fallback || ''
3596        } else if (value == 0 && fallback != null) {
3597            text = fallback || ''
3598        } else {
3599            text = '' + value
3600        }
3601        this.valueSpan.textContent = text
3602        if (Bubblehouse.verbose) console.log(`[Bubblehouse] rendered <bubblehouse-value key="${key}">`)
3603    }
3604}
3605
3606customElements.define('bubblehouse-value', BubblehouseValue);
3607})() || true;
3608
3609/* tags/bubblehouse-panel.js */
3610;(function() {
3611// <bubblehouse-panel> unhides itself after Bubblehouse gets loaded.
3612class BubblehousePanel extends HTMLElement {
3613    connectedCallback() {
3614        adjustPanelVisibility(this)
3615    }
3616}
3617
3618customElements.define('bubblehouse-panel', BubblehousePanel);
3619})() || true;
3620
3621/* tags/bubblehouse-block.js */
3622;(function() {
3623// <bubblehouse-block> inserts the given Bubblehouse page/block.
3624class BubblehouseBlock extends HTMLElement {
3625    constructor() {
3626        super()
3627        this.attachShadow({ mode: 'open' })
3628        this._onMessage = this._onMessage.bind(this)
3629    }
3630
3631    connectedCallback() {
3632        this._initialize();
3633        if (!adjustPanelVisibility(this)) return;
3634        window.addEventListener('message', this._onMessage)
3635    }
3636
3637    disconnectedCallback() {
3638        window.removeEventListener('message', this._onMessage)
3639    }
3640
3641    _initialize() {
3642        if (this._initialized) {
3643            return
3644        }
3645        this._initialized = true;
3646
3647        let blockName = this.getAttribute('block')
3648        let suffix = this.getAttribute('suffix') || ''
3649        let height = this.getAttribute('height') || '0'
3650        let shopSlug = this.getAttribute('shop')
3651        let auth = this.getAttribute('auth')
3652        let title = (this.getAttribute('title') || '').trim()
3653        if (!blockName) {
3654            throw new Error('<bubblehouse-block>: block name must be specified (e.g. block="rewards")')
3655        }
3656        let encodedBlockName = encodeBubblehouseBlockPathSegment(blockName, 'block name')
3657        let instance = `bubblehouse-embedded-${blockName}${suffix}`
3658        let url
3659        if (!shopSlug && window.Shopify) {
3660            shopSlug = window.Shopify.shop.replace(/\.myshopify\.com$/, '')
3661            if (shopSlug) {
3662                shopSlug = 'shopify-' + shopSlug
3663            }
3664            auth = auth || Bubblehouse.data.authToken || ''
3665        }
3666        if (window.Shopify && !shopSlug) {
3667            url = new URL(`/apps/bubblehouse/${encodedBlockName}/content/`, window.location.origin)
3668        } else {
3669            if (!shopSlug) {
3670                throw new Error('<bubblehouse-block>: shop slug must be specified (e.g. shop="my-shop")')
3671            }
3672            if (auth == null) {
3673                throw new Error('<bubblehouse-block>: auth must be specified (as either an empty string or a JWT token, e.g. auth="" or auth="...")')
3674            }
3675            let encodedShopSlug = encodeBubblehouseBlockPathSegment(shopSlug, 'shop slug')
3676            url = new URL(`https://app.bubblehouse.com/s/${encodedShopSlug}/blocks/${encodedBlockName}`)
3677        }
3678        if (!this._canUseBubblehouseAuth(url)) {
3679            throw new Error('<bubblehouse-block>: invalid Bubblehouse iframe URL')
3680        }
3681        let shouldFetchAuthTokenForBlock = Boolean(Bubblehouse.data.fetchAuthTokenForBlock) && !auth
3682        if (!shouldFetchAuthTokenForBlock) {
3683            url.searchParams.set('auth', auth || '')
3684        }
3685        url.searchParams.set('instance', instance)
3686        for (let [name, value] of Object.entries(collectCurrentMarketQueryParams())) {
3687            url.searchParams.set(name, value)
3688        }
3689
3690        this.iframe = h('iframe', {
3691            src: shouldFetchAuthTokenForBlock ? null : url.toString(),
3692            style: {
3693                width: '100%',
3694                border: 'none',
3695                height: height + 'px',
3696            },
3697            title: title || null,
3698            sandbox: 'allow-top-navigation allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin',
3699            allow: 'clipboard-write',
3700        })
3701        this.shadowRoot.append(this.iframe)
3702        if (shouldFetchAuthTokenForBlock) {
3703            this._shopifyProxyAuthTokenPromise = this._loadWithShopifyProxyAuthToken(url)
3704        }
3705    }
3706
3707    applyBubblehouseAuthToken(token) {
3708        if (!token || !this.iframe) {
3709            return false
3710        }
3711        let url
3712        try {
3713            url = new URL(this.iframe.src, window.location.href)
3714        } catch {
3715            return false
3716        }
3717        if (url.searchParams.get('auth')) {
3718            return false
3719        }
3720        if (!this._canUseBubblehouseAuth(url)) {
3721            return false
3722        }
3723        url.searchParams.set('auth', token)
3724        this.iframe.src = url.toString()
3725        return true
3726    }
3727
3728    async _loadWithShopifyProxyAuthToken(url) {
3729        let token = await fetchShopifyProxyAuthToken()
3730        if (token) {
3731            Bubblehouse.data.authToken = token
3732            url.searchParams.set('auth', token)
3733        } else {
3734            url.searchParams.set('auth', '')
3735        }
3736        if (this.iframe) {
3737            this.iframe.src = url.toString()
3738        }
3739    }
3740
3741    _canUseBubblehouseAuth(url) {
3742        return isBubblehouseAuthFrameURL(url, 'https://app.bubblehouse.com', window.location.origin)
3743    }
3744
3745    _onMessage(ev) {
3746        let data = ev.data
3747        if (!data || ev.source !== this.iframe.contentWindow) {
3748            return
3749        }
3750        if (data.bubblehouseBlockClientCall === 'Scroll1') {
3751            this.iframe.dispatchEvent(new CustomEvent('bhclient:Scroll1', {
3752                detail: Object.assign({}, data, { sourceWindow: ev.source, sourceOrigin: ev.origin }),
3753                bubbles: true,
3754                composed: true,
3755            }))
3756            return
3757        }
3758        if (data.bubblehouseBlockClientCall !== 'Resize1') {
3759            return
3760        }
3761        if (data.height) {
3762            this.iframe.style.height = data.height + 'px'
3763        }
3764        if (!this.iframe.title && typeof data.title === 'string') {
3765            let title = data.title.trim()
3766            if (title) {
3767                this.iframe.title = title
3768            }
3769        }
3770    }
3771}
3772
3773function encodeBubblehouseBlockPathSegment(value, name) {
3774    if (!/^[A-Za-z0-9_-]+$/.test(value)) {
3775        throw new Error(`<bubblehouse-block>: ${name} must contain only letters, numbers, underscores, or dashes`)
3776    }
3777    return encodeURIComponent(value)
3778}
3779
3780async function fetchShopifyProxyAuthToken() {
3781    try {
3782        let response = await fetch('/apps/bubblehouse/customer-auth-token', {
3783            credentials: 'include',
3784            headers: {
3785                Accept: 'application/json',
3786            },
3787        })
3788        if (!response.ok) {
3789            return ''
3790        }
3791        let data = await response.json()
3792        return typeof data.token === 'string' ? data.token : ''
3793    } catch {
3794        return ''
3795    }
3796}
3797
3798customElements.define('bubblehouse-block', BubblehouseBlock);
3799})() || true;
3800
3801/* toc-scroll.js */
3802async function handleScroll1(ev) {
3803  let scrollEl = null;
3804  let gap = 16;
3805
3806  let ionContent = ev.target.closest('ion-content') || document.querySelector('ion-content');
3807  if (ionContent) {
3808    scrollEl = await ionContent.getScrollElement();
3809  }
3810
3811  if (scrollEl) {
3812    let containerRect = scrollEl.getBoundingClientRect();
3813    let baseOffset = ev.target.getBoundingClientRect().top - containerRect.top + scrollEl.scrollTop;
3814    let offset = Math.max(0, baseOffset - gap + ev.detail.offset);
3815    let scrollElDesc = scrollEl.id ? `${scrollEl.tagName.toLowerCase()}#${scrollEl.id}` : scrollEl.className ? `${scrollEl.tagName.toLowerCase()}.${scrollEl.className.split(' ')[0]}` : scrollEl.tagName.toLowerCase();
3816    console.log(`[Bubblehouse] scrolling ${scrollElDesc} to ${Math.round(offset)}px (currently at ${Math.round(scrollEl.scrollTop)}px, target ${ev.target.id} at ${Math.round(baseOffset)}px + ${Math.round(ev.detail.offset)}px offset - ${gap}px gap)`);
3817    scrollEl.scrollTo({ top: Math.round(offset), behavior: 'smooth' });
3818  } else {
3819    let baseOffset = ev.target.getBoundingClientRect().top + window.scrollY;
3820    let offset = Math.max(0, baseOffset - gap + ev.detail.offset);
3821    console.log(`[Bubblehouse] scrolling to ${Math.round(offset)}px (${Math.round(ev.detail.offset)}px within ${ev.target.id} + ${Math.round(baseOffset)}px iframe offset - ${gap}px gap)`);
3822    window.scroll({ top: Math.round(offset), behavior: 'smooth' });
3823  }
3824}
3825
3826document.addEventListener('bhclient:Scroll1', handleScroll1);
3827
3828
3829/* anchor-navigation.js */
3830const BUBBLEHOUSE_ANCHOR_PREFIX = "#bh-";
3831const BUBBLEHOUSE_ANCHOR_RETRY_DELAY_MS = 100;
3832const BUBBLEHOUSE_ANCHOR_RETRY_COUNT = 50;
3833const BUBBLEHOUSE_ANCHOR_FINAL_WAIT_MS = 1500;
3834const BUBBLEHOUSE_ANCHOR_SETTLE_DELAY_MS = 2000;
3835const BUBBLEHOUSE_ANCHOR_SETTLE_THRESHOLD_PX = 50;
3836
3837let bubblehouseAnchorNavigation = null;
3838let bubblehouseAnchorSettleTimerID = 0;
3839
3840navigateToCurrentBubblehouseAnchor();
3841window.addEventListener("hashchange", navigateToCurrentBubblehouseAnchor);
3842window.addEventListener("message", handleBubblehouseAnchorResizeMessage);
3843document.addEventListener("bhclient:Scroll1", handleBubblehouseAnchorScroll1);
3844
3845function navigateToCurrentBubblehouseAnchor() {
3846  let anchor = currentBubblehouseAnchor();
3847  if (!anchor) {
3848    cancelBubblehouseAnchorNavigation();
3849    return;
3850  }
3851  startBubblehouseAnchorNavigation(anchor);
3852}
3853
3854function startBubblehouseAnchorNavigation(anchor) {
3855  cancelBubblehouseAnchorNavigation();
3856  bubblehouseAnchorNavigation = {
3857    anchor,
3858    resizeRetryUsed: false,
3859    retriesRemaining: BUBBLEHOUSE_ANCHOR_RETRY_COUNT,
3860    timerID: 0,
3861  };
3862  attemptBubblehouseAnchorNavigation(false);
3863}
3864
3865function attemptBubblehouseAnchorNavigation(finalAttempt) {
3866  let state = bubblehouseAnchorNavigation;
3867  if (!state) {
3868    return;
3869  }
3870  state.timerID = 0;
3871  if (scrollToSameOriginBubblehouseAnchor(state.anchor)) {
3872    completeBubblehouseAnchorNavigation();
3873    return;
3874  }
3875  let posted = postNavigateToAnchor1(state.anchor);
3876  if (finalAttempt) {
3877    scheduleBubblehouseAnchorGiveUp(state.anchor, posted);
3878  } else if (state.retriesRemaining > 0) {
3879    state.retriesRemaining--;
3880    state.timerID = setTimeout(() => attemptBubblehouseAnchorNavigation(false), BUBBLEHOUSE_ANCHOR_RETRY_DELAY_MS);
3881  } else {
3882    warnBubblehouseAnchorNavigationFailed(state.anchor);
3883  }
3884}
3885
3886function currentBubblehouseAnchor() {
3887  let hash = location.hash || "";
3888  if (!hash.startsWith(BUBBLEHOUSE_ANCHOR_PREFIX)) {
3889    return "";
3890  }
3891  let anchor = hash.slice(BUBBLEHOUSE_ANCHOR_PREFIX.length);
3892  if (!anchor || anchor.includes("=")) {
3893    return "";
3894  }
3895  return anchor;
3896}
3897
3898function scrollToSameOriginBubblehouseAnchor(anchor) {
3899  for (let frame of bubblehouseAnchorFrames()) {
3900    let section;
3901    try {
3902      section = frame.contentDocument?.getElementById(anchor);
3903    } catch {
3904      section = null;
3905    }
3906    if (!section) {
3907      continue;
3908    }
3909    let inTabgroup = false;
3910    try {
3911      inTabgroup = !!(section.closest && section.closest('[data-controller~="tabgroup"]'));
3912    } catch {
3913    }
3914    let reveal;
3915    try {
3916      reveal = frame.contentWindow?.Bubblehouse?.revealTabgroupContaining;
3917    } catch {
3918      reveal = null;
3919    }
3920    if (inTabgroup && typeof reveal !== 'function') {
3921      continue;
3922    }
3923    if (typeof reveal === 'function') {
3924      try {
3925        section = reveal(section) || section;
3926      } catch {
3927      }
3928    }
3929    let offset = bubblehouseAnchorOffset(section);
3930    dispatchSameOriginBubblehouseAnchorScroll(frame, offset);
3931    scheduleSameOriginBubblehouseAnchorSettle(frame, section, offset);
3932    return true;
3933  }
3934  return false;
3935}
3936
3937function scheduleSameOriginBubblehouseAnchorSettle(frame, section, firstOffset) {
3938  clearBubblehouseAnchorSettleTimer();
3939  bubblehouseAnchorSettleTimerID = setTimeout(() => {
3940    bubblehouseAnchorSettleTimerID = 0;
3941    let settledOffset = bubblehouseAnchorOffset(section);
3942    if (Math.abs(settledOffset - firstOffset) > BUBBLEHOUSE_ANCHOR_SETTLE_THRESHOLD_PX) {
3943      dispatchSameOriginBubblehouseAnchorScroll(frame, settledOffset);
3944    }
3945  }, BUBBLEHOUSE_ANCHOR_SETTLE_DELAY_MS);
3946}
3947
3948function bubblehouseAnchorOffset(section) {
3949  return section.getBoundingClientRect ? section.getBoundingClientRect().top : section.offsetTop;
3950}
3951
3952function dispatchSameOriginBubblehouseAnchorScroll(frame, offset) {
3953  frame.dispatchEvent(new CustomEvent("bhclient:Scroll1", {
3954    detail: {
3955      bubblehouseBlockClientCall: "Scroll1",
3956      instance: frame.id,
3957      offset,
3958    },
3959    bubbles: true,
3960    composed: true,
3961  }));
3962}
3963
3964function postNavigateToAnchor1(anchor) {
3965  let posted = 0;
3966  for (let frame of bubblehouseAnchorFrames()) {
3967    let contentWindow = frame.contentWindow;
3968    if (!contentWindow?.postMessage) {
3969      continue;
3970    }
3971    contentWindow.postMessage({
3972      bubblehouseBlockClientCall: "NavigateToAnchor1",
3973      instance: "#self",
3974      anchor,
3975    }, bubblehouseAnchorTargetOrigin(frame));
3976    posted++;
3977  }
3978  return posted;
3979}
3980
3981function handleBubblehouseAnchorScroll1(ev) {
3982  if (!bubblehouseAnchorNavigation || !isTrustedBubblehouseAnchorSource(ev.detail?.sourceWindow)) {
3983    return;
3984  }
3985  completeBubblehouseAnchorNavigation();
3986}
3987
3988function handleBubblehouseAnchorResizeMessage(ev) {
3989  let state = bubblehouseAnchorNavigation;
3990  if (!state || state.resizeRetryUsed || !isResize1Message(ev.data) || !isTrustedBubblehouseAnchorSource(ev.source)) {
3991    return;
3992  }
3993  state.resizeRetryUsed = true;
3994  clearBubblehouseAnchorTimer();
3995  attemptBubblehouseAnchorNavigation(true);
3996}
3997
3998function isResize1Message(data) {
3999  return data && data.bubblehouseBlockClientCall === "Resize1";
4000}
4001
4002function isTrustedBubblehouseAnchorSource(source) {
4003  return !!source && bubblehouseAnchorFrames().some((frame) => frame.contentWindow === source);
4004}
4005
4006function bubblehouseAnchorFrames() {
4007  return collectBubblehouseFrames(document).filter((frame) => !!bubblehouseAnchorFrameURL(frame));
4008}
4009
4010function bubblehouseAnchorFrameURL(frame) {
4011  try {
4012    let url = new URL(frame.getAttribute("src") || frame.src, location.href);
4013    return isBubblehouseAuthFrameURL(url, bubblehouseAppOrigin(), location.origin) ? url : null;
4014  } catch {
4015    return null;
4016  }
4017}
4018
4019function bubblehouseAnchorTargetOrigin(frame) {
4020  return bubblehouseAnchorFrameURL(frame)?.origin || "*";
4021}
4022
4023function scheduleBubblehouseAnchorGiveUp(anchor, posted) {
4024  if (!bubblehouseAnchorNavigation) {
4025    return;
4026  }
4027  if (posted) {
4028    bubblehouseAnchorNavigation.timerID = setTimeout(() => warnBubblehouseAnchorNavigationFailed(anchor), BUBBLEHOUSE_ANCHOR_FINAL_WAIT_MS);
4029  } else {
4030    warnBubblehouseAnchorNavigationFailed(anchor);
4031  }
4032}
4033
4034function warnBubblehouseAnchorNavigationFailed(anchor) {
4035  if (!bubblehouseAnchorNavigation) {
4036    return;
4037  }
4038  console.warn("[Bubblehouse] giving up on navigation to anchor " + anchor);
4039  cancelBubblehouseAnchorNavigation();
4040}
4041
4042function completeBubblehouseAnchorNavigation() {
4043  clearBubblehouseAnchorTimer();
4044  bubblehouseAnchorNavigation = null;
4045}
4046
4047function cancelBubblehouseAnchorNavigation() {
4048  completeBubblehouseAnchorNavigation();
4049  clearBubblehouseAnchorSettleTimer();
4050}
4051
4052function clearBubblehouseAnchorTimer() {
4053  if (bubblehouseAnchorNavigation?.timerID) {
4054    clearTimeout(bubblehouseAnchorNavigation.timerID);
4055    bubblehouseAnchorNavigation.timerID = 0;
4056  }
4057}
4058
4059function clearBubblehouseAnchorSettleTimer() {
4060  if (bubblehouseAnchorSettleTimerID) {
4061    clearTimeout(bubblehouseAnchorSettleTimerID);
4062    bubblehouseAnchorSettleTimerID = 0;
4063  }
4064}
4065
4066
4067/* public.js */
4068Bubblehouse.centsToAccruedPoints = centsToAccruedPoints
4069Bubblehouse.pointsBalance = pointsBalance
4070Bubblehouse.pointsBalanceStr = pointsBalanceStr
4071Bubblehouse.pointsBalanceMonetaryValue = pointsBalanceMonetaryValue
4072Bubblehouse.pointsBalanceMonetaryValueStr = pointsBalanceMonetaryValueStr
4073
4074Bubblehouse.ONE_TIME_PURCHASE = 'onetime'
4075Bubblehouse.SUBSCRIPTION = 'sub'
4076
4077function centsToAccruedPoints(cents, scenario, options = {}) {
4078  if (scenario === 'onetime') {
4079    return Math.round(cents * Bubblehouse.data.ppc + 0.001)
4080  } else if (scenario === 'sub') {
4081    return Math.round(cents * Bubblehouse.data.ppcs + 0.001)
4082  } else {
4083    throw new Error('unsupported scenario')
4084  }
4085}
4086
4087Bubblehouse.PTS_TOTAL = 'total'
4088Bubblehouse.PTS_AVAILABLE = 'avail'
4089
4090function pointsBalance(kind) {
4091  if (kind === 'total') {
4092    return Bubblehouse.data.userPointBalance
4093  } else if (kind === 'avail') {
4094    return Bubblehouse.data.userPointBalanceAvail
4095  } else {
4096    throw new Error('unsupported kind')
4097  }
4098}
4099
4100function pointsBalanceStr(kind) {
4101  if (kind === 'total') {
4102    return Bubblehouse.data.userPointBalanceStr
4103  } else if (kind === 'avail') {
4104    return Bubblehouse.data.userPointBalanceAvailStr
4105  } else {
4106    throw new Error('unsupported kind')
4107  }
4108}
4109
4110function pointsBalanceMonetaryValue(kind) {
4111  if (kind === 'total') {
4112    return Bubblehouse.data.userPointBalanceValue
4113  } else if (kind === 'avail') {
4114    return Bubblehouse.data.userPointBalanceAvailValue
4115  } else {
4116    throw new Error('unsupported kind')
4117  }
4118}
4119
4120function pointsBalanceMonetaryValueStr(kind) {
4121  if (kind === 'total') {
4122    return Bubblehouse.data.userPointBalanceValueStr
4123  } else if (kind === 'avail') {
4124    return Bubblehouse.data.userPointBalanceAvailValueStr
4125  } else {
4126    throw new Error('unsupported kind')
4127  }
4128}
4129
4130
4131/* integrations.js */
4132;(function() {
4133if (Bubblehouse.isInner) {
4134   function invokeIntegration(key, method, ...args) {
4135    let message = {
4136      bubblehouseBlockClientCall: 'InvokeIntegration1',
4137      instance: '#self',
4138      key, method, args
4139    };
4140    window.parent.postMessage(message, '*');
4141  }
4142
4143  Bubblehouse.invokeIntegration = invokeIntegration;
4144} else {
4145  Bubblehouse.integrations = {};
4146
4147  function handleInvokeIntegration1(ev) {
4148    let { key, method, args } = ev.detail;
4149    if (Bubblehouse.integrations[key] === undefined)
4150      throw new Error(`[Bubblehouse] Integration with key "${key}" is not enabled`);
4151    let ingr = Bubblehouse.integrations[key];
4152    if (ingr[method] === undefined)
4153      throw new Error(`[Bubblehouse] Integration "${key}" does not provide method "${method}"`);
4154
4155    console.log(`[Bubblehouse] Calling integration "${key}" method "${method}" with args "${JSON.stringify(args)}"`);
4156    ingr[method].call(ingr, ...args);
4157  }
4158
4159  document.addEventListener('bhclient:InvokeIntegration1', handleInvokeIntegration1);
4160}
4161})() || true;
4162
4163/* integrations/postscript.js */
4164;(function() {
4165class Postscript {
4166  constructor() {}
4167
4168  async OpenPopup1(popups, opts) {
4169    let id = popups.default;
4170    if (popups.auto) {
4171      id = await window.postscript.popups.find();
4172    } else {
4173      if (popups.mobile && this.isMobile()) {
4174        id = popups.mobile;
4175      }
4176    }
4177
4178    // Postscript requires Number for legacy popup ID, while new popups use UUID.
4179    if (!isNaN(+id)) {
4180        id = +id;
4181    }
4182
4183    window.postscript.popups.open(id, opts);
4184  }
4185
4186  isMobile() {
4187    return window.matchMedia("(min-device-width : 300px)").matches &&
4188      window.matchMedia("(max-device-width : 480px)").matches;
4189  }
4190}
4191
4192Bubblehouse.integrations.postscript = new Postscript();
4193})() || true;
4194
4195/* load.js */
4196document.dispatchEvent(new CustomEvent('bubblehouse:load'));
4197
4198
4199/* elements/embed-iframe.js */
4200class EmbedIframe extends HTMLElement {
4201    constructor() {
4202        super();
4203    }
4204
4205    connectedCallback() {
4206      const iframe = document.createElement("iframe");
4207
4208      this.getAttributeNames().map((an) => {
4209        iframe.setAttribute(an, this.getAttribute(an));
4210      });
4211
4212      const src = iframe.getAttribute("src");
4213      iframe.setAttribute("src", src.replace("$AUTH_TOKEN", Bubblehouse.data.authToken));
4214
4215      this.replaceWith(iframe);
4216    }
4217}
4218
4219customElements.define("bh-embed-iframe", EmbedIframe);
4220
4221
4222})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.