PageSourceSearch

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

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

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.