PageSourceSearch

https://www.comirnaty.com/lib/blocks/pharmacy-finder/pharmacy-finder.js

js comirnaty.com collected 2026-09-25 20:51:09 UTC 5,631 bytes, 157 lines download raw bytes

1import { FranklinBlock, getAvailableChildrenRow } from '../../scripts/lib-franklin.js';
2import { getFormsEnv, FORMS_API_ENDPOINT_PROD, FORMS_API_ENDPOINT_UAT } from '../../scripts/lib-franklin/forms-service.js';
3
4const CONFIG_TOKEN_PROD_DEFAULT = 'paxlovid-pharma-finder-generic-prod';
5const CONFIG_TOKEN_UAT_DEFAULT = 'paxlovid-pharma-finder-generic';
6
7const DEFAULT_REQUESTED_SERVICE = 'PharmacyFinder';
8const DEFAULT_HEADLINE = 'Find a Pharmacy';
9const CONTENT_HEIGHT_MESSAGE = 'CONTENT_HEIGHT_UPDATED';
10
11const isUat = () => getFormsEnv() !== 'production';
12
13/**
14 * Fetch the embed URL from the Pfizer Forms Service. The service authenticates
15 * with AssistRX server-side and returns the iframe URL for the finder tool.
16 *
17 * Both requests are sent to the same environment-resolved endpoint so the CSRF
18 * token primed by the GET stays valid for the POST (the shared token-substring
19 * endpoint selection does not recognise the `-prod` config-token suffix).
20 */
21const requestPharmacyFinder = async (token, requestedService) => {
22  const endpoint = isUat() ? FORMS_API_ENDPOINT_UAT : FORMS_API_ENDPOINT_PROD;
23  const headers = {
24    'Content-Type': 'application/json',
25    'x-config-token': token,
26  };
27
28  // Prime the CSRF token from the same endpoint the POST will use.
29  const primeResp = await fetch(endpoint, { method: 'GET', cache: 'no-cache', headers });
30  if (!primeResp.ok) {
31    throw new Error(`Pharmacy finder config request failed (${primeResp.status})`);
32  }
33  const primed = await primeResp.json();
34  const csrfToken = primed?.data?.csrfToken ?? null;
35
36  const resp = await fetch(endpoint, {
37    method: 'POST',
38    cache: 'no-cache',
39    headers,
40    body: JSON.stringify({
41      requestedServices: [requestedService],
42      csrfToken,
43    }),
44  });
45
46  if (!resp.ok) {
47    throw new Error(`Pharmacy finder request failed (${resp.status})`);
48  }
49
50  return resp.json();
51};
52
53export default class PharmacyFinder extends FranklinBlock {
54  constructor(blockName, block) {
55    super(blockName, block);
56    this.config = {};
57    this.onContentHeightMessage = null;
58  }
59
60  async beforeBlockDataRead() {
61    const read = (name) => {
62      const row = getAvailableChildrenRow(this.block, name);
63      return row ? row.textContent.trim() : null;
64    };
65
66    this.config = {
67      headline: read('headline'),
68      requestedService: read('requested_service') || DEFAULT_REQUESTED_SERVICE,
69      configTokenProd: read('config_token_prod') || CONFIG_TOKEN_PROD_DEFAULT,
70      configTokenUat: read('config_token_uat') || CONFIG_TOKEN_UAT_DEFAULT,
71    };
72  }
73
74  renderIframe(iframeSrc) {
75    const wrapper = document.createElement('div');
76    wrapper.classList.add('core-pharmacy-finder-iframe-wrapper');
77
78    const iframe = document.createElement('iframe');
79    iframe.classList.add('core-pharmacy-finder-iframe');
80    iframe.setAttribute('title', this.config.headline || DEFAULT_HEADLINE);
81    iframe.setAttribute('loading', 'lazy');
82    iframe.src = iframeSrc;
83
84    wrapper.append(iframe);
85    return { wrapper, iframe };
86  }
87
88  // The embedded tool reports its own height; mirror it onto the iframe so there
89  // is no inner scrollbar. The handler is stored so it can be removed and to
90  // avoid stacking duplicate listeners on re-render.
91  listenForContentHeight(iframe, iframeSrc) {
92    let iframeOrigin = null;
93    try {
94      iframeOrigin = new URL(iframeSrc, window.location.href).origin;
95    } catch {
96      iframeOrigin = null;
97    }
98
99    if (this.onContentHeightMessage) {
100      window.removeEventListener('message', this.onContentHeightMessage);
101    }
102
103    this.onContentHeightMessage = (event) => {
104      if (!iframeOrigin || event.origin !== iframeOrigin) return;
105      // If more than one finder shares the AssistRX origin on a page, only resize
106      // the iframe that actually sent the message.
107      if (event.source && iframe.contentWindow && event.source !== iframe.contentWindow) return;
108      const { messageType, height } = event.data || {};
109      if (messageType === CONTENT_HEIGHT_MESSAGE && Number.isFinite(height) && height > 0) {
110        iframe.style.height = `${height}px`;
111      }
112    };
113
114    window.addEventListener('message', this.onContentHeightMessage);
115  }
116
117  renderError(error) {
118    console.error('Error loading pharmacy finder', error);
119    this.block.classList.add('core-pharmacy-finder-error');
120
121    const message = document.createElement('p');
122    message.classList.add('core-pharmacy-finder-error-message');
123    message.setAttribute('role', 'alert');
124    message.textContent = 'The pharmacy finder is temporarily unavailable. Please try again later.';
125    this.block.append(message);
126  }
127
128  async afterBlockRender() {
129    this.block.textContent = '';
130
131    if (this.config.headline) {
132      const headline = document.createElement('h2');
133      headline.classList.add('core-pharmacy-finder-headline');
134      headline.textContent = this.config.headline;
135      this.block.append(headline);
136    }
137
138    try {
139      const token = isUat() ? this.config.configTokenUat : this.config.configTokenProd;
140      const response = await requestPharmacyFinder(token, this.config.requestedService);
141      const iframeSrc = response?.data?.[0]?.details?.url;
142
143      if (!iframeSrc) {
144        throw new Error('Pharmacy finder response did not include an iframe source');
145      }
146
147      const { wrapper, iframe } = this.renderIframe(iframeSrc);
148      this.block.append(wrapper);
149      this.listenForContentHeight(iframe, iframeSrc);
150    } catch (error) {
151      this.renderError(error);
152    }
153
154    this.block.classList.add('block-padding-standard');
155    this.block.classList.add('block-focus-visible');
156  }
157}

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.