PageSourceSearch

https://www.plantpeople.co/apps/bubblehouse/bubblehouse.js

js plantpeople.co collected 2026-10-03 22:40:16 UTC 128,732 bytes, 4,176 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-plant-people-new",
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; margin: auto; 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; margin: auto; 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.plantpeople.co/apps/bubblehouse/blocks/POPUPNAME","callUrl":"https://app.bubblehouse.com/api/v2023061/shopify-plant-people-new/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:","widget1":true,"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-plant-people-new"],"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/* bh-magic.js */
1069(() => {
1070  const url = new URL(location);
1071  const query = url.searchParams;
1072  const token = query.get("bh-magic");
1073  if (token) {
1074    query.delete("bh-magic");
1075    const target = new URL(Bubblehouse.root + "/sso/auth/" + token);
1076    target.searchParams.set("return_url", url.toString());
1077    location.href = target.toString()
1078  }
1079})();
1080
1081
1082/* actionTokenRelay.js */
1083const BUBBLEHOUSE_ACTION_QUERY_PARAM = Bubblehouse.actionQueryParam;
1084
1085let bubblehouseActionToken = readBubblehouseActionToken();
1086let bubblehouseRejectedActionToken = "";
1087let bubblehouseActionTokenConflict = false;
1088let bubblehouseActionTokenObserver = null;
1089
1090processBubblehouseActionToken();
1091onDOMContentLoaded(processBubblehouseActionToken);
1092window.addEventListener("message", handleBubblehouseActionTokenStatus);
1093Bubblehouse.prepareFrameURLForAuthReload = prepareBubblehouseActionFrameURLForAuthReload;
1094
1095function readBubblehouseActionToken() {
1096  try {
1097    let values = new URL(location.href).searchParams.getAll(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1098    return values.length === 1 ? values[0] : "";
1099  } catch {
1100    return "";
1101  }
1102}
1103
1104function processBubblehouseActionToken() {
1105  if (!bubblehouseActionToken) {
1106    return
1107  }
1108  applyBubblehouseActionTokenToNode(document);
1109  observeLateBubblehouseActionFrames();
1110}
1111
1112function applyBubblehouseActionTokenToNode(node) {
1113  let appOrigin = bubblehouseActionAppOrigin();
1114  let shopSlugs = bubblehouseActionShopSlugs();
1115  let proxyPaths = bubblehouseActionProxyPaths();
1116  let candidates = [];
1117  for (let frame of collectBubblehouseActionFramesIncludingRoot(node)) {
1118    let frameURL;
1119    try {
1120      frameURL = new URL(frame.src, location.href);
1121    } catch {
1122      continue
1123    }
1124    if (!isBubblehouseActionFrameURL(frameURL, appOrigin, location.origin, shopSlugs, proxyPaths)) {
1125      continue
1126    }
1127    candidates.push({ frame, frameURL });
1128  }
1129  for (let { frame, frameURL } of candidates) {
1130    if (bubblehouseRejectedActionToken &&
1131        frameURL.searchParams.get(BUBBLEHOUSE_ACTION_QUERY_PARAM) === bubblehouseRejectedActionToken) {
1132      try {
1133        frameURL.searchParams.delete(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1134        frame.src = frameURL.toString();
1135      } catch {
1136      }
1137    }
1138  }
1139  if (!bubblehouseActionToken || bubblehouseActionTokenConflict) {
1140    return
1141  }
1142  let conflictingCandidates = candidates.filter(({ frameURL }) => {
1143    let values = frameURL.searchParams.getAll(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1144    return values.length > 0 && (values.length !== 1 || values[0] !== bubblehouseActionToken)
1145  });
1146  if (conflictingCandidates.length > 0) {
1147    bubblehouseActionTokenConflict = true;
1148    if (Bubblehouse.verbose) {
1149      console.log("[Bubblehouse] Action Token relay conflict; no outer token selected");
1150    }
1151    return
1152  }
1153  for (let { frame, frameURL } of candidates) {
1154    let values = frameURL.searchParams.getAll(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1155    if (values.length > 0) {
1156      continue
1157    }
1158    try {
1159      frameURL.searchParams.set(BUBBLEHOUSE_ACTION_QUERY_PARAM, bubblehouseActionToken);
1160      frame.src = frameURL.toString();
1161    } catch {
1162    }
1163  }
1164}
1165
1166function observeLateBubblehouseActionFrames() {
1167  if (bubblehouseActionTokenObserver || typeof MutationObserver !== "function") {
1168    return
1169  }
1170  let root = document.body || document.documentElement;
1171  if (!root) {
1172    return
1173  }
1174  try {
1175    bubblehouseActionTokenObserver = new MutationObserver((mutations) => {
1176      for (let mutation of mutations) {
1177        for (let node of mutation.addedNodes || []) {
1178          applyBubblehouseActionTokenToNode(node);
1179        }
1180      }
1181    });
1182    bubblehouseActionTokenObserver.observe(root, { childList: true, subtree: true });
1183  } catch {
1184  }
1185}
1186
1187function collectBubblehouseActionFramesIncludingRoot(root) {
1188  let frames = collectBubblehouseFrames(root);
1189  if (root && root.tagName && root.tagName.toLowerCase() === "iframe") {
1190    frames.unshift(root);
1191  }
1192  return frames
1193}
1194
1195function bubblehouseActionAppOrigin() {
1196  for (let value of [Bubblehouse.data.callUrl, Bubblehouse.root]) {
1197    if (!value) {
1198      continue
1199    }
1200    try {
1201      return new URL(value, location.href).origin
1202    } catch {
1203    }
1204  }
1205  return ""
1206}
1207
1208function handleBubblehouseActionTokenStatus(ev) {
1209  let status = ev.data && ev.data.bubblehouseActionTokenStatus;
1210  let acknowledgedToken = ev.data && ev.data.bubblehouseActionToken;
1211  let trustedTokenStatus = acknowledgedToken === bubblehouseActionToken ||
1212    (status === "rejected" && acknowledgedToken === "");
1213  if (!bubblehouseActionToken ||
1214      !ev.data || !trustedTokenStatus ||
1215      (bubblehouseActionTokenConflict && status !== "rejected") ||
1216      (status !== "captured" && status !== "rejected")) {
1217    return
1218  }
1219  if (!isTrustedBubblehouseActionTokenStatus(ev)) {
1220    return
1221  }
1222  let token = bubblehouseActionToken;
1223  if (status === "rejected") {
1224    bubblehouseRejectedActionToken = token;
1225    bubblehouseActionToken = "";
1226  }
1227  clearOuterBubblehouseActionToken(token);
1228}
1229
1230function isTrustedBubblehouseActionTokenStatus(ev) {
1231  let appOrigin = bubblehouseActionAppOrigin();
1232  let shopSlugs = bubblehouseActionShopSlugs();
1233  let proxyPaths = bubblehouseActionProxyPaths();
1234  for (let frame of collectBubblehouseFrames(document)) {
1235    let frameURL;
1236    try {
1237      frameURL = new URL(frame.src, location.href);
1238    } catch {
1239      continue
1240    }
1241    let actionValues = frameURL.searchParams.getAll(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1242    if (frame.contentWindow === ev.source &&
1243        frameURL.origin === ev.origin &&
1244        actionValues.length === 1 &&
1245        actionValues[0] === bubblehouseActionToken &&
1246        isBubblehouseActionFrameURL(frameURL, appOrigin, location.origin, shopSlugs, proxyPaths)) {
1247      return true
1248    }
1249  }
1250  return false
1251}
1252
1253function bubblehouseActionShopSlugs() {
1254  let configured = Bubblehouse.extra && Bubblehouse.extra.actionShopSlugs;
1255  let slugs = Array.isArray(configured) ? configured.filter((slug) => typeof slug === "string" && slug) : [];
1256  let rootSlug = bubblehouseRootShopSlug(Bubblehouse.root, location.href);
1257  if (rootSlug && !slugs.includes(rootSlug)) {
1258    slugs.push(rootSlug);
1259  }
1260  return slugs
1261}
1262
1263function bubblehouseActionProxyPaths() {
1264  let configured = Bubblehouse.extra && Bubblehouse.extra.actionProxyPaths;
1265  return Array.isArray(configured) ? configured.filter((path) => typeof path === "string" && path) : []
1266}
1267
1268function prepareBubblehouseActionFrameURLForAuthReload(url) {
1269  if (bubblehouseRejectedActionToken &&
1270      url.searchParams.get(BUBBLEHOUSE_ACTION_QUERY_PARAM) === bubblehouseRejectedActionToken) {
1271    url.searchParams.delete(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1272  }
1273}
1274
1275function clearOuterBubblehouseActionToken(token) {
1276  try {
1277    let url = new URL(location.href);
1278    if (url.searchParams.get(BUBBLEHOUSE_ACTION_QUERY_PARAM) !== token) {
1279      return
1280    }
1281    url.searchParams.delete(BUBBLEHOUSE_ACTION_QUERY_PARAM);
1282    history.replaceState(history.state, "", url.toString());
1283  } catch {
1284  }
1285}
1286
1287
1288/* util/wantSuccessfulJson.js */
1289function wantSuccessfulJson(response) {
1290  if (response.ok) {
1291    return response.json()
1292  } else {
1293    throw new Error(`${response.url} returned HTTP ${response.status} ${response.statusText}`)
1294  }
1295}
1296
1297
1298/* util/waitAll.js */
1299function waitAll(results) {
1300  return Promise.all(results.map((v) => Promise.resolve(v)))
1301}
1302
1303
1304/* util/reportError.js */
1305function reportError(error, context = '') {
1306  console.error(`[Bubblehouse] error: ${context ? context + ': ' : ''}${error.message || error}`)
1307  if (error.stack) {
1308    console.error(error.stack)
1309  }
1310}
1311
1312function reportWarning(error, context = '') {
1313  console.warn(`[Bubblehouse] warning: ${context ? context + ': ' : ''}${error.message || error}`)
1314}
1315
1316
1317/* util/debounceAsyncWithRepeat.js */
1318function debounceAsyncWithRepeat(debugName, debounceInterval, f) {
1319  const IDLE = 0, PENDING = 1, RUNNING = 2, RUNNING_AND_PENDING = 3;
1320  let state = -1; setState(IDLE);
1321  let pendingArgs = [];
1322  return request;
1323
1324  function request(...args) {
1325    pendingArgs = args;
1326    if (state === IDLE) {
1327      schedule()
1328    } else if (state === RUNNING) {
1329      setState(RUNNING_AND_PENDING)
1330    }
1331  }
1332
1333  function schedule() {
1334    setState(PENDING);
1335    setTimeout(start, debounceInterval);
1336  }
1337
1338  function start() {
1339    let args = pendingArgs;
1340    pendingArgs = [];
1341    setState(RUNNING);
1342    let promise = f.apply(null, args);
1343    if (promise && promise.then) {
1344      promise.then(finish, finish)
1345    } else {
1346      finish()
1347    }
1348  }
1349
1350  function finish(result) {
1351    if (result instanceof Error) {
1352      console.error(`[Bubblehouse] ${debugName} failed:`, result)
1353    } else {
1354      if (Bubblehouse.verbose) console.log(`[Bubblehouse] ${debugName} finished:`, result)
1355    }
1356    if (state === RUNNING_AND_PENDING) {
1357      start();
1358    } else {
1359      setState(IDLE);
1360    }
1361  }
1362
1363  function setState(newState) {
1364    // if (Bubblehouse.verbose) console.log(`[Bubblehouse] ${debugName} state transition: ${state} => ${newState}`);
1365    state = newState;
1366  }
1367}
1368
1369
1370/* shopify/cartApi.js */
1371// updateCart = debounceAsyncWithRepeat('updateCart', 1, updateCart);
1372
1373Bubblehouse.loadCart = loadCart;
1374Bubblehouse.addToCart = addToCart;
1375Bubblehouse.updateCart = updateCart;
1376Bubblehouse.callStorefront = callStorefront;
1377Bubblehouse.addDiscountCode = addDiscountCode;
1378
1379function loadCart() {
1380  return fetch(Shopify.routes.root + "cart.js").then(wantSuccessfulJson);
1381}
1382
1383// items = [{ id: giftVariantId, quantity: 1 }]
1384function addToCart(items, { origin } = {}) {
1385  let body = { items };
1386  console.log(`[Bubblehouse] adding to cart (${origin}):`, body);
1387  return fetch(Shopify.routes.root + "cart/add.js?bubblehouse-cart-ignore=1", {
1388    method: "POST",
1389    body: JSON.stringify(body),
1390    headers: { "Content-Type": "application/json" },
1391  }).then(wantSuccessfulJson);
1392}
1393
1394// updates = { key1: quantity1, ... }
1395function updateCart(updates, { origin } = {}) {
1396  let body = { updates };
1397  console.log(`[Bubblehouse] updating cart (${origin}):`, body);
1398  return fetch(
1399    `${Shopify.routes.root}cart/update.js?bubblehouse-cart-ignore=1`,
1400    {
1401      method: "POST",
1402      body: JSON.stringify(body),
1403      headers: { "Content-Type": "application/json" },
1404    },
1405  );
1406}
1407
1408async function addDiscountCode(_cartToken, discountCode, codesToRemove = []) {
1409  const cart = await loadCart();
1410  const existingDiscountCodes = (cart.discount_codes || []).map(({ code }) => code);
1411  const discountCodes = [...new Set([
1412    ...existingDiscountCodes.filter((code) => !codesToRemove.includes(code)),
1413    discountCode,
1414  ])];
1415  if (Bubblehouse.verbose) {
1416    console.log(`[BH Debug] new discountCodes = ${JSON.stringify(discountCodes)}`)
1417  }
1418  const updated = await setDiscountCodes(discountCodes);
1419  if (!updated.discount_codes?.some(({ code, applicable }) =>
1420    code.toLowerCase() === discountCode.toLowerCase() && applicable
1421  )) {
1422    throw new Error("Shopify did not apply the discount code");
1423  }
1424}
1425
1426function setDiscountCodes(discountCodes) {
1427  const body = { discount: discountCodes.join(",") };
1428  return fetch(
1429    `${Shopify.routes.root}cart/update.js?bubblehouse-cart-ignore=1`,
1430    {
1431      method: "POST",
1432      body: JSON.stringify(body),
1433      headers: { "Content-Type": "application/json" },
1434    },
1435  ).then(wantSuccessfulJson);
1436}
1437
1438function isShopifyStorefrontApiAvailable() {
1439  return !!Bubblehouse.data.storefrontToken;
1440}
1441
1442async function callStorefront(query, variables) {
1443  let token = Bubblehouse.data.storefrontToken;
1444  if (!token) {
1445    throw new Error("misconfigured: missing storefront token");
1446  }
1447
1448  let m = query.match(/result: (\w+)/);
1449  if (!m) {
1450    throw new Error("invalid GQL query");
1451  }
1452  let callName = m[1];
1453
1454  let response = await fetch("/api/2025-01/graphql.json", {
1455    method: "POST",
1456    headers: {
1457      "Content-Type": "application/json",
1458      "X-Shopify-Storefront-Access-Token": token,
1459    },
1460    body: JSON.stringify({ query, variables }),
1461  });
1462  let data = await response.json();
1463  if (Bubblehouse.verbose) {
1464    console.log(`[Bubblehouse] callStorefront ${callName}(${JSON.stringify(variables)}) => ${JSON.stringify(data)}`)
1465}
1466  if (data?.errors) {
1467    // {"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"}}]}
1468
1469    console.error(`[Bubblehouse] ${callName} GQL errors: ${JSON.stringify(data.errors)}`);
1470    throw new Error(`${callName}: GQL error: ${data.errors[0].message} (${data.errors.length})`);
1471  }
1472  let result = data.data?.result;
1473  if (result?.userErrors?.length) {
1474    console.error(`[Bubblehouse] ${callName} user errors:`, result.userErrors);
1475    throw new Error(`${callName}: GQL userErrors: ${result.userErrors}`);
1476  }
1477  return result;
1478}
1479
1480function navigateToCartOrReloadPage(cartUrl) {
1481  window.location.href = cartUrl ? cartUrl : "/cart";
1482}
1483
1484
1485/* xhrMonitor.js */
1486;(function() {
1487let origXHROpen = XMLHttpRequest.prototype.open;
1488let origFetch = window.fetch;
1489XMLHttpRequest.prototype.open = xhrOpenWrapperUnlikelyToBeRelatedToYourError;
1490window.fetch = fetchWrapperUnlikelyToBeRelatedToYourError;
1491if (Bubblehouse.verbose) console.log('[Bubblehouse] xhr monitoring enabled');
1492
1493function xhrOpenWrapperUnlikelyToBeRelatedToYourError(method, url) {
1494  if (this.bubblehouseListener)
1495    this.removeEventListener('loadend', this.bubblehouseListener);
1496  this.bubblehouseListener = handleXMLHttpRequestLoaded.bind(this, method.toUpperCase(), url)
1497  this.addEventListener('loadend', this.bubblehouseListener);
1498  origXHROpen.apply(this, arguments);
1499}
1500
1501function handleXMLHttpRequestLoaded(method, url) {
1502  // if (Bubblehouse.verbose) console.log(`[Bubblehouse] xhr finished via XMLHttpRequest: ${method} ${url}`);
1503  document.dispatchEvent(new CustomEvent('bubblehouse:xhrend', { detail: { method: method, url: url, ok: true } }));
1504}
1505
1506function fetchWrapperUnlikelyToBeRelatedToYourError(resource, options) {
1507  let method = 'GET';
1508  if (options && typeof options === 'object' && options.method && typeof options.method === 'string') {
1509    method = options.method.toUpperCase();
1510  }
1511  let promise = origFetch.apply(this, arguments);
1512  promise.then(fetchWrapperSucceeded.bind(null, method));
1513  return promise;
1514}
1515
1516function fetchWrapperSucceeded(method, response) {
1517  // if (Bubblehouse.verbose) console.log(`[Bubblehouse] xhr finished via fetch: ${method} ${response.url}`);
1518  document.dispatchEvent(new CustomEvent('bubblehouse:xhrend', { detail: { method: method, url: response.url, ok: response.ok } }));
1519}
1520})() || true;
1521
1522/* cartMonitor.js */
1523;(function() {
1524Bubblehouse.cartUpdaters = []
1525Bubblehouse.cartListeners = []
1526document.addEventListener('bubblehouse:xhrend', considerUpdatingCart);
1527document.addEventListener('bubblehouse:external-cart-update', handleExternalCartUpdate);
1528setTimeout(() => { handleCartUpdate('pageLoad', false) }, 1)
1529
1530function considerUpdatingCart(ev) {
1531  let { method, url } = ev.detail;
1532  let m
1533  if ((method === 'POST') && (m = /\/cart\/(add|update|change)/.exec(url))) {
1534    let isBubblehouseOrigin = /bubblehouse-cart-ignore/.test(url)
1535    if (Bubblehouse.verbose) console.log(`[Bubblehouse] cart update detected (from ${isBubblehouseOrigin ? 'bubblehouse' : 'outside'})`);
1536    handleCartUpdate(m[1], isBubblehouseOrigin).then(null, reportError)
1537  // } else if ((method === 'POST') && (m = /\/api\/([0-9-]+)\/graphql\.json/.exec(url))) {
1538  }
1539}
1540
1541function handleExternalCartUpdate() {
1542  handleCartUpdate('external_update', false).then(null, reportError)
1543}
1544
1545// document.dispatchEvent(new Event('bubblehouse_cart_update', { detail: { origin: url } }))
1546
1547async function handleCartUpdate(cartAction, isBubblehouseOrigin) {
1548  if (Bubblehouse.cartListeners.length === 0 && Bubblehouse.cartUpdaters.length === 0) return;
1549  let cartBody
1550  try {
1551    cartBody = await loadCart()
1552  } catch (error) {
1553    reportWarning(error, `cart fetch failed after ${cartAction}`)
1554    return
1555  }
1556  if (Bubblehouse.verbose) console.log(`[Bubblehouse] cart after ${cartAction}:`, cartBody);
1557  if (!isBubblehouseOrigin) {
1558    await waitAll(Bubblehouse.cartUpdaters.map((f) => f(cartBody, cartAction)))
1559  }
1560  await waitAll(Bubblehouse.cartListeners.map((f) => f(cartBody, cartAction)))
1561  //await rerenderIfModified(cartBody)
1562}
1563
1564function rerenderIfModified(originalCart) {
1565  return loadCart().then((newCart) => {
1566    if (JSON.stringify(originalCart.items) != JSON.stringify(newCart.items)) {
1567      if (Bubblehouse.verbose) console.log(`[Bubblehouse] cart modified after running cart updaters`);
1568      return rerenderCart();
1569    } else {
1570      if (Bubblehouse.verbose) console.log(`[Bubblehouse] cart unmodified after running cart updaters`);
1571    }
1572  })
1573}
1574})() || true;
1575
1576/* shopify/isOnCartPage.js */
1577Bubblehouse.isOnCartPage = isOnCartPage;
1578Bubblehouse.isCartVisible = isCartVisible;
1579
1580function isOnCartPage() {
1581  return /\/cart$/.test(location.pathname)
1582}
1583
1584function isCartVisible() {
1585  return isOnCartPage() || !!document.querySelector('#cart-drawer')
1586}
1587
1588
1589/* util/ignoreErr.js */
1590function ignoreErr(debugPrefix, f) {
1591  try {
1592    let result = f()
1593    if (result && result.then) {
1594      return result['catch'](function (error) {
1595        console.error(`[Bubblehouse] ${debugPrefix} failed:`, error)
1596      })
1597    } else {
1598      return result
1599    }
1600  } catch (error) {
1601    console.error(`[Bubblehouse] ${debugPrefix} failed:`, error)
1602  }
1603}
1604
1605
1606/* rerenderCart.js */
1607Bubblehouse.rerenderCart = rerenderCart;
1608
1609function rerenderCart(force) {
1610  if (!isOnCartPage() && !force) {
1611    return
1612  }
1613  location.reload();
1614  // return waitAll([
1615  //   ignoreErr('rerenderCart.shopify', () => {
1616  //     let el = document.querySelector('cart-notification');
1617  //     if (el && 'function' === typeof el.renderContents) {
1618  //       return fetch(Shopify.routes.root + 'cart?sections=cart-drawer,cart-icon-bubble', { cache: 'no-cache', credentials: 'same-origin' }).then(wantSuccessfulJson).then((body) => {
1619  //         el.renderContents({ sections: body })
1620  //       })
1621  //     }
1622  //   }),
1623  // ])
1624}
1625
1626
1627/* freeSample.js */
1628;(function() {
1629if (!Bubblehouse.data.freeSample) {
1630  return
1631}
1632let freeSampleVariantId = Bubblehouse.data.freeSample.variantId;
1633Bubblehouse.cartUpdaters.push(adjustGiftsInCart);
1634if (Bubblehouse.verbose) console.log(`[Bubblehouse] [free-sample] enabled with variant ID ${freeSampleVariantId}`)
1635
1636let minPurchaseAmountInCents = ~~(parseFloat(Bubblehouse.data.freeSample.minPurchaseAmount, 10) * 100 + 0.5)
1637
1638function adjustGiftsInCart(cart, cartAction) {
1639  let sampleItems = [], regularItems = []
1640  for (let item of cart.items) {
1641    if (item.variant_id === freeSampleVariantId) {
1642      sampleItems.push(item)
1643    } else {
1644      regularItems.push(item)
1645    }
1646  }
1647
1648  let isFreeSampleOwed = (regularItems.length > 0) && (cart.total_price >= minPurchaseAmountInCents);
1649
1650  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.`)
1651
1652  if (isFreeSampleOwed && sampleItems.length === 0) {
1653    if (cartAction === 'add' || (cartAction === 'update' && Bubblehouse.data.freeSample.autoAdd) || (cartAction === 'pageLoad' && isCartVisible())) {
1654      if (Bubblehouse.data.freeSample.autoAdd) {
1655        addGiftToCart().then(() => rerenderCart(false))
1656      } else {
1657        console.log(`[Bubblehouse] [free-sample] offering via popup`)
1658
1659        let popup
1660        popup = openPopupHere('free-sample', {}, {
1661          handlers: {
1662            'AddGiftToCart1': function (message) { doAddSampleToCart1(popup, message) },
1663          }
1664        })
1665      }
1666    } else {
1667      console.log(`[Bubblehouse] [free-sample] eligible, but not offered on ${cartAction} action`)
1668    }
1669  } else if (sampleItems.length > 0) {
1670    let firstItem = sampleItems.pop();
1671    let firstItemQuantity = (isFreeSampleOwed ? 1 : 0);
1672
1673    let updates = {};
1674    if (firstItem.quantity !== firstItemQuantity) {
1675      updates[firstItem.key] = firstItemQuantity;
1676    }
1677    for (let item of sampleItems) {
1678      updates[item.key] = 0;
1679    }
1680
1681    if (Object.keys(updates).length > 0) {
1682      if (Bubblehouse.verbose) console.log(`[Bubblehouse] [free-sample] removing from cart.`)
1683
1684      let u = updateCart(updates, { origin: 'free-sample' })
1685      console.log('u = ', u)
1686      return u.then(() => rerenderCart(false))
1687    } else {
1688      if (Bubblehouse.verbose) console.log(`[Bubblehouse] [free-sample] is up to date`)
1689    }
1690  }
1691}
1692
1693function doAddSampleToCart1(popup, message) {
1694  if (Bubblehouse.verbose) console.log(`[Bubblehouse] [free-sample] adding to cart`);
1695  addGiftToCart().then(() => {
1696    popup.close();
1697    return rerenderCart(true);
1698  })
1699}
1700
1701function addGiftToCart() {
1702  return addToCart([{ id: freeSampleVariantId, quantity: 1 }], { origin: 'free-sample' })
1703}
1704})() || true;
1705
1706/* util/observeElements.js */
1707function observeElements(selector, func) {
1708  let observer = new MutationObserver(function (mutations) {
1709    mutations.forEach(function (mutation) {
1710      for (let node of mutation.addedNodes) {
1711        if (node.nodeType === Node.ELEMENT_NODE && node.matches(selector)) {
1712          func(node)
1713        }
1714      }
1715    })
1716  })
1717  observer.observe(document, { subtree: true, childList: true })
1718
1719  for (let node of document.querySelectorAll(selector)) {
1720    func(node)
1721  }
1722}
1723
1724
1725/* shopify/productPage.js */
1726function isShopifyProductPage() {
1727  return (
1728    /\/products\/[^/]+/.test(window.location.pathname) ||
1729    window.ShopifyAnalytics?.meta?.page?.pageType === 'product' ||
1730    window.meta?.product != null
1731  )
1732}
1733
1734
1735/* inlinePanels.js */
1736;(function() {
1737observeElements('.bubblehouse-product-points-panel', populateProductPointsPanel)
1738
1739function populateProductPointsPanel(panel) {
1740  if (!panel) {
1741    return
1742  }
1743  if (!Bubblehouse.data.ppc) {
1744    return
1745  }
1746  let valueSpan = panel.querySelector('.bubblehouse-product-points-value')
1747  let amount = panel.dataset.priceCents
1748  if (Bubblehouse.verbose) console.log(`[Bubblehouse] Found inline panel, ppc = ${Bubblehouse.data.ppc}, amount = ${amount}`)
1749  if (valueSpan && amount) {
1750    let points = Math.floor(Bubblehouse.data.ppc * +amount)
1751    valueSpan.textContent = points.toFixed(0)
1752
1753    if (Bubblehouse.data.prelaunch) {
1754      return
1755    }
1756    panel.style.visibility = ''
1757    panel.style.display = ''
1758    updatePointsOnVariantChange(valueSpan)
1759  }
1760}
1761
1762async function updatePointsOnVariantChange(valueSpan) {
1763  if (!isShopifyProductPage()) {
1764    return
1765  }
1766
1767  let url = new URL(window.location.href)
1768  url.pathname += '.js'
1769
1770  let data
1771  try {
1772    let response = await fetch(url.toString())
1773    if (!response.ok) {
1774      console.error(`[Bubblehouse] error fetching product json`)
1775      return
1776    }
1777    data = await response.json()
1778  } catch {
1779    console.error(`[Bubblehouse] error fetching product json`)
1780    return
1781  }
1782
1783  if (!data || !Array.isArray(data.variants)) {
1784    console.error(`[Bubblehouse] product variants array is undefined`)
1785    return
1786  }
1787
1788  window.addEventListener('bubblehouse:variant-change', e => {
1789    let variant = data.variants.find(o => o.id == e.detail.variantId)
1790    if (!variant) {
1791      console.log(`[Bubblehouse] variant ${e.detail.variantId} not found`)
1792      return
1793    }
1794    let points = Math.floor(Bubblehouse.data.ppc * +variant.price)
1795    valueSpan.textContent = points.toFixed(0)
1796  })
1797}
1798})() || true;
1799
1800/* util/h.js */
1801function h(tagSpec, ...children) {
1802  let [tagName, ...classesAndIds] = tagSpec.match(/[#.]?[^#.]+/g)
1803  let element = document.createElement(tagName)
1804  for (let identifier of classesAndIds) {
1805    if (identifier.startsWith('#')) {
1806      element.id = identifier.slice(1)
1807      element.classList.add(identifier.slice(1))
1808    } else if (identifier.startsWith('.')) {
1809      element.classList.add(identifier.slice(1))
1810    } else {
1811      throw new Error(`invalid tag spec: "${tagSpec}" (invalid part is "${identifier}")`)
1812    }
1813  }
1814  while (children.length > 0 && children[0] && children[0].constructor === Object) {
1815    setAttrs(element, children.shift())
1816  }
1817  if (children.length === 1 && typeof children[0] === 'string') {
1818    element.textContent = children[0]
1819  } else {
1820    for (let child of children) {
1821      appendChild(element, child)
1822    }
1823  }
1824  return element;
1825}
1826
1827function setAttrs(element, attrs) {
1828  for (const [attr, value] of Object.entries(attrs)) {
1829    if (value == null || value === false) {
1830      continue
1831    } else if (attr === 'class') {
1832      adjustClasses(element, value)
1833    } else if (attr.startsWith('on')) {
1834      let event = attr.substr(2)
1835      element.addEventListener(event, value)
1836    } else if (value === true) {
1837      element.setAttribute(attr, '')
1838    } else if (value && value.constructor === Object) {
1839      if (attr === 'style') {
1840        Object.assign(element.style, value)
1841      } else {
1842        throw new TypeError(`Invalid object value for attribute "${attr}"`)
1843      }
1844    } else {
1845      element.setAttribute(attr, value.toString())
1846    }
1847  }
1848}
1849
1850// classes -> classItem | array of classes
1851// classItem -> falsy | classSpecString | {classSpecString: booleanish}
1852// classSpecString -> whitespace-separated list of classes
1853function adjustClasses(element, classes) {
1854  if (!classes) return;
1855  if (typeof classes === 'string') {
1856    element.classList.add(...classes.trim().split(/\s+/))
1857  } else if (Array.isArray(classes)) {
1858    for (let item of classes) {
1859      adjustClasses(element, item)
1860    }
1861  } else if (classes && classes.constructor === Object) {
1862    for (let [name, included] of Object.entries(classes)) {
1863      element.classList.toggle(name, !!included)
1864    }
1865  } else {
1866    throw new TypeError('Classes must be strings, objects, or false/null/undefined');
1867  }
1868}
1869
1870function appendChild(element, child) {
1871  if (child == null || child === false) return;
1872  if (child instanceof Node || typeof child === 'string') {
1873    element.append(child)
1874  } else if (typeof child === 'number') {
1875    element.append(child.toString())
1876  } else if (Array.isArray(child)) {
1877    for (let nestedChild of child) {
1878      appendChild(element, nestedChild)
1879    }
1880  } else {
1881    throw new TypeError('Children must be elements, strings, numbers, arrays, or false/null/undefined');
1882  }
1883  return child
1884}
1885
1886function assignElementRefs(host, root) {
1887  for (let child of root.children) {
1888    if (child.id) {
1889      host[child.id] = child
1890    }
1891    assignElementRefs(host, child)
1892  }
1893  return root
1894}
1895
1896
1897/* util/removeArrayElement.js */
1898function removeArrayElement(array, element) {
1899  let index = array.indexOf(element)
1900  if (index >= 0) {
1901    array.splice(index, 1)
1902  }
1903}
1904
1905
1906/* util/parseStringTemplate.js */
1907function parseStringTemplate(str, fs, fk, prefix = '{{.', suffix = '}}') {
1908  // Given "This is {{.foo}} sample {{.bar}} value", calls
1909  // - fs('This is ')
1910  // - fk('foo')
1911  // - fs(' sample ')
1912  // - fk('bar')
1913  // - fs(' value.')
1914  let remaining = str;
1915  let startIndex = 0,
1916    endIndex = 0;
1917  while (
1918    (startIndex = remaining.indexOf(prefix)) >= 0 &&
1919    (endIndex = remaining.indexOf(suffix, startIndex + prefix.length)) >= 0
1920  ) {
1921    if (startIndex > 0) {
1922      fs(remaining.substring(0, startIndex));
1923    }
1924    fk(remaining.substring(startIndex + prefix.length, endIndex));
1925    remaining = remaining.substring(endIndex + suffix.length);
1926  }
1927  if (remaining) {
1928    fs(remaining);
1929  }
1930}
1931
1932function mapStringTemplate(str, fs, fk) {
1933  let result = [];
1934  parseStringTemplate(
1935    str,
1936    (s) => {
1937      result.push(fs(s, result.length));
1938    },
1939    (s) => {
1940      result.push(fk(s, result.length));
1941    },
1942  );
1943  return result;
1944}
1945
1946function parseTags(str, { validTags }) {
1947  // Given "This is <foo>sample</foo> value", returns
1948  // - ['This is ', {tag: 'foo', body: ['sample']}, ' value']
1949  // Does not support nested tags so far.
1950
1951  var result = [];
1952  var body = result;
1953  var element = null;
1954  parseStringTemplate(str, (str) => {
1955    body.push(str);
1956  }, (rawStr) => {
1957    let innerStr = rawStr.trim();
1958    if (innerStr === '') {
1959      body.push('(!EMPTY_TAG!)');
1960      return;
1961    }
1962
1963    let isClosing = innerStr.startsWith('/');
1964    let tag = isClosing ? innerStr.substring(1).trim() : innerStr;
1965
1966    if (!validTags.includes(tag)) {
1967      body.push('<' + rawStr + '>');
1968    } else if (element != null) {
1969      if (!isClosing) {
1970        body.push('(!NESTED_TAG!)');
1971      } else if (tag === element.tag) {
1972        body = result;
1973        element = null;
1974      } else {
1975        body.push('(!WRONG_CLOSING_TAG!)');
1976      }
1977    } else if (isClosing) {
1978      body.push('(!UNOPENED_TAG!)');
1979    } else {
1980      element = { tag: tag, body: [] };
1981      body.push(element);
1982      body = element.body;
1983    }
1984  }, '<', '>');
1985  return result;
1986}
1987
1988
1989/* util/localize.js */
1990function localize(strings, key, vals = {}) {
1991  if (!strings.hasOwnProperty(key)) {
1992    throw new Error(`missing string "${key}"`);
1993  }
1994  let str = strings[key];
1995  for (let k in vals)
1996    if (vals.hasOwnProperty(k)) {
1997      str = str.replaceAll("{{." + k + "}}", "" + vals[k]);
1998    }
1999  return str;
2000}
2001
2002
2003/* vm/RedeemVM.js */
2004class RedeemVM {
2005  constructor(
2006    data,
2007    cartCost,
2008    itemCount,
2009    cartCouponCodes,
2010    desiredRedemptionAmountOrPoints,
2011    locale,
2012    overrides = {},
2013    options = { subtractProvisionalAmount: false},
2014  ) {
2015    data || (data = {});
2016    let isFreeCart =
2017      data.enableWhenFree &&
2018      cartCost != null &&
2019      itemCount != null &&
2020      cartCost == 0 &&
2021      itemCount > 0;
2022    this.data = data;
2023    this.cartCost = cartCost;
2024    this.locale = locale;
2025    this.isLoaded = data && cartCost != null; // && (cartCost > 0 || isFreeCart);
2026    this.offers = (data && data.offers) || [];
2027    this.enablePointRedemption = data && data.enablePointRedemption;
2028    if (options.verbose !== false) {
2029      console.log(`[Bubblehouse] data =`, data);
2030      console.log(
2031        `[Bubblehouse] RedeemVM: cartCost = ${cartCost}, itemCount = ${itemCount}, isLoaded = ${this.isLoaded}, isFreeCart = ${isFreeCart}`,
2032      );
2033    }
2034    if (!this.isLoaded || !data.styles) {
2035      return;
2036    }
2037
2038    this.values = data.styles.v || {};
2039    this.classes = data.styles.c || {};
2040    this.stylesheetUrl = data.styles.css;
2041    this.col1 =
2042      overrides.col1 || this.resolveValue(".widget.presentation.col1.derived");
2043    this.col2 =
2044      overrides.col2 || this.resolveValue(".widget.presentation.col2.derived");
2045    if (!["amount", "points"].includes(this.col1)) {
2046      throw new Error(
2047        `<bubblehouse-redeem-panel>: internal error: col1 = ${this.col1}`,
2048      );
2049    }
2050    if (
2051      !["amount", "points", "none"].includes(this.col2) ||
2052      this.col2 === this.col1
2053    ) {
2054      throw new Error(
2055        `<bubblehouse-redeem-panel>: internal error: col2 = ${this.col2}`,
2056      );
2057    }
2058
2059    this.spendablePts = data.ptsSpendable;
2060    this.spendableAmount = this.convertPointsToAmount(this.spendablePts);
2061
2062    let maxRedeemPts = data.redeemPtsMax;
2063    if (cartCost != null) {
2064      let maxOrderAmount = cartCost;
2065      if (options.subtractProvisionalAmount && data.provisionalAmount != null) {
2066        maxOrderAmount = Math.max(0, cartCost - data.provisionalAmount);
2067        // if (data.provisionalCouponCode == null || cartCouponCodes == null || !cartCouponCodes.includes(data.provisionalCouponCode)) {
2068        // }
2069      }
2070      let maxOrderPts = Math.ceil(this.convertAmountToPoints(maxOrderAmount));
2071      maxRedeemPts = Math.min(maxRedeemPts, maxOrderPts);
2072    }
2073    let defaultRedeemPts =
2074      Number.isFinite(data.redeemPtsDefault) ? data.redeemPtsDefault : maxRedeemPts;
2075    defaultRedeemPts = Math.min(Math.max(0, defaultRedeemPts), maxRedeemPts);
2076    if (defaultRedeemPts != 0 && defaultRedeemPts < data.redeemPtsMin) {
2077      defaultRedeemPts = 0;
2078    }
2079
2080    if (
2081      desiredRedemptionAmountOrPoints == null ||
2082      !isNaN(desiredRedemptionAmountOrPoints)
2083    ) {
2084      if (this.col1 === "amount") {
2085        if (desiredRedemptionAmountOrPoints == null) {
2086          this.effectiveRedemptionPts = defaultRedeemPts;
2087        } else {
2088          this.effectiveRedemptionPts = Math.ceil(
2089            this.convertAmountToPoints(desiredRedemptionAmountOrPoints),
2090          );
2091        }
2092        this.effectiveRedemptionAmount = this.convertPointsToAmount(
2093          this.effectiveRedemptionPts,
2094        );
2095      } else if (this.col1 === "points") {
2096        this.effectiveRedemptionPts =
2097          desiredRedemptionAmountOrPoints == null
2098            ? defaultRedeemPts
2099            : desiredRedemptionAmountOrPoints;
2100        this.effectiveRedemptionAmount = this.convertPointsToAmount(
2101          this.effectiveRedemptionPts,
2102        );
2103      }
2104    } else {
2105      this.effectiveRedemptionPts = null;
2106      this.effectiveRedemptionAmount = null;
2107    }
2108
2109    this.amountError = null;
2110    let applyStatusGates =
2111      options.applyStatusGates === true ||
2112      options.applyCheckoutStatusGates === true;
2113    let applyDiscountCodeGate =
2114      options.applyDiscountCodeGate === true ||
2115      options.applyCheckoutStatusGates === true;
2116    this.redeemDisabledMessage =
2117      applyStatusGates && data.redeemDisabledMessage != null
2118        ? data.redeemDisabledMessage
2119        : "";
2120    this.isRedemptionBlockedByStatusGate =
2121      applyStatusGates &&
2122      (
2123        this.redeemDisabledMessage !== "" ||
2124        data.enabled === false ||
2125        data.enablePointRedemption === false
2126      );
2127    this.isRedemptionBlockedByDiscountCodeGate =
2128      applyDiscountCodeGate &&
2129      data.disableIfDiscountApplied === true &&
2130      (cartCouponCodes || []).length > 0;
2131    this.isRedemptionBlockedByCheckoutState =
2132      this.isRedemptionBlockedByStatusGate ||
2133      this.isRedemptionBlockedByDiscountCodeGate;
2134    if (this.effectiveRedemptionPts != null) {
2135      if (this.effectiveRedemptionPts < 0) {
2136        this.amountError = this.localize(".error.negative.string");
2137      } else if (
2138        this.effectiveRedemptionPts != 0 &&
2139        this.effectiveRedemptionPts < data.redeemPtsMin
2140      ) {
2141        let valueStr;
2142        if (this.col1 === "amount") {
2143          valueStr = this.formatBareCurrency(
2144            this.convertPointsToAmount(data.redeemPtsMin),
2145          );
2146        } else if (this.col1 === "points") {
2147          valueStr = this.formatPoints(data.redeemPtsMin);
2148        }
2149        this.amountError = this.localize(".error.below-minimum.string", {
2150          value: valueStr,
2151        });
2152      } else if (
2153        this.effectiveRedemptionPts != 0 &&
2154        this.effectiveRedemptionPts > maxRedeemPts
2155      ) {
2156        let pts;
2157        let valueStr;
2158
2159        if (maxRedeemPts === 0 && data.redeemPtsPeriodLimit > 0) {
2160          pts = data.redeemPtsPeriodLimit
2161        } else {
2162          pts = maxRedeemPts
2163        }
2164
2165        if (this.col1 === "amount") {
2166          valueStr = this.formatBareCurrency(
2167            this.convertPointsToAmount(pts),
2168          );
2169        } else if (this.col1 === "points") {
2170          valueStr = this.formatPoints(pts);
2171        }
2172        this.amountError = this.localize(".error.above-balance.string", {
2173          value: valueStr,
2174        });
2175      }
2176
2177      this.visibleRedemptionPts = null;
2178      if (this.effectiveRedemptionPts != null) {
2179        this.visibleRedemptionPts = this.effectiveRedemptionPts;
2180        if (this.resolveValue('.redeem-points.presentation.enum') === 'negative') {
2181          this.visibleRedemptionPts = -this.visibleRedemptionPts;
2182        }
2183      }
2184    }
2185
2186    this.isRedeemEnabled =
2187      this.effectiveRedemptionAmount != null &&
2188      (this.effectiveRedemptionAmount != 0 || isFreeCart) &&
2189      this.amountError == null &&
2190      !this.isRedemptionBlockedByCheckoutState;
2191    if (this.effectiveRedemptionAmount != null) {
2192      if (this.effectiveRedemptionAmount == 0 && isFreeCart) {
2193        this.redeemLabel = this.localize(".redeem-button.free.string");
2194      } else {
2195        this.redeemLabel = this.localize(".redeem-button.active.string", {
2196          amount: this.formatCurrency(this.effectiveRedemptionAmount),
2197          pts: this.formatPoints(this.effectiveRedemptionPts),
2198        });
2199      }
2200    } else {
2201      this.redeemLabel = this.localize(".redeem-button.disabled.string");
2202    }
2203
2204    // if (shopDetails.isVerbose) {
2205    //   console.log(`[Bubblehouse] spendableAmount = ${spendableAmount}, cartCost = ${cartCost}, defaultRedemptionAmount = ${defaultRedemptionAmount}, unroundedEffectiveRedemptionAmount = ${unroundedEffectiveRedemptionAmount}, effectiveRedemptionPts = ${effectiveRedemptionPts}, effectiveRedemptionAmount = ${effectiveRedemptionAmount}`)
2206    // }
2207  }
2208
2209  minimalCurrencyStep() {
2210    return Math.pow(10, -this.data.shopCurrencyPrecision);
2211  }
2212  formatCurrency(amount) {
2213    return (
2214      this.data.shopCurrencyPrefix +
2215      this.formatBareCurrency(amount) +
2216      this.data.shopCurrencySuffix
2217    );
2218  }
2219  formatPoints(pts) {
2220    if (pts < 0) {
2221      return '–' + this.formatPoints(-pts)
2222    }
2223    return new Intl.NumberFormat(this.locale).format(pts);
2224  }
2225  formatBareCurrency(amount) {
2226    let n = this.data.shopCurrencyPrecision;
2227    return new Intl.NumberFormat(this.locale, {
2228      minimumFractionDigits: n,
2229      maximumFractionDigits: n,
2230    }).format(amount);
2231  }
2232  formatCurrencyInputValue(amount) {
2233    return amount.toFixed(this.data.shopCurrencyPrecision);
2234  }
2235  convertPointsToAmount(pts) {
2236    return Math.round(pts * this.data.ptValue * 10000) / 10000;
2237  }
2238  convertAmountToPoints(amount) {
2239    return Math.round((amount / this.data.ptValue) * 10000) / 10000;
2240  }
2241
2242  resolveValue(key) {
2243    let str = this.values[key];
2244    if (typeof str === "undefined") {
2245      throw new Error(`missing value ${key}`);
2246    }
2247    return str;
2248  }
2249
2250  resolvePixelValue(key) {
2251    let value = this.resolveValue(key);
2252    if (value == null) {
2253      return null;
2254    }
2255    let m = value.match(/^(\d+)px$/);
2256    if (m) {
2257      return parseInt(m[1], 10);
2258    }
2259    return null;
2260  }
2261
2262  customEmbed(key, stringFunc, childrenFuncs) {
2263    let string = this.resolveValue(key)
2264    if (string === '') {
2265      return;
2266    }
2267    parseStringTemplate(string, stringFunc, (fieldKey, i) => {
2268      let child = childrenFuncs[fieldKey];
2269      if (child) {
2270        child();
2271      } else {
2272        stringFunc(`{{!.${fieldKey}!}}`);
2273      }
2274    });
2275  }
2276
2277  localize(key, vals) {
2278    return localize(this.values, key, vals);
2279  }
2280}
2281
2282
2283/* conditions.js */
2284function isConditionAttributeValueMatched(conditions) {
2285    if (!conditions) {
2286        return true
2287    }
2288    return conditions.split(/\s+/).every(isConditionMatched)
2289}
2290
2291function isConditionMatched(cond) {
2292    if (cond && cond[0] == '!') {
2293        return !isConditionMatched(cond.substr(1))
2294    }
2295    switch (cond) {
2296        case '': return true
2297        case 'never': return false
2298        case 'tester': return !!Bubblehouse.data.tester
2299        case 'logged_in': return !!Bubblehouse.data.loggedIn
2300        case 'widget1': return !!Bubblehouse.data.widget1
2301        case 'widget2': return !!Bubblehouse.data.widget2
2302        case 'widget3': return !!Bubblehouse.data.widget3
2303        default: console.error(`[Bubblehouse] invalid condition: if="... ${cond} ..."`); return false
2304    }
2305}
2306
2307function adjustPanelVisibility(element) {
2308    let conditions = element.getAttribute('if')
2309    if (!isConditionAttributeValueMatched(conditions)) {
2310        console.log(`[Bubblehouse] hiding <${element.tagName}> because conditions do not match (${conditions})`)
2311        if (element.style.display !== 'none') {
2312            element.style.display = 'none'
2313        }
2314        return false
2315    }
2316    if (element.style.display === 'none') {
2317        element.style.display = element.getAttribute('display') || ''
2318    }
2319    if (element.style.visibility === 'hidden') {
2320        element.style.visibility = 'visible'
2321    }
2322    if (Bubblehouse.verbose) console.log(`[Bubblehouse] showing <${element.tagName}>`)
2323    return true
2324}
2325
2326
2327/* call.js */
2328Bubblehouse.call = call;
2329
2330function call(funcName, input) {
2331  let headers = {
2332    'Content-Type': 'application/json',
2333    'Accept': 'application/json',
2334  }
2335  if (Bubblehouse.data.authToken) {
2336    headers['Authorization'] = `Bearer ${Bubblehouse.data.authToken}`
2337  }
2338  return fetch(Bubblehouse.data.callUrl.replace('FUNCNAME', funcName), {
2339    method: 'POST',
2340    body: JSON.stringify(input || {}),
2341    headers: headers,
2342  }).then(wantSuccessfulJson)    
2343}
2344
2345
2346/* tags/bubblehouse-redeem-panel.js */
2347;(function() {
2348const stylesheet = `
2349:root { display: flex; padding: 4px }
2350.hidden { display: none !important }
2351
2352.inputItem { margin-left: 4px; margin-right: 4px }
2353.inputItem:first-child { margin-left: 0 }
2354.inputItem:last-child { margin-right: 0 }
2355
2356input[type=number].nostepper {-moz-appearance: textfield }
2357input[type=number].nostepper::-webkit-inner-spin-button,
2358input[type=number].nostepper::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0 }
2359`;
2360const UTILITY_CLASSES = { hidden: true };
2361
2362const experimentalInplaceFlow = true;
2363
2364class BubblehouseRedeemPanel extends HTMLElement {
2365  constructor() {
2366    super();
2367
2368    this._onCartChanged = this._onCartChanged.bind(this);
2369    this._onCartLoaded = this._onCartLoaded.bind(this);
2370    this._onCartLoadingFailed = this._onCartLoadingFailed.bind(this);
2371    this._onWidgetDataLoaded = this._onWidgetDataLoaded.bind(this);
2372    this._onWidgetDataLoadingFailed =
2373      this._onWidgetDataLoadingFailed.bind(this);
2374    this._onRedemptionAmountChange = this._onRedemptionAmountChange.bind(this);
2375    this._onRedemptionPointsChange = this._onRedemptionPointsChange.bind(this);
2376    this.redeem = this.redeem.bind(this);
2377    this.initialized = false;
2378
2379    this.redemptionPending = false;
2380    this.redemptionError = null;
2381    this.cartData = null;
2382    this.redemptionData = null;
2383    this.desiredRedemptionAmountOrPoints = null;
2384    this.stylesheetReady = false;
2385    this.cartGeneration = 0;
2386    this.statusGeneration = 0;
2387
2388    this.attachShadow({ mode: "open" });
2389  }
2390
2391  connectedCallback() {
2392    if (!adjustPanelVisibility(this)) return;
2393    Bubblehouse.cartListeners.push(this._onCartChanged);
2394    this.reload("initial");
2395    this.render();
2396  }
2397
2398  disconnectedCallback() {
2399    removeArrayElement(Bubblehouse.cartListeners, this._onCartChanged);
2400  }
2401
2402  cartCouponCodes() {
2403    if (!this.cartData) {
2404      return null;
2405    }
2406    return (this.cartData.cart_level_discount_applications || [])
2407      .filter((da) => da.type === "discount_code")
2408      .map((da) => da.title)
2409  }
2410
2411  _collectCheckoutContext() {
2412    let context = {
2413      site: "cart",
2414      country: Shopify.country,
2415      currency: Shopify.currency && Shopify.currency.active,
2416      locale: (Shopify.locale || "").trim(),
2417      timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
2418
2419      discountCodesKnown: true,
2420      discountCodes: this.cartCouponCodes(),
2421    };
2422    if (this.cartData) {
2423      context.cartSubtotal = (this.cartData.total_price / 100).toFixed(6);
2424    }
2425    return context;
2426  }
2427
2428  reload(why) {
2429    this.reloadCart();
2430    this.reloadStatus(why);
2431  }
2432
2433  reloadCart() {
2434    let generation = ++this.cartGeneration;
2435    loadCart().then(
2436      (cartData) => this._onCartLoaded(cartData, generation),
2437      (error) => this._onCartLoadingFailed(error, generation),
2438    );
2439  }
2440
2441  reloadStatus(why) {
2442    if (!this.cartData) {
2443      return;
2444    }
2445    let generation = ++this.statusGeneration;
2446    call("CheckoutStatus1", {
2447      ...this._collectCheckoutContext(),
2448    }).then(
2449      (redemptionData) => this._onWidgetDataLoaded(redemptionData, generation),
2450      (error) => this._onWidgetDataLoadingFailed(error, generation),
2451    );
2452  }
2453
2454  render(origin) {
2455    // if (Bubblehouse.verbose) console.log(`<${this.tagName}> rendering:\n\ncartData = ${JSON.stringify(this.cartData, null, 2)}\n\nredemptionData = ${JSON.stringify(this.redemptionData, null, 2)}`);
2456
2457    let vm = (this.vm = new RedeemVM(
2458      this.redemptionData,
2459      this.cartData ? this.cartData.total_price / 100 : null,
2460      this.cartData ? this.cartData.item_count : null,
2461      this.cartCouponCodes(),
2462      this.desiredRedemptionAmountOrPoints,
2463      undefined,
2464      {},
2465      { subtractProvisionalAmount: false, applyStatusGates: true }
2466    ));
2467
2468    let isContentVisible = vm.isLoaded && !vm.data?.hide;
2469    let isStatusVisible = Boolean(vm.data?.hide && vm.data?.hideMessage);
2470    let isRootVisible = isContentVisible || isStatusVisible;
2471    let isDisabled = isStatusVisible || vm.isRedemptionBlockedByCheckoutState;
2472
2473    if (!this.initialized && vm.stylesheetUrl) {
2474      this.initialized = true;
2475      if (Bubblehouse.verbose) {
2476        console.log(
2477          `[Bubblehouse] <${this.tagName}> initialized with vm: `,
2478          vm,
2479        );
2480      }
2481
2482      let panelCells = [];
2483      if (vm.col1 === "amount") {
2484        panelCells.push(h("div#availableAmount"));
2485      } else if (vm.col1 === "points") {
2486        panelCells.push(h("div#availablePoints"));
2487      }
2488      if (vm.col2 === "points") {
2489        panelCells.push(h("div#availablePoints"));
2490      } else if (vm.col2 === "amount") {
2491        panelCells.push(h("div#availableAmount"));
2492      }
2493      if (vm.col1 === "amount") {
2494        panelCells.push(h("div#redemptionAmountCell"));
2495      } else if (vm.col1 === "points") {
2496        panelCells.push(h("div#redemptionPtsCell"));
2497      }
2498      if (vm.col2 === "points") {
2499        panelCells.push(h("div#redemptionPtsCell"));
2500      } else if (vm.col2 === "amount") {
2501        panelCells.push(h("div#redemptionAmountCell"));
2502      }
2503
2504      let logo = null;
2505      let logoUrl = vm.values[".logo.image"];
2506      if (logoUrl) {
2507        logo = h("img#logoImg", { src: logoUrl, alt: "" });
2508      }
2509
2510      let stylesheetEl = h("link", { rel: "stylesheet", href: vm.stylesheetUrl });
2511      stylesheetEl.addEventListener("load", () => {
2512        this.stylesheetReady = true;
2513        this.render("stylesheet_loaded");
2514      });
2515      this.shadowRoot.append(
2516        h("style", stylesheet),
2517        stylesheetEl,
2518        h(
2519          "div#rootPanel",
2520          { style: { display: "none" } },
2521          logo,
2522          h("div#amountsPanel", panelCells),
2523          h(`button#redeemButton`, {
2524            type: "button",
2525            onclick: this.redeem,
2526          }),
2527          h("div#statusPanel", h("span#statusMessageSpan")),
2528        ),
2529      );
2530      assignElementRefs(this, this.shadowRoot);
2531      this.setRootPanelVisible(false);
2532
2533      this.applyClass(this.rootPanel, "widget");
2534      this.applyClass(this.logoImg, "logo");
2535      this.applyClass(this.statusPanel, "message-panel");
2536      this.applyClass(this.amountsPanel, "amounts-panel");
2537      this.applyClass(this.redeemButton, "redeem-button");
2538      this.applyClass(this.availableAmount, "available-amount");
2539      this.applyClass(this.availablePoints, "available-points");
2540      this.applyClass(this.redemptionAmountCell, "redeem-amount");
2541      this.applyClass(this.redemptionPtsCell, "redeem-points");
2542
2543      this.embedChildren(
2544        this.availableAmount,
2545        ".available-amount.overall.string",
2546        {
2547          value: () => h("span#availableAmountValue"),
2548        },
2549      );
2550      this.applyClass(this.availableAmountValue, "available-amount-value");
2551
2552      this.embedChildren(
2553        this.availablePoints,
2554        ".available-points.overall.string",
2555        {
2556          value: () => h("span#availablePointsValue"),
2557        },
2558      );
2559      this.applyClass(this.availablePointsValue, "available-points-value");
2560
2561      if (vm.col1 === "amount") {
2562        this.embedChildren(
2563          this.redemptionAmountCell,
2564          ".redeem-amount.overall.string",
2565          {
2566            value: () =>
2567              h(
2568                "span#redemptionAmountValue.inputItem",
2569                {},
2570                h(
2571                  "span#redemptionAmountPrefixSpan",
2572                  vm.data.shopCurrencyPrefix,
2573                ),
2574                h("input#redemptionInput.nostepper", {
2575                  type: "number",
2576                  min: "0",
2577                  step: "" + vm.minimalCurrencyStep(),
2578                  placeholder: vm.formatBareCurrency(0),
2579                  onchange: this._onRedemptionAmountChange,
2580                  oninput: this._onRedemptionAmountChange,
2581                }),
2582                h(
2583                  "span#redemptionAmountSuffixSpan",
2584                  vm.data.shopCurrencySuffix,
2585                ),
2586              ),
2587          },
2588        );
2589        this.embedChildren(
2590          this.redemptionPtsCell,
2591          ".redeem-points.overall.string",
2592          {
2593            value: () => h("span#redemptionPtsValueSpan"),
2594          },
2595        );
2596        this.detailRedemptionCell = this.redemptionPtsCell;
2597      } else if (vm.col1 === "points") {
2598        this.embedChildren(
2599          this.redemptionPtsCell,
2600          ".redeem-points.overall.string",
2601          {
2602            value: () =>
2603              h(
2604                "span#redemptionPtsValueSpan.inputItem",
2605                h("input#redemptionInput.nostepper", {
2606                  type: "number",
2607                  min: "0",
2608                  onchange: this._onRedemptionPointsChange,
2609                  oninput: this._onRedemptionPointsChange,
2610                }),
2611              ),
2612          },
2613        );
2614        this.embedChildren(
2615          this.redemptionAmountCell,
2616          ".redeem-amount.overall.string",
2617          {
2618            value: () => h("span#redemptionAmountValue"),
2619          },
2620        );
2621        this.detailRedemptionCell = this.redemptionAmountCell;
2622      }
2623      this.applyClass(this.redemptionInput, "input-field");
2624      this.applyClass(this.redemptionAmountValue, "redeem-amount-value");
2625      this.applyClass(this.redemptionPtsValueSpan, "redeem-points-value");
2626    }
2627    if (!this.initialized) {
2628      return;
2629    }
2630
2631    this.applyClass(this.logoImg, {
2632      "logo--enabled": !isDisabled,
2633      "logo--disabled": isDisabled,
2634    });
2635    // this.applyClass(this.logoImg, , isDisabled);
2636    this.applyClass(this.rootPanel, {
2637      "widget--enabled": !isDisabled,
2638      "widget--disabled": isDisabled,
2639    });
2640    this.applyClass(this.statusPanel, {
2641      "message-panel--enabled": !isDisabled,
2642      "message-panel--disabled": isDisabled,
2643    });
2644    this.applyClass(this.amountsPanel, { hidden: !isContentVisible });
2645    this.applyClass(this.redeemButton, { hidden: !isContentVisible });
2646
2647    console.log(
2648      `[Bubblehouse] <${this.tagName}> render: isContentVisible = ${isContentVisible}, vm.data?.hide = ${vm.data?.hide}, this.amountsPanel.className = ${this.amountsPanel.className}, this.redeemButton.className = ${this.redeemButton.className}`,
2649    );
2650
2651    if (vm.data?.hide) {
2652      this._renderStatusMessage(vm.data.hideMessage, vm.data.actions);
2653      this.setRootPanelVisible(isRootVisible);
2654      return;
2655    } else if (!vm.isLoaded) {
2656      this._renderStatusMessage("");
2657      this.setRootPanelVisible(isRootVisible);
2658      return;
2659    }
2660
2661    if (this.availableAmountValue) {
2662      this.availableAmountValue.textContent = vm.formatCurrency(
2663        vm.spendableAmount,
2664      );
2665    }
2666    if (this.availablePointsValue) {
2667      this.availablePointsValue.textContent = vm.formatPoints(vm.spendablePts);
2668    }
2669    if (this.detailRedemptionCell) {
2670      this.detailRedemptionCell.classList.toggle(
2671        "hidden",
2672        vm.visibleRedemptionPts == null,
2673      );
2674    }
2675    if (this.redemptionPtsValueSpan && vm.col2 === "points") {
2676      this.redemptionPtsValueSpan.textContent =
2677        vm.visibleRedemptionPts != null
2678          ? vm.formatPoints(vm.visibleRedemptionPts)
2679          : "";
2680    }
2681    if (this.redemptionAmountValue && vm.col2 === "amount") {
2682      this.redemptionAmountValue.textContent =
2683        vm.effectiveRedemptionAmount != null
2684          ? vm.formatCurrency(vm.effectiveRedemptionAmount)
2685          : "";
2686    }
2687    if (this.shadowRoot.activeElement !== this.redemptionInput) {
2688      if (vm.col1 === "amount" && vm.effectiveRedemptionAmount != null) {
2689        this.redemptionInput.value = vm.formatBareCurrency(
2690          vm.effectiveRedemptionAmount,
2691        );
2692      } else if (vm.col1 === "points" && vm.effectiveRedemptionPts != null) {
2693        this.redemptionInput.value = "" + vm.effectiveRedemptionPts;
2694      }
2695    }
2696    this.redeemButton.textContent = (this.redemptionPending ? vm.localize(".message.redeeming.string") : vm.redeemLabel);
2697    this.redeemButton.disabled = this.redemptionPending || !vm.isRedeemEnabled;
2698
2699    this.redemptionInput.setCustomValidity(vm.amountError || "");
2700
2701    if (this.redemptionPending) {
2702      this._renderStatusMessage("");
2703    } else if (vm.redeemDisabledMessage) {
2704      this._renderStatusMessage(vm.redeemDisabledMessage);
2705    } else if (this.redemptionError) {
2706      this._renderStatusMessage(this.redemptionError);
2707    } else {
2708      this._renderStatusMessage("");
2709    }
2710    this.setRootPanelVisible(isRootVisible);
2711  }
2712
2713  redeem() {
2714    if (this.redemptionPending) {
2715      return;
2716    }
2717    this.redemptionPending = true;
2718    this.redemptionError = null;
2719    this.render();
2720
2721    this._redeemAsync()
2722      .then(() => {
2723        if (this.redemptionData.useAdvancedIntegration) {
2724          this.redemptionPending = false;
2725        } else {
2726          // don't mark as done -- we're navigating away
2727        }
2728        this.redemptionError = null;
2729        this.render();
2730      })
2731      .catch((error) => {
2732        reportError(error, "redeem");
2733        this.redemptionPending = false;
2734        this.redemptionError = error.toString();
2735        this.render();
2736      });
2737  }
2738
2739  applyClass(el, ...args) {
2740    if (!el) {
2741      return;
2742    }
2743    for (let arg of args) {
2744      if (typeof arg === 'string') {
2745        this._toggleSingleClass(el, arg, true);
2746      } else if (typeof arg === 'object') {
2747        for (let [shortClassName, isOn] of Object.entries(arg)) {
2748          this._toggleSingleClass(el, shortClassName, isOn);
2749        }
2750      }
2751    }
2752  }
2753
2754  setRootPanelVisible(isVisible) {
2755    this.rootPanel.classList.toggle("hidden", !isVisible);
2756    if (isVisible) {
2757      if (this.stylesheetReady) {
2758        this.rootPanel.style.removeProperty("display");
2759      } else {
2760        this.rootPanel.style.setProperty("display", "none");
2761      }
2762    } else {
2763      this.rootPanel.style.setProperty("display", "none", "important");
2764    }
2765  }
2766
2767  _toggleSingleClass(el, shortClassName, isOn) {
2768    if (UTILITY_CLASSES[shortClassName]) {
2769      el.classList.toggle(shortClassName, isOn);
2770    } else if (this.vm.data) {
2771      let className = this.vm.classes[shortClassName];
2772      if (!className) {
2773        throw new Error(`missing theme class ${shortClassName}`);
2774      }
2775      el.classList.toggle(className, isOn);
2776    }
2777  }
2778
2779  embedChildren(el, stringKey, childrenKeys) {
2780    if (!el) {
2781      return;
2782    }
2783    parseStringTemplate(
2784      this.vm.resolveValue(stringKey),
2785      (staticStr) => {
2786        el.append(h("span", {}, staticStr));
2787      },
2788      (fieldKey) => {
2789        let child = childrenKeys[fieldKey];
2790        if (typeof child === "undefined") {
2791          el.append(`{{!.${fieldKey}!}}`);
2792        } else {
2793          if (typeof child === "function") {
2794            child = child();
2795          }
2796          appendChild(el, child);
2797        }
2798      },
2799    );
2800    assignElementRefs(this, el);
2801  }
2802
2803  async _redeemAsync() {
2804    if (this.vm.effectiveRedemptionAmount == null) {
2805      throw new Error("Invalid amount");
2806    }
2807    if (this.vm.effectiveRedemptionAmount == 0) {
2808      if (!this.redemptionData.useAdvancedIntegration) {
2809        window.location.href = `/checkout`;
2810      }
2811      return;
2812    }
2813    let input = {
2814      amount: `${this.vm.effectiveRedemptionAmount.toFixed(6)}`,
2815      canRemove: true,
2816      ...this._collectCheckoutContext(),
2817    };
2818    let output = await call("RedeemCheckoutReward1", input);
2819    if (Bubblehouse.verbose)
2820      console.log(
2821        `[Bubblehouse] <${this.tagName}> redeem input = ${JSON.stringify(input)}, output = ${JSON.stringify(output)}`,
2822      );
2823
2824    if (output.error) {
2825      throw new Error(output.message || output.error);
2826    }
2827
2828    if (output.newStatus) {
2829      this.statusGeneration++;
2830      this.redemptionData = output.newStatus;
2831    }
2832
2833    if (output.type === "discount_code" || output.type === "gift_card") {
2834      await this._addDiscountCode({ type: output.type, code: output.code }, output.removeCodes);
2835    } else {
2836      throw new Error(
2837        `Unknown type of reward: ${output.type} (full response: ${JSON.stringify(output)})`,
2838      );
2839    }
2840    this.desiredRedemptionAmountOrPoints = null;
2841
2842    this.reloadCart();
2843  }
2844
2845  async _addDiscountCode(coupon, removeCodes) {
2846    if (isShopifyStorefrontApiAvailable()) {
2847      await addDiscountCode(this.cartData.token, coupon.code, removeCodes);
2848      this._sendToDestination(null);
2849    } else {
2850      this._sendToDestination(coupon);
2851    }
2852  }
2853
2854  _sendToDestination(coupon) {
2855    this.dispatchEvent(
2856      new CustomEvent("bubblehouse:cart-updated", {
2857        detail: {},
2858        bubbles: true,
2859      }),
2860    );
2861    if (this.redemptionData.useAdvancedIntegration) {
2862      return;
2863    }
2864    let destination = this.redemptionData.sendToCart ? "/cart" : "/checkout";
2865    if (!coupon) {
2866      window.location.href = destination;
2867    } else if (coupon.type === "discount_code" && this.redemptionData.useShopifyDiscountPath) {
2868      window.location.href = `/discount/${encodeURIComponent(coupon.code)}?redirect=${destination}`;
2869    } else {
2870      window.location.href = `${destination}?discount=${encodeURIComponent(coupon.code)}`;
2871    }
2872  }
2873
2874  _onRedemptionAmountChange(ev) {
2875    let amount = parseFloat(this.redemptionInput.value || "");
2876    if (isNaN(amount) || !isFinite(amount)) {
2877      amount = NaN; // null would mean default; NaN means custom but invalid
2878    } else {
2879      amount = this.vm.convertPointsToAmount(
2880        Math.ceil(this.vm.convertAmountToPoints(amount)),
2881      );
2882    }
2883    this.desiredRedemptionAmountOrPoints = amount;
2884    this.render();
2885    this.redemptionInput.reportValidity();
2886  }
2887
2888  _onRedemptionPointsChange(ev) {
2889    let pts = parseInt(this.redemptionInput.value || "", 10);
2890    this.desiredRedemptionAmountOrPoints = pts;
2891    this.render();
2892    this.redemptionInput.reportValidity();
2893  }
2894
2895  _renderStatusMessage(message, actions = null) {
2896    if (message) {
2897      this.statusPanel.classList.remove("hidden");
2898      if (actions) {
2899        applyContentWithActions(this.statusMessageSpan, message, actions);
2900      } else {
2901        this.statusMessageSpan.textContent = message;
2902      }
2903    } else {
2904      this.statusPanel.classList.add("hidden");
2905      this.statusMessageSpan.textContent = "";
2906    }
2907  }
2908
2909  _onCartChanged(cartData, cartAction) {
2910    this.cartGeneration++;
2911    this.cartData = cartData;
2912    this.reloadStatus("cart_updated");
2913    this.render();
2914  }
2915
2916  _onCartLoaded(cartData, generation) {
2917    if (generation !== this.cartGeneration) {
2918      return;
2919    }
2920    this.cartData = cartData;
2921    this.reloadStatus("cart_loaded");
2922    this.render();
2923  }
2924  _onCartLoadingFailed(error, generation) {
2925    if (generation !== this.cartGeneration) {
2926      return;
2927    }
2928    reportWarning(error, "cannot load cart");
2929    this.render();
2930  }
2931
2932  _onWidgetDataLoaded(redemptionData, generation) {
2933    if (generation !== this.statusGeneration) {
2934      return;
2935    }
2936    this.redemptionData = redemptionData;
2937    this.render();
2938  }
2939  _onWidgetDataLoadingFailed(error, generation) {
2940    if (generation !== this.statusGeneration) {
2941      return;
2942    }
2943    reportWarning(error, "cannot load redemption data");
2944    this.render();
2945  }
2946}
2947
2948function applyContentWithActions(el, contentStr, actions) {
2949  let validTags = Object.keys(actions).map((k) => 'bhaction:' + k);
2950  let root = parseTags(contentStr, { validTags });
2951  console.log(`[Bubblehouse] applyContentWithActions: str = ${JSON.stringify(contentStr)}, actions = ${JSON.stringify(actions)}, content = ${JSON.stringify(root, null, 2)}`)
2952
2953  while (el.firstChild) {
2954    el.removeChild(el.firstChild);
2955  }
2956
2957  appendRenderTagContent(el, root, actions);
2958}
2959
2960function appendRenderTagContent(el, content, actions) {
2961  for (let item of content) {
2962    appendRenderTagItem(el, item, actions);
2963  }
2964}
2965
2966function appendRenderTagItem(el, item, actions) {
2967  if (typeof item === 'string') {
2968    el.appendChild(document.createTextNode(item));
2969  } else {
2970    let actionName = item.tag.replace(/^bhaction:/, '')
2971    let action = actions[actionName]
2972    if (!action) {
2973      el.appendChild(document.createTextNode('(!TAG!)'));
2974      appendRenderTagContent(el, item.body, actions);
2975      return
2976    }
2977    let actionEl = document.createElement('a');
2978    if (action.jscall) {
2979      actionEl.addEventListener('click', (ev) => {
2980        if (runActionJSCall(item.tag, action.jscall)) {
2981          ev.preventDefault();
2982        }
2983      });
2984    }
2985    actionEl.setAttribute('part', 'actionlink');
2986    if (action.nav) {
2987      actionEl.href = action.nav.url;
2988      let target = actionNavigationTarget(action.nav.target);
2989      if (target) {
2990        actionEl.target = target;
2991      }
2992    }
2993    el.appendChild(actionEl);
2994    appendRenderTagContent(actionEl, item.body, actions);
2995  }
2996}
2997
2998function runActionJSCall(tag, call) {
2999  let name = call.func;
3000  let func = window[name];
3001  if (typeof func !== "function") {
3002    console.error(`[Bubblehouse] <${tag}>: global function ${name} not found`);
3003    return false;
3004  }
3005  try {
3006    func();
3007    return true;
3008  } catch (error) {
3009    reportError(error, `<${tag}>`);
3010    return false;
3011  }
3012}
3013
3014function actionNavigationTarget(target) {
3015  switch (target) {
3016  case "new":
3017  case "_blank":
3018    return "_blank";
3019  case "top":
3020  case "_top":
3021    return "_top";
3022  case "same":
3023  case "":
3024  case null:
3025  case undefined:
3026    return "";
3027  default:
3028    console.error(`[Bubblehouse] unsupported action navigation target ${target}`);
3029    return "";
3030  }
3031}
3032
3033customElements.define("bubblehouse-redeem-panel", BubblehouseRedeemPanel);
3034})() || true;
3035
3036/* variantChangeEvent.js */
3037;(function() {
3038if (isShopifyProductPage()) {
3039  window.addEventListener("load", () => {
3040    observeElements('[name="id"]', observeAndDispatchVariantChange)
3041  })
3042}
3043
3044function observeAndDispatchVariantChange(input) {
3045  let observer = new MutationObserver((mutations) => {
3046    for (const mutation of mutations) {
3047      if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
3048        window.dispatchEvent(
3049          new CustomEvent('bubblehouse:variant-change', { detail: { variantId: mutation.target.value } })
3050        )
3051      }
3052    }
3053  })
3054
3055  observer.observe(input, {
3056    attributes: true,
3057    attributeFilter: ['value'],
3058  })
3059}
3060})() || true;
3061
3062/* bigcommerce/api.js */
3063;(function() {
3064window.Bubblehouse.BigCommerce ??= {};
3065window.Bubblehouse.BigCommerce.getToken = getToken;
3066window.Bubblehouse.BigCommerce.loadCart = loadCart;
3067window.Bubblehouse.BigCommerce.createCart = createCart;
3068window.Bubblehouse.BigCommerce.addToCart = addToCart;
3069window.Bubblehouse.BigCommerce.updateCartItem = updateCartItem;
3070window.Bubblehouse.BigCommerce.deleteCartItem = deleteCartItem;
3071window.Bubblehouse.BigCommerce.deleteCart = deleteCart;
3072window.Bubblehouse.BigCommerce.getCheckout = getCheckout;
3073window.Bubblehouse.BigCommerce.applyCouponToCheckout = applyCouponToCheckout;
3074
3075async function getToken(appClientId) {
3076  return fetch(`/customer/current.jwt?app_client_id=${appClientId}`).then(
3077    (response) => response.text());
3078}
3079
3080function loadCart() {
3081  return fetch("/api/storefront/carts?include=lineItems.physicalItems.options,lineItems.digitalItems.options", {
3082    method: "GET",
3083    credentials: "same-origin",
3084  }).then(wantSuccessfulJson);
3085}
3086
3087function createCart(lineItems) {
3088  let body = { lineItems };
3089  console.log("[BH] [DEBUG] {BigCommerce} creating BigCommerce cart:", body);
3090  return fetch("/api/storefront/carts", {
3091    method: "POST",
3092    credentials: "same-origin",
3093    headers: { "Content-Type": "application/json" },
3094    body: JSON.stringify(body),
3095  }).then(wantSuccessfulJson);
3096}
3097
3098function addToCart(cartId, lineItems, { origin } = {}) {
3099  let body = { lineItems };
3100  console.log(`[BH] [DEBUG] {BigCommerce} adding to BigCommerce cart (${origin}):`, body);
3101  return fetch(`/api/storefront/carts/${cartId}/items`, {
3102    method: "POST",
3103    credentials: "same-origin",
3104    headers: { "Content-Type": "application/json" },
3105    body: JSON.stringify(body),
3106  }).then(wantSuccessfulJson);
3107}
3108
3109function updateCartItem(cartId, itemId, lineItem, { origin } = {}) {
3110  let body = { lineItem };
3111  console.log(`[BH] [DEBUG] {BigCommerce} updating BigCommerce cart item (${origin}):`, body);
3112  return fetch(`/api/storefront/carts/${cartId}/items/${itemId}`, {
3113    method: "PUT",
3114    credentials: "same-origin",
3115    headers: { "Content-Type": "application/json" },
3116    body: JSON.stringify(body),
3117  }).then(wantSuccessfulJson);
3118}
3119
3120function deleteCartItem(cartId, itemId, { origin } = {}) {
3121  console.log(`[BH] [DEBUG] {BigCommerce} deleting BigCommerce cart item (${origin}):`, itemId);
3122  return fetch(`/api/storefront/carts/${cartId}/items/${itemId}`, {
3123    method: "DELETE",
3124    credentials: "same-origin",
3125  }).then(wantSuccessfulJson);
3126}
3127
3128function deleteCart(cartId, { origin } = {}) {
3129  console.log(`[BH] [DEBUG] {BigCommerce} deleting BigCommerce cart (${origin}):`, cartId);
3130  return fetch(`/api/storefront/carts/${cartId}`, {
3131    method: "DELETE",
3132    credentials: "same-origin",
3133  }).then(wantSuccessfulJson);
3134}
3135
3136async function getCheckout(checkoutId) {
3137  return fetch(`/api/storefront/checkouts/${checkoutId}`, {
3138    method: "GET",
3139    credentials: "same-origin",
3140    headers: { "Content-Type": "application/json" },
3141  }).then(wantSuccessfulJson);
3142}
3143
3144async function applyCouponToCheckout(checkoutId, couponCode) {
3145  console.log(`[BH] [DEBUG] {BigCommerce}
3145 applying coupon to BigCommerce checkout: ${couponCode}`);
3146  let body = { couponCode };
3147  return fetch(`/api/storefront/checkouts/${checkoutId}/coupons`, {
3148    method: "POST",
3149    credentials: "same-origin",
3150    headers: { "Content-Type": "application/json" },
3151    body: JSON.stringify(body),
3152  }).then(wantSuccessfulJson);
3153}
3154})() || true;
3155
3156/* woocommerce/api.js */
3157window.Bubblehouse ??= {};
3158window.Bubblehouse.WooCommerce ??= {};
3159
3160window.Bubblehouse.WooCommerce.loadCart = async function() {
3161  return await fetch(`/wp-json/wc/store/v1/cart`, {
3162    credentials: 'include',
3163  })
3164    .then(async (response) => ({
3165      ...await response.json(),
3166      token: response.headers.get("cart-token"),
3167    }))
3168    .catch((err) => {
3169      console.error(`[BH] {WooCommerce} Unable to load cart: ${err}`);
3170      return null;
3171    });
3172}
3173
3174window.Bubblehouse.WooCommerce.applyCoupon = async function(_token, code) {
3175  // NOTE (@seletskiy): Detect version of checkout used (older vs newer WooCommerce).
3176  //                    'bh-discounts' namespace and handler is defined in bubblehouse-woocomm PHP extension.
3177  var isBlockCheckout = (
3178    typeof window.wc !== 'undefined' &&
3179    window.wc.blocksCheckout &&
3180    typeof window.wc.blocksCheckout.extensionCartUpdate === 'function'
3181  );
3182
3183  if (isBlockCheckout) {
3184    try {
3185      await window.wc.blocksCheckout.extensionCartUpdate({
3186        namespace: 'bh-discounts',
3187        data: { coupon_code: code },
3188      });
3189
3190      console.log(`[BH] [DEBUG] {applyCoupon} Block Checkout - coupon applied`);
3191    } catch (err) {
3192      console.log(`[BH] [DEBUG] {applyCoupon} Block Checkout - error: ${err}`);
3193    };
3194  } else {
3195    jQuery.ajax({
3196      type: 'POST',
3197      url: woocommerce_params.ajax_url,
3198      data: {
3199        action: 'bh_apply_coupon',
3200        coupon_code: code,
3201      },
3202      success: () => {
3203        $(document.body).trigger('update_checkout');
3204        console.log(`[BH] [DEBUG] {applyCoupon} Classic Checkout - coupon applied`)
3205      },
3206      error: (err) => {
3207        console.log(`[BH] [DEBUG] {applyCoupon} Classic Checkout - error: ${err}`)
3208      },
3209    });
3210  }
3211}
3212
3213window.Bubblehouse.WooCommerce.removeCoupon = async function(token, code) {
3214  return await fetch(`/wp-json/wc/store/v1/cart/coupons/${code}`, {
3215    method: 'DELETE',
3216    credentials: 'include',
3217    headers: {
3218      'Content-Type': 'application/json',
3219      'Cart-Token': token,
3220    },
3221  })
3222    .then((response) => response.json())
3223    .catch((err) => {
3224      console.error(`[BH] {WooCommerce} Unable to remove coupon: ${err}`);
3225      return null;
3226    });
3227}
3228
3229window.Bubblehouse.WooCommerce.removeAllCoupons = async function(token) {
3230  return await fetch(`/wp-json/wc/store/v1/cart/coupons`, {
3231    method: 'DELETE',
3232    credentials: 'include',
3233    headers: {
3234      'Content-Type': 'application/json',
3235      'Cart-Token': token,
3236    },
3237  })
3238    .then((response) => response.json())
3239    .catch((err) => {
3240      console.error(`[BH] {WooCommerce} Unable to remove all coupons: ${err}`);
3241      return null;
3242    });
3243}
3244
3245window.Bubblehouse.WooCommerce.addItem = async function(token, productId, quantity) {
3246  return await fetch(`/wp-json/wc/store/v1/cart/add-item`, {
3247    method: 'POST',
3248    credentials: 'include',
3249    headers: {
3250      'Content-Type': 'application/json',
3251      'Cart-Token': token,
3252    },
3253    body: JSON.stringify({ id: productId, quantity }),
3254  })
3255    .then((response) => response.json())
3256    .catch((err) => {
3257      console.error(`[BH] {WooCommerce} Unable to add item: ${err}`);
3258      return null;
3259    });
3260}
3261
3262window.Bubblehouse.WooCommerce.updateItem = async function(token, key, quantity) {
3263  return await fetch(`/wp-json/wc/store/v1/cart/update-item`, {
3264    method: 'POST',
3265    credentials: 'include',
3266    headers: {
3267      'Content-Type': 'application/json',
3268      'Cart-Token': token,
3269    },
3270    body: JSON.stringify({ key, quantity }),
3271  })
3272    .then((response) => response.json())
3273    .catch((err) => {
3274      console.error(`[BH] {WooCommerce} Unable to update item: ${err}`);
3275      return null;
3276    });
3277}
3278
3279
3280/* cart.js */
3281;(function() {
3282function CartApi() {
3283  const providers = {
3284    BigCommerce: {
3285      load: async () => {
3286        const carts = await Bubblehouse.BigCommerce.loadCart()
3287        if (!carts) {
3288          console.error(`[BH] {CartApi/BigCommerce} Unable to load carts.`);
3289          return null;
3290        }
3291
3292        if (carts?.length < 1) {
3293          console.warn(`[BH] {CartApi/BigCommerce} No active carts found.`);
3294          return { id: null, items: [] };
3295        }
3296
3297        const cart = carts[0];
3298
3299        return {
3300          id: cart.id,
3301          items: [
3302            ...cart.lineItems.physicalItems.map((item) => ({
3303              id: item.id,
3304              variant: { id: item.variantId },
3305              product: { id: item.productId },
3306            })),
3307          ]
3308        }
3309      },
3310
3311      add: async (cartId, { products, discount }, opts) => {
3312        if (products) {
3313          const items = products.map(({ product, quantity, variant, options }) => ({
3314            productId: product?.id,
3315            variantId: variant?.id,
3316            quantity,
3317            optionSelections: options
3318              ? Object.entries(options).map(([k, v]) => ({ optionId: k, optionValue: v }))
3319              : [],
3320          }));
3321
3322          if (cartId) {
3323            await Bubblehouse.BigCommerce.addToCart(cartId, items, opts);
3324          } else {
3325            const { id } = await Bubblehouse.BigCommerce.createCart(items);
3326            cartId = id;
3327          }
3328        }
3329
3330        if (discount) {
3331          await Bubblehouse.BigCommerce.applyCouponToCheckout(cartId, discount.code)
3332        }
3333      },
3334
3335      update: async (cartId, { items }, opts) => {
3336        return await Promise.all(items.map(({ id, variant, product, quantity }) =>
3337          Bubblehouse.BigCommerce.updateCartItem(
3338            cartId,
3339            id,
3340            {
3341              productId: product?.id,
3342              variantId: variant?.id,
3343              quantity,
3344            },
3345            opts,
3346          ),
3347        ))
3348      },
3349    },
3350
3351    Shopify: {
3352      load: async () => {
3353        const cart = await Bubblehouse.loadCart();
3354        if (!cart) {
3355          console.error(`[BH] {CartApi/Shopify} Unable to load cart.`);
3356          return null;
3357        }
3358
3359        return {
3360          id: cart.token,
3361          items: cart.items.map((item) => ({
3362            id: item.key,
3363            variant: { id: item.variant_id },
3364            product: { id: item.product_id },
3365          }))
3366        }
3367      },
3368
3369      add: async (cartId, { products, discount }, opts) => {
3370        if (products) {
3371          const items = products.map(({ variant, quantity }) => ({
3372            id: variant?.id,
3373            quantity,
3374          }));
3375
3376          await Bubblehouse.addToCart(items, opts);
3377        }
3378
3379        if (discount) {
3380          await Bubblehouse.addDiscountCode(cartId, discount.code);
3381        }
3382      },
3383
3384      update: async (_cartId, { items }, opts) => {
3385        const updates = {};
3386        items.forEach(({ id, quantity }) => {
3387          updates[id] = quantity;
3388        });
3389
3390        return await Bubblehouse.updateCart(updates, opts);
3391      },
3392    },
3393
3394    WooCommerce: {
3395      load: async () => {
3396        const cart = await Bubblehouse.WooCommerce.loadCart();
3397        if (!cart) {
3398          console.error(`[BH] {CartApi/WooCommerce} Unable to load cart.`);
3399          return null;
3400        }
3401
3402        return {
3403          id: cart.token,
3404          items: cart.items?.map((item) => ({
3405            id: item.key,
3406            variant: { id: item.id?.toString() },
3407            product: { id: item.id?.toString() },
3408          })) ?? []
3409        }
3410      },
3411
3412      add: async (token, { products, discount }, _opts) => {
3413        if (products) {
3414          for (const { product, quantity } of products) {
3415            await Bubblehouse.WooCommerce.addItem(token, product?.id, quantity);
3416          }
3417        }
3418
3419        if (discount) {
3420          await Bubblehouse.WooCommerce.applyCoupon(token, discount.code);
3421        }
3422      },
3423
3424      update: async (token, { items }, _opts) => {
3425        for (const { id, quantity } of items) {
3426          await Bubblehouse.WooCommerce.updateItem(token, id, quantity);
3427        }
3428      },
3429    }
3430  }
3431
3432  const isBigCommerce = window.BCData !== undefined;
3433  const isShopify = window.Shopify !== undefined;
3434  const isWooCommerce = window.wc !== undefined;
3435
3436  if (isBigCommerce) {
3437    console.log("[BH] {CartApi} Using BigCommerce.")
3438    return providers.BigCommerce;
3439  }
3440
3441  if (isWooCommerce) {
3442    console.log("[BH] {CartApi} Using WooCommerce.")
3443    return providers.WooCommerce;
3444  }
3445
3446  if (isShopify) {
3447    console.log("[BH] {CartApi} Using Shopify.")
3448    return providers.Shopify;
3449  }
3450
3451  const noop = () => {
3452    console.log("[BH] {CartApi} Not supported.")
3453  }
3454
3455  return { load: noop, add: noop, update: noop };
3456}
3457
3458window.Bubblehouse.CartApi = CartApi;
3459})() || true;
3460
3461/* outerFlash.js */
3462function showFlashMessage(message) {
3463  if (isPopupAvailable('message')) {
3464    openPopup('message', { message })
3465  } else {
3466    let text = message.t
3467    if (message.m === 'failure') {
3468      text = 'Error: ' + text
3469    }
3470    alert(text)
3471  }
3472}
3473
3474
3475/* outerApi.js */
3476;(function() {
3477async function handleApplyDiscountCode1(ev) {
3478  const code = ev.detail.code;
3479  if (!code) {
3480    return;
3481  }
3482
3483  const api = window.Bubblehouse.CartApi();
3484  try {
3485    const cart = await api.load();
3486    if (!cart) {
3487      console.log("[BH] {ApplyDiscountCode1} No cart info available.")
3488      return;
3489    }
3490
3491    await api.add(
3492      cart.id,
3493      {
3494        products: null,
3495        discount: { code },
3496      },
3497      { origin: "ApplyDiscountCode1" }
3498    );
3499  } catch (error) {
3500    cartErrorShow(error, code);
3501  }
3502}
3503
3504async function handleAddProductToCart1(ev) {
3505  const {
3506    variantId, productId, options, discountCode, quantity,
3507    successMessage, cartUrl, refreshOnAdd, ignoreIfAlreadyAdded,
3508  } = ev.detail;
3509  const api = window.Bubblehouse.CartApi();
3510
3511  try {
3512    const cart = await api.load();
3513    if (!cart) {
3514      console.log("[BH] {AddProductToCart1} No cart info available.")
3515      return;
3516    }
3517
3518    const alreadyAdded = ignoreIfAlreadyAdded && cart.items.some(({ variant }) => variant?.id == variantId);
3519    if (alreadyAdded && !discountCode) {
3520      console.log(`[BH] {AddProductToCart1} Already got product variant in cart.`)
3521      return;
3522    }
3523
3524    await api.add(
3525      cart.id,
3526      {
3527        products: alreadyAdded ? null : [{
3528          variant: { id: variantId },
3529          quantity,
3530          ...(productId ? { product: { id: productId } } : {}),
3531          options,
3532        }],
3533        discount: discountCode ? { code: discountCode } : null,
3534      },
3535      { origin: "AddProductToCart1" }
3536    );
3537  } catch (error) {
3538    cartErrorShow(error, discountCode);
3539    return;
3540  }
3541
3542  if (successMessage) {
3543    showFlashMessage({ t: successMessage })
3544    if (refreshOnAdd) {
3545      setTimeout(() => { window.location.reload(); }, 1000)
3546    }
3547  } else {
3548    if (cartUrl !== null) {
3549      navigateToCartOrReloadPage(cartUrl)
3550    }
3551  }
3552}
3553
3554function cartErrorShow(error, code) {
3555  console.error("[BH] Unable to update cart:", error);
3556  const message = (code ? Bubblehouse.data?.cartCouponErrorMessage : Bubblehouse.data?.cartErrorMessage) ||
3557    error?.message || String(error);
3558  showFlashMessage({ m: 'failure', t: [message, code].filter(Boolean).join('\n') });
3559}
3560
3561document.addEventListener('bhclient:AddProductToCart1', handleAddProductToCart1);
3562document.addEventListener('bhclient:ApplyDiscountCode1', handleApplyDiscountCode1);
3563})() || true;
3564
3565/* resizer.js */
3566document.addEventListener('bhclient:Resize1', function (ev) {
3567  ev.target.style.height = ev.detail.height + 'px';
3568});
3569
3570
3571/* outerMessage.js */
3572document.addEventListener('bhclient:PostOuterMessage1', handlePostOuterMessage1);
3573
3574function handlePostOuterMessage1(ev) {
3575  window.postMessage({ event: ev.detail.event, data: ev.detail.data }, '*')
3576}
3577
3578
3579/* tags/bubblehouse-value.js */
3580;(function() {
3581const VALUES = {
3582    'points': () => Bubblehouse.data.userPointBalanceStr,
3583    'points-value': () => Bubblehouse.data.userPointBalanceValueStr,
3584    'tier': () => Bubblehouse.data.userTierName,
3585}
3586
3587// <bubblehouse-value key="..."> displays the specified loyalty value (properly formatted for the current customer).
3588class BubblehouseValue extends HTMLElement {
3589    constructor() {
3590        super();
3591        const shadow = this.attachShadow({ mode: 'open' });
3592        this.valueSpan = document.createElement('span');
3593        shadow.appendChild(this.valueSpan);
3594    }
3595
3596    connectedCallback() {
3597        this.render();
3598    }
3599
3600    render() {
3601        let key = this.getAttribute('key')
3602        if (!key) {
3603            key = this.getAttribute('altkey') // don't ask, it's an attempt to debug a problem with Wypeuk
3604        }
3605        let func = VALUES[key]
3606        if (!func) {
3607            console.error(`[Bubblehouse] <bubblehouse-value> has invalid key=${JSON.stringify(key)}`)
3608        }
3609        let value = (func ? func() : 'invalid-value-key')
3610        let fallback = this.getAttribute('fallback')
3611        let text
3612        if (value == null || value === '') {
3613            text = fallback || ''
3614        } else if (value == 0 && fallback != null) {
3615            text = fallback || ''
3616        } else {
3617            text = '' + value
3618        }
3619        this.valueSpan.textContent = text
3620        if (Bubblehouse.verbose) console.log(`[Bubblehouse] rendered <bubblehouse-value key="${key}">`)
3621    }
3622}
3623
3624customElements.define('bubblehouse-value', BubblehouseValue);
3625})() || true;
3626
3627/* tags/bubblehouse-panel.js */
3628;(function() {
3629// <bubblehouse-panel> unhides itself after Bubblehouse gets loaded.
3630class BubblehousePanel extends HTMLElement {
3631    connectedCallback() {
3632        adjustPanelVisibility(this)
3633    }
3634}
3635
3636customElements.define('bubblehouse-panel', BubblehousePanel);
3637})() || true;
3638
3639/* tags/bubblehouse-block.js */
3640;(function() {
3641// <bubblehouse-block> inserts the given Bubblehouse page/block.
3642class BubblehouseBlock extends HTMLElement {
3643    constructor() {
3644        super()
3645        this.attachShadow({ mode: 'open' })
3646        this._onMessage = this._onMessage.bind(this)
3647    }
3648
3649    connectedCallback() {
3650        this._initialize();
3651        if (!adjustPanelVisibility(this)) return;
3652        window.addEventListener('message', this._onMessage)
3653    }
3654
3655    disconnectedCallback() {
3656        window.removeEventListener('message', this._onMessage)
3657    }
3658
3659    _initialize() {
3660        if (this._initialized) {
3661            return
3662        }
3663        this._initialized = true;
3664
3665        let blockName = this.getAttribute('block')
3666        let suffix = this.getAttribute('suffix') || ''
3667        let height = this.getAttribute('height') || '0'
3668        let shopSlug = this.getAttribute('shop')
3669        let auth = this.getAttribute('auth')
3670        let title = (this.getAttribute('title') || '').trim()
3671        if (!blockName) {
3672            throw new Error('<bubblehouse-block>: block name must be specified (e.g. block="rewards")')
3673        }
3674        let encodedBlockName = encodeBubblehouseBlockPathSegment(blockName, 'block name')
3675        let instance = `bubblehouse-embedded-${blockName}${suffix}`
3676        let url
3677        if (!shopSlug && window.Shopify) {
3678            shopSlug = window.Shopify.shop.replace(/\.myshopify\.com$/, '')
3679            if (shopSlug) {
3680                shopSlug = 'shopify-' + shopSlug
3681            }
3682            auth = auth || Bubblehouse.data.authToken || ''
3683        }
3684        if (window.Shopify && !shopSlug) {
3685            url = new URL(`/apps/bubblehouse/${encodedBlockName}/content/`, window.location.origin)
3686        } else {
3687            if (!shopSlug) {
3688                throw new Error('<bubblehouse-block>: shop slug must be specified (e.g. shop="my-shop")')
3689            }
3690            if (auth == null) {
3691                throw new Error('<bubblehouse-block>: auth must be specified (as either an empty string or a JWT token, e.g. auth="" or auth="...")')
3692            }
3693            let encodedShopSlug = encodeBubblehouseBlockPathSegment(shopSlug, 'shop slug')
3694            url = new URL(`https://app.bubblehouse.com/s/${encodedShopSlug}/blocks/${encodedBlockName}`)
3695        }
3696        if (!this._canUseBubblehouseAuth(url)) {
3697            throw new Error('<bubblehouse-block>: invalid Bubblehouse iframe URL')
3698        }
3699        let shouldFetchAuthTokenForBlock = Boolean(Bubblehouse.data.fetchAuthTokenForBlock) && !auth
3700        if (!shouldFetchAuthTokenForBlock) {
3701            url.searchParams.set('auth', auth || '')
3702        }
3703        url.searchParams.set('instance', instance)
3704        for (let [name, value] of Object.entries(collectCurrentMarketQueryParams())) {
3705            url.searchParams.set(name, value)
3706        }
3707
3708        this.iframe = h('iframe', {
3709            src: shouldFetchAuthTokenForBlock ? null : url.toString(),
3710            style: {
3711                width: '100%',
3712                border: 'none',
3713                height: height + 'px',
3714            },
3715            title: title || null,
3716            sandbox: 'allow-top-navigation allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin',
3717            allow: 'clipboard-write',
3718        })
3719        this.shadowRoot.append(this.iframe)
3720        if (shouldFetchAuthTokenForBlock) {
3721            this._shopifyProxyAuthTokenPromise = this._loadWithShopifyProxyAuthToken(url)
3722        }
3723    }
3724
3725    applyBubblehouseAuthToken(token) {
3726        if (!token || !this.iframe) {
3727            return false
3728        }
3729        let url
3730        try {
3731            url = new URL(this.iframe.src, window.location.href)
3732        } catch {
3733            return false
3734        }
3735        if (url.searchParams.get('auth')) {
3736            return false
3737        }
3738        if (!this._canUseBubblehouseAuth(url)) {
3739            return false
3740        }
3741        url.searchParams.set('auth', token)
3742        this.iframe.src = url.toString()
3743        return true
3744    }
3745
3746    async _loadWithShopifyProxyAuthToken(url) {
3747        let token = await fetchShopifyProxyAuthToken()
3748        if (token) {
3749            Bubblehouse.data.authToken = token
3750            url.searchParams.set('auth', token)
3751        } else {
3752            url.searchParams.set('auth', '')
3753        }
3754        if (this.iframe) {
3755            this.iframe.src = url.toString()
3756        }
3757    }
3758
3759    _canUseBubblehouseAuth(url) {
3760        return isBubblehouseAuthFrameURL(url, 'https://app.bubblehouse.com', window.location.origin)
3761    }
3762
3763    _onMessage(ev) {
3764        let data = ev.data
3765        if (!data || ev.source !== this.iframe.contentWindow) {
3766            return
3767        }
3768        if (data.bubblehouseBlockClientCall === 'Scroll1') {
3769            this.iframe.dispatchEvent(new CustomEvent('bhclient:Scroll1', {
3770                detail: Object.assign({}, data, { sourceWindow: ev.source, sourceOrigin: ev.origin }),
3771                bubbles: true,
3772                composed: true,
3773            }))
3774            return
3775        }
3776        if (data.bubblehouseBlockClientCall !== 'Resize1') {
3777            return
3778        }
3779        if (data.height) {
3780            this.iframe.style.height = data.height + 'px'
3781        }
3782        if (!this.iframe.title && typeof data.title === 'string') {
3783            let title = data.title.trim()
3784            if (title) {
3785                this.iframe.title = title
3786            }
3787        }
3788    }
3789}
3790
3791function encodeBubblehouseBlockPathSegment(value, name) {
3792    if (!/^[A-Za-z0-9_-]+$/.test(value)) {
3793        throw new Error(`<bubblehouse-block>: ${name} must contain only letters, numbers, underscores, or dashes`)
3794    }
3795    return encodeURIComponent(value)
3796}
3797
3798async function fetchShopifyProxyAuthToken() {
3799    try {
3800        let response = await fetch('/apps/bubblehouse/customer-auth-token', {
3801            credentials: 'include',
3802            headers: {
3803                Accept: 'application/json',
3804            },
3805        })
3806        if (!response.ok) {
3807            return ''
3808        }
3809        let data = await response.json()
3810        return typeof data.token === 'string' ? data.token : ''
3811    } catch {
3812        return ''
3813    }
3814}
3815
3816customElements.define('bubblehouse-block', BubblehouseBlock);
3817})() || true;
3818
3819/* toc-scroll.js */
3820async function handleScroll1(ev) {
3821  let scrollEl = null;
3822  let gap = 16;
3823
3824  let ionContent = ev.target.closest('ion-content') || document.querySelector('ion-content');
3825  if (ionContent) {
3826    scrollEl = await ionContent.getScrollElement();
3827  }
3828
3829  if (scrollEl) {
3830    let containerRect = scrollEl.getBoundingClientRect();
3831    let baseOffset = ev.target.getBoundingClientRect().top - containerRect.top + scrollEl.scrollTop;
3832    let offset = Math.max(0, baseOffset - gap + ev.detail.offset);
3833    let scrollElDesc = scrollEl.id ? `${scrollEl.tagName.toLowerCase()}#${scrollEl.id}` : scrollEl.className ? `${scrollEl.tagName.toLowerCase()}.${scrollEl.className.split(' ')[0]}` : scrollEl.tagName.toLowerCase();
3834    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)`);
3835    scrollEl.scrollTo({ top: Math.round(offset), behavior: 'smooth' });
3836  } else {
3837    let baseOffset = ev.target.getBoundingClientRect().top + window.scrollY;
3838    let offset = Math.max(0, baseOffset - gap + ev.detail.offset);
3839    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)`);
3840    window.scroll({ top: Math.round(offset), behavior: 'smooth' });
3841  }
3842}
3843
3844document.addEventListener('bhclient:Scroll1', handleScroll1);
3845
3846
3847/* anchor-navigation.js */
3848const BUBBLEHOUSE_ANCHOR_PREFIX = "#bh-";
3849const BUBBLEHOUSE_ANCHOR_RETRY_DELAY_MS = 100;
3850const BUBBLEHOUSE_ANCHOR_RETRY_COUNT = 50;
3851const BUBBLEHOUSE_ANCHOR_FINAL_WAIT_MS = 1500;
3852const BUBBLEHOUSE_ANCHOR_SETTLE_DELAY_MS = 2000;
3853const BUBBLEHOUSE_ANCHOR_SETTLE_THRESHOLD_PX = 50;
3854
3855let bubblehouseAnchorNavigation = null;
3856let bubblehouseAnchorSettleTimerID = 0;
3857
3858navigateToCurrentBubblehouseAnchor();
3859window.addEventListener("hashchange", navigateToCurrentBubblehouseAnchor);
3860window.addEventListener("message", handleBubblehouseAnchorResizeMessage);
3861document.addEventListener("bhclient:Scroll1", handleBubblehouseAnchorScroll1);
3862
3863function navigateToCurrentBubblehouseAnchor() {
3864  let anchor = currentBubblehouseAnchor();
3865  if (!anchor) {
3866    cancelBubblehouseAnchorNavigation();
3867    return;
3868  }
3869  startBubblehouseAnchorNavigation(anchor);
3870}
3871
3872function startBubblehouseAnchorNavigation(anchor) {
3873  cancelBubblehouseAnchorNavigation();
3874  bubblehouseAnchorNavigation = {
3875    anchor,
3876    resizeRetryUsed: false,
3877    retriesRemaining: BUBBLEHOUSE_ANCHOR_RETRY_COUNT,
3878    timerID: 0,
3879  };
3880  attemptBubblehouseAnchorNavigation(false);
3881}
3882
3883function attemptBubblehouseAnchorNavigation(finalAttempt) {
3884  let state = bubblehouseAnchorNavigation;
3885  if (!state) {
3886    return;
3887  }
3888  state.timerID = 0;
3889  if (scrollToSameOriginBubblehouseAnchor(state.anchor)) {
3890    completeBubblehouseAnchorNavigation();
3891    return;
3892  }
3893  let posted = postNavigateToAnchor1(state.anchor);
3894  if (finalAttempt) {
3895    scheduleBubblehouseAnchorGiveUp(state.anchor, posted);
3896  } else if (state.retriesRemaining > 0) {
3897    state.retriesRemaining--;
3898    state.timerID = setTimeout(() => attemptBubblehouseAnchorNavigation(false), BUBBLEHOUSE_ANCHOR_RETRY_DELAY_MS);
3899  } else {
3900    warnBubblehouseAnchorNavigationFailed(state.anchor);
3901  }
3902}
3903
3904function currentBubblehouseAnchor() {
3905  let hash = location.hash || "";
3906  if (!hash.startsWith(BUBBLEHOUSE_ANCHOR_PREFIX)) {
3907    return "";
3908  }
3909  let anchor = hash.slice(BUBBLEHOUSE_ANCHOR_PREFIX.length);
3910  if (!anchor || anchor.includes("=")) {
3911    return "";
3912  }
3913  return anchor;
3914}
3915
3916function scrollToSameOriginBubblehouseAnchor(anchor) {
3917  for (let frame of bubblehouseAnchorFrames()) {
3918    let section;
3919    try {
3920      section = frame.contentDocument?.getElementById(anchor);
3921    } catch {
3922      section = null;
3923    }
3924    if (!section) {
3925      continue;
3926    }
3927    let inTabgroup = false;
3928    try {
3929      inTabgroup = !!(section.closest && section.closest('[data-controller~="tabgroup"]'));
3930    } catch {
3931    }
3932    let reveal;
3933    try {
3934      reveal = frame.contentWindow?.Bubblehouse?.revealTabgroupContaining;
3935    } catch {
3936      reveal = null;
3937    }
3938    if (inTabgroup && typeof reveal !== 'function') {
3939      continue;
3940    }
3941    if (typeof reveal === 'function') {
3942      try {
3943        section = reveal(section) || section;
3944      } catch {
3945      }
3946    }
3947    let offset = bubblehouseAnchorOffset(section);
3948    dispatchSameOriginBubblehouseAnchorScroll(frame, offset);
3949    scheduleSameOriginBubblehouseAnchorSettle(frame, section, offset);
3950    return true;
3951  }
3952  return false;
3953}
3954
3955function scheduleSameOriginBubblehouseAnchorSettle(frame, section, firstOffset) {
3956  clearBubblehouseAnchorSettleTimer();
3957  bubblehouseAnchorSettleTimerID = setTimeout(() => {
3958    bubblehouseAnchorSettleTimerID = 0;
3959    let settledOffset = bubblehouseAnchorOffset(section);
3960    if (Math.abs(settledOffset - firstOffset) > BUBBLEHOUSE_ANCHOR_SETTLE_THRESHOLD_PX) {
3961      dispatchSameOriginBubblehouseAnchorScroll(frame, settledOffset);
3962    }
3963  }, BUBBLEHOUSE_ANCHOR_SETTLE_DELAY_MS);
3964}
3965
3966function bubblehouseAnchorOffset(section) {
3967  return section.getBoundingClientRect ? section.getBoundingClientRect().top : section.offsetTop;
3968}
3969
3970function dispatchSameOriginBubblehouseAnchorScroll(frame, offset) {
3971  frame.dispatchEvent(new CustomEvent("bhclient:Scroll1", {
3972    detail: {
3973      bubblehouseBlockClientCall: "Scroll1",
3974      instance: frame.id,
3975      offset,
3976    },
3977    bubbles: true,
3978    composed: true,
3979  }));
3980}
3981
3982function postNavigateToAnchor1(anchor) {
3983  let posted = 0;
3984  for (let frame of bubblehouseAnchorFrames()) {
3985    let contentWindow = frame.contentWindow;
3986    if (!contentWindow?.postMessage) {
3987      continue;
3988    }
3989    contentWindow.postMessage({
3990      bubblehouseBlockClientCall: "NavigateToAnchor1",
3991      instance: "#self",
3992      anchor,
3993    }, bubblehouseAnchorTargetOrigin(frame));
3994    posted++;
3995  }
3996  return posted;
3997}
3998
3999function handleBubblehouseAnchorScroll1(ev) {
4000  if (!bubblehouseAnchorNavigation || !isTrustedBubblehouseAnchorSource(ev.detail?.sourceWindow)) {
4001    return;
4002  }
4003  completeBubblehouseAnchorNavigation();
4004}
4005
4006function handleBubblehouseAnchorResizeMessage(ev) {
4007  let state = bubblehouseAnchorNavigation;
4008  if (!state || state.resizeRetryUsed || !isResize1Message(ev.data) || !isTrustedBubblehouseAnchorSource(ev.source)) {
4009    return;
4010  }
4011  state.resizeRetryUsed = true;
4012  clearBubblehouseAnchorTimer();
4013  attemptBubblehouseAnchorNavigation(true);
4014}
4015
4016function isResize1Message(data) {
4017  return data && data.bubblehouseBlockClientCall === "Resize1";
4018}
4019
4020function isTrustedBubblehouseAnchorSource(source) {
4021  return !!source && bubblehouseAnchorFrames().some((frame) => frame.contentWindow === source);
4022}
4023
4024function bubblehouseAnchorFrames() {
4025  return collectBubblehouseFrames(document).filter((frame) => !!bubblehouseAnchorFrameURL(frame));
4026}
4027
4028function bubblehouseAnchorFrameURL(frame) {
4029  try {
4030    let url = new URL(frame.getAttribute("src") || frame.src, location.href);
4031    return isBubblehouseAuthFrameURL(url, bubblehouseAppOrigin(), location.origin) ? url : null;
4032  } catch {
4033    return null;
4034  }
4035}
4036
4037function bubblehouseAnchorTargetOrigin(frame) {
4038  return bubblehouseAnchorFrameURL(frame)?.origin || "*";
4039}
4040
4041function scheduleBubblehouseAnchorGiveUp(anchor, posted) {
4042  if (!bubblehouseAnchorNavigation) {
4043    return;
4044  }
4045  if (posted) {
4046    bubblehouseAnchorNavigation.timerID = setTimeout(() => warnBubblehouseAnchorNavigationFailed(anchor), BUBBLEHOUSE_ANCHOR_FINAL_WAIT_MS);
4047  } else {
4048    warnBubblehouseAnchorNavigationFailed(anchor);
4049  }
4050}
4051
4052function warnBubblehouseAnchorNavigationFailed(anchor) {
4053  if (!bubblehouseAnchorNavigation) {
4054    return;
4055  }
4056  console.warn("[Bubblehouse] giving up on navigation to anchor " + anchor);
4057  cancelBubblehouseAnchorNavigation();
4058}
4059
4060function completeBubblehouseAnchorNavigation() {
4061  clearBubblehouseAnchorTimer();
4062  bubblehouseAnchorNavigation = null;
4063}
4064
4065function cancelBubblehouseAnchorNavigation() {
4066  completeBubblehouseAnchorNavigation();
4067  clearBubblehouseAnchorSettleTimer();
4068}
4069
4070function clearBubblehouseAnchorTimer() {
4071  if (bubblehouseAnchorNavigation?.timerID) {
4072    clearTimeout(bubblehouseAnchorNavigation.timerID);
4073    bubblehouseAnchorNavigation.timerID = 0;
4074  }
4075}
4076
4077function clearBubblehouseAnchorSettleTimer() {
4078  if (bubblehouseAnchorSettleTimerID) {
4079    clearTimeout(bubblehouseAnchorSettleTimerID);
4080    bubblehouseAnchorSettleTimerID = 0;
4081  }
4082}
4083
4084
4085/* public.js */
4086Bubblehouse.centsToAccruedPoints = centsToAccruedPoints
4087Bubblehouse.pointsBalance = pointsBalance
4088Bubblehouse.pointsBalanceStr = pointsBalanceStr
4089Bubblehouse.pointsBalanceMonetaryValue = pointsBalanceMonetaryValue
4090Bubblehouse.pointsBalanceMonetaryValueStr = pointsBalanceMonetaryValueStr
4091
4092Bubblehouse.ONE_TIME_PURCHASE = 'onetime'
4093Bubblehouse.SUBSCRIPTION = 'sub'
4094
4095function centsToAccruedPoints(cents, scenario, options = {}) {
4096  if (scenario === 'onetime') {
4097    return Math.round(cents * Bubblehouse.data.ppc + 0.001)
4098  } else if (scenario === 'sub') {
4099    return Math.round(cents * Bubblehouse.data.ppcs + 0.001)
4100  } else {
4101    throw new Error('unsupported scenario')
4102  }
4103}
4104
4105Bubblehouse.PTS_TOTAL = 'total'
4106Bubblehouse.PTS_AVAILABLE = 'avail'
4107
4108function pointsBalance(kind) {
4109  if (kind === 'total') {
4110    return Bubblehouse.data.userPointBalance
4111  } else if (kind === 'avail') {
4112    return Bubblehouse.data.userPointBalanceAvail
4113  } else {
4114    throw new Error('unsupported kind')
4115  }
4116}
4117
4118function pointsBalanceStr(kind) {
4119  if (kind === 'total') {
4120    return Bubblehouse.data.userPointBalanceStr
4121  } else if (kind === 'avail') {
4122    return Bubblehouse.data.userPointBalanceAvailStr
4123  } else {
4124    throw new Error('unsupported kind')
4125  }
4126}
4127
4128function pointsBalanceMonetaryValue(kind) {
4129  if (kind === 'total') {
4130    return Bubblehouse.data.userPointBalanceValue
4131  } else if (kind === 'avail') {
4132    return Bubblehouse.data.userPointBalanceAvailValue
4133  } else {
4134    throw new Error('unsupported kind')
4135  }
4136}
4137
4138function pointsBalanceMonetaryValueStr(kind) {
4139  if (kind === 'total') {
4140    return Bubblehouse.data.userPointBalanceValueStr
4141  } else if (kind === 'avail') {
4142    return Bubblehouse.data.userPointBalanceAvailValueStr
4143  } else {
4144    throw new Error('unsupported kind')
4145  }
4146}
4147
4148
4149/* load.js */
4150document.dispatchEvent(new CustomEvent('bubblehouse:load'));
4151
4152
4153/* elements/embed-iframe.js */
4154class EmbedIframe extends HTMLElement {
4155    constructor() {
4156        super();
4157    }
4158
4159    connectedCallback() {
4160      const iframe = document.createElement("iframe");
4161
4162      this.getAttributeNames().map((an) => {
4163        iframe.setAttribute(an, this.getAttribute(an));
4164      });
4165
4166      const src = iframe.getAttribute("src");
4167      iframe.setAttribute("src", src.replace("$AUTH_TOKEN", Bubblehouse.data.authToken));
4168
4169      this.replaceWith(iframe);
4170    }
4171}
4172
4173customElements.define("bh-embed-iframe", EmbedIframe);
4174
4175
4176})();

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.