PageSourceSearch

https://donate.sirgiving.org/widget-runtime.js

js sirgiving.org collected 2026-10-05 03:47:02 UTC 51,400 bytes, 1,211 lines download raw bytes

1(() => {
2  const script = document.currentScript;
3  const publicId = script?.dataset.widgetId;
4  const previewToken = script?.dataset.previewToken;
5  if (!script || (!publicId && !previewToken)) return;
6  const previewDevice =
7    previewToken && ['desktop', 'mobile'].includes(script.dataset.previewDevice)
8      ? script.dataset.previewDevice
9      : undefined;
10
11  const runtimeOrigin = new URL(script.src, window.location.href).origin;
12  const inferApiOrigin = (origin) => {
13    try {
14      const url = new URL(origin);
15      if (url.hostname === 'devdonate.sirgiving.org') {
16        url.hostname = 'devapi.sirgiving.org';
17        return url.origin;
18      }
19      if (url.hostname === 'donate.sirgiving.org') {
20        url.hostname = 'api.sirgiving.org';
21        return url.origin;
22      }
23      return origin;
24    } catch {
25      return origin;
26    }
27  };
28  const normalizeOrigin = (value) => {
29    if (!value) return undefined;
30    try {
31      return new URL(value, window.location.href).origin;
32    } catch {
33      return undefined;
34    }
35  };
36  const inferredApiOrigin = inferApiOrigin(runtimeOrigin);
37  const explicitApiOrigin = normalizeOrigin(script.dataset.apiOrigin);
38  const isRuntimeApiPairAllowed = (origin) => {
39    try {
40      const runtimeUrl = new URL(runtimeOrigin);
41      const apiUrl = new URL(origin);
42      if (runtimeUrl.hostname === 'devdonate.sirgiving.org') {
43        return apiUrl.origin === 'https://devapi.sirgiving.org';
44      }
45      if (runtimeUrl.hostname === 'donate.sirgiving.org') {
46        return apiUrl.origin === 'https://api.sirgiving.org';
47      }
48      return true;
49    } catch {
50      return false;
51    }
52  };
53  const apiOrigin = (() => {
54    if (explicitApiOrigin && isRuntimeApiPairAllowed(explicitApiOrigin)) {
55      return explicitApiOrigin;
56    }
57    if (explicitApiOrigin) {
58      console.warn(
59        '[SIR Giving Widget] Ignoring data-api-origin because it does not match the widget runtime environment.',
60        { runtimeOrigin, apiOrigin: explicitApiOrigin },
61      );
62    }
63    return inferredApiOrigin;
64  })().replace(/\/$/, '');
65  const api = publicId
66    ? `${apiOrigin}/api/widgets/${encodeURIComponent(publicId)}`
67    : `${apiOrigin}/api/widgets/preview`;
68  const instanceId = `sir-widget-${Math.random().toString(36).slice(2, 10)}`;
69  const isHostedIframe =
70    window.parent !== window && window.location.origin === runtimeOrigin;
71  let configuration;
72  let trigger;
73  let embedOrigin;
74  let rewardConfiguration;
75  let environment;
76  let donationCapabilities;
77  const everyOrgMethodLabels = {
78    card: 'Card',
79    bank: 'Bank transfer',
80    paypal: 'PayPal',
81    venmo: 'Venmo',
82    pay: 'Mobile pay',
83    crypto: 'Crypto',
84    stocks: 'Stocks',
85    daf: 'Donor-advised fund',
86    ira: 'IRA',
87    gift: 'Gift card',
88  };
89  const defer =
90    typeof queueMicrotask === 'function'
91      ? queueMicrotask
92      : (callback) => window.setTimeout(callback, 0);
93
94  const clearChildren = (element) => {
95    while (element.firstChild) {
96      element.removeChild(element.firstChild);
97    }
98  };
99
100  const removeNode = (element) => {
101    if (element && element.parentNode) {
102      element.parentNode.removeChild(element);
103    }
104  };
105
106  const dispatchWidgetClosed = () => {
107    let event;
108    if (typeof CustomEvent === 'function') {
109      event = new CustomEvent('sir-widget-closed', {
110        detail: { instanceId },
111      });
112    } else {
113      event = document.createEvent('CustomEvent');
114      event.initCustomEvent('sir-widget-closed', false, false, {
115        instanceId,
116      });
117    }
118    window.dispatchEvent(event);
119  };
120
121  const resolveAncestorOrigin = () => {
122    const ancestorOrigins = window.location.ancestorOrigins;
123    const ancestorOrigin = ancestorOrigins?.length
124      ? normalizeOrigin(ancestorOrigins[0])
125      : undefined;
126    if (ancestorOrigin) return ancestorOrigin;
127    return normalizeOrigin(document.referrer);
128  };
129
130  const resolveEmbeddingOrigin = () => {
131    if (previewToken) return undefined;
132    if (isHostedIframe) return resolveAncestorOrigin();
133    if (window.location.origin !== runtimeOrigin) return window.location.origin;
134    return undefined;
135  };
136
137  const resolveEmbedOrigin = (allowedOrigins) => {
138    const embeddingOrigin = resolveEmbeddingOrigin();
139    if (!embeddingOrigin) return { allowed: true, origin: undefined };
140    const normalizedAllowedOrigins = (allowedOrigins || [])
141      .map(normalizeOrigin)
142      .filter(Boolean);
143    return {
144      allowed: normalizedAllowedOrigins.includes(embeddingOrigin),
145      origin: embeddingOrigin,
146    };
147  };
148
149  const notifyVerifiedParent = () => {
150    if (!isHostedIframe || !embedOrigin) return;
151    window.parent.postMessage(
152      { type: 'SIR_WIDGET_READY', instanceId },
153      embedOrigin,
154    );
155  };
156
157  window.addEventListener('message', (event) => {
158    if (
159      !embedOrigin ||
160      event.source !== window.parent ||
161      event.origin !== embedOrigin ||
162      event.data?.type !== 'SIR_WIDGET_PARENT_READY'
163    ) return;
164    notifyVerifiedParent();
165  });
166
167  const money = (amount) => {
168    const roundedAmount = Math.round(Number(amount));
169    const hasFractionalAmount = Math.abs(roundedAmount % 100) !== 0;
170
171    return new Intl.NumberFormat('en-US', {
172      style: 'currency',
173      currency: configuration.amounts.currency,
174      minimumFractionDigits: hasFractionalAmount ? 2 : 0,
175      maximumFractionDigits: 2,
176    }).format(roundedAmount / 100);
177  };
178
179  const normalizeThemeColor = (value) =>
180    /^#[0-9a-f]{6}$/i.test(value || '') ? value : '#049479';
181
182  const readableTextColor = (hex) => {
183    const luminance = (value) => {
184      const channels = value
185        .slice(1)
186        .match(/.{2}/g)
187        .map((channel) =>
187 Number.parseInt(channel, 16) / 255)
188        .map((channel) =>
189          channel <= 0.03928
190            ? channel / 12.92
191            : ((channel + 0.055) / 1.055) ** 2.4,
192        );
193      return (
194        channels[0] * 0.2126 +
195        channels[1] * 0.7152 +
196        channels[2] * 0.0722
197      );
198    };
199    const backgroundLuminance = luminance(hex);
200    const darkText = '#17302d';
201    const lightText = '#f9fffd';
202    const darkContrast =
203      (backgroundLuminance + 0.05) / (luminance(darkText) + 0.05);
204    const lightContrast =
205      (luminance(lightText) + 0.05) / (backgroundLuminance + 0.05);
206    return darkContrast >= lightContrast ? darkText : lightText;
207  };
208
209  const setStatus = (element, message) => {
210    element.textContent = message;
211    element.hidden = !message;
212  };
213
214  const isVerifiedStripeEnvironment = (value) =>
215    value?.stripeEnabled === true &&
216    ((value.paymentEnvironment === 'test' &&
217      value.tokenEnvironment === 'production') ||
218      (value.paymentEnvironment === 'live' &&
219        value.tokenEnvironment === 'production'));
220
221  const stripeEnvironmentLabel = (value) => {
222    if (!isVerifiedStripeEnvironment(value)) return 'Stripe unavailable';
223    return value.paymentEnvironment === 'test'
224      ? 'Stripe test mode · Development SIR tokens · No live Stripe funds are affected'
225      : 'Live payment · Production SIR rewards';
226  };
227
228  const donationFailureMessage = (failure, statusCode) => {
229    const messages = {
230      WIDGET_SESSION_INVALID:
231        'Your secure donation session expired. Close and reopen this donation window to continue.',
232      WIDGET_ORIGIN_NOT_ALLOWED:
233        'This donation button is not approved for this website.',
234      WIDGET_AMOUNT_INVALID: 'Choose a valid donation amount.',
235      WIDGET_AMOUNT_NOT_ALLOWED:
236        'Choose one of the suggested donation amounts.',
237      WIDGET_AMOUNT_TOO_LOW: 'Choose a larger donation amount.',
238      WIDGET_AMOUNT_TOO_HIGH: 'Choose a smaller donation amount.',
239      WIDGET_PAYMENT_METHOD_NOT_ALLOWED:
240        'That payment method is not available. Choose another way to give.',
241      WIDGET_STRIPE_NOT_READY:
242        'Stripe donations are not available for this nonprofit yet. Choose another payment option or contact the nonprofit.',
243      STRIPE_DISABLED:
244        'Stripe donations are disabled in this environment.',
245      STRIPE_MODE_MISMATCH:
246        'The secure payment environment could not be verified.',
247    };
248    if (failure?.code && messages[failure.code]) return messages[failure.code];
249    if (statusCode >= 400 && statusCode < 500 && failure?.message) {
250      return String(failure.message);
251    }
252    return 'Secure checkout is temporarily unavailable. Please try again.';
253  };
254
255  const readConfigurationDocument = (value) => value?._doc || value || {};
256
257  const calculateReward = (amount) => {
258    if (!rewardConfiguration || !amount) return 0;
259    const tokenPrice = Number(rewardConfiguration.tokenPrice) || 0;
260    if (tokenPrice <= 0) return 0;
261    const status = configuration.organization?.status || 'UNTAGGED';
262    const split =
263      status === 'FEATURED'
264        ? rewardConfiguration.rewardSplitFeatured
265        : status === 'MAIN'
266          ? rewardConfiguration.rewardSplitMain
267          : rewardConfiguration.rewardSplitUntagged;
268    const percentage = Number(split?.donorDafPercentage) || 0;
269    return (amount / 100) * (percentage / 100) / tokenPrice;
270  };
271
272  const formatReward = (value) =>
273    new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(value);
274
275  const startDonation = async ({
276    status,
277    amount,
278    email,
279    paymentMethod,
280    frequency,
281    referralCode,
282    firstName,
283    lastName,
284    submit,
285    checkoutButtonLabel,
286  }) => {
287    if (!email.value || !email.validity.valid) {
288      setStatus(status, 'Enter a valid email address to continue.');
289      email.focus();
290      return;
291    }
292
293    submit.disabled = true;
294    submit.textContent = 'Opening secure checkout…';
295    setStatus(status, '');
296
297    try {
298      let sessionToken;
299      const hostedIframeOrigin = isHostedIframe ? embedOrigin : undefined;
300      if (!previewToken) {
301        if (isHostedIframe && !embedOrigin) {
302          setStatus(status, 'This widget must be opened from an approved website.');
303          return;
304        }
305        const sessionResponse = await fetch(`${api}/sessions`, {
306          method: 'POST',
307          headers: { 'Content-Type': 'application/json' },
308          body: hostedIframeOrigin
309            ? JSON.stringify({ embedOrigin: hostedIframeOrigin })
310            : undefined,
311        });
312        if (!sessionResponse.ok) {
313          setStatus(status, 'This widget is not approved for this website.');
314          return;
315        }
316        ({ sessionToken } = await sessionResponse.json());
317      }
318
319      const response = await fetch(`${api}/donations`, {
320        method: 'POST',
321        headers: { 'Content-Type': 'application/json' },
322        body: JSON.stringify({
323          ...(previewToken ? { token: previewToken } : { sessionToken }),
324          amount,
325          ...(frequency === 'monthly' ? { frequency } : {}),
326          donorEmail: email.value.trim(),
327          ...(paymentMethod ? { paymentMethod } : {}),
328          ...(firstName?.value.trim()
329            ? { firstName: firstName.value.trim() }
330            : {}),
331          ...(lastName?.value.trim()
332            ? { lastName: lastName.value.trim() }
333            : {}),
334          ...(referralCode.trim() ? { referralCode: referralCode.trim() } : {}),
335          ...(hostedIframeOrigin ? { embedOrigin: hostedIframeOrigin } : {}),
336        }),
337      });
338      if (!response.ok) {
339        const failure = await response.json().catch(() => undefined);
340        setStatus(status, donationFailureMessage(failure, response.status));
341        return;
342      }
343      const result = await response.json();
344      if (
345        configuration.provider === 'STRIPE' &&
346        (!isVerifiedStripeEnvironment(environment) ||
347          result.paymentEnvironment !== environment.paymentEnvironment ||
348          result.tokenEnvironment !== environment.tokenEnvironment)
349      ) {
350        setStatus(
351          status,
352          'The secure payment environment could not be verified. Please try again later.',
353        );
354        return;
355      }
356      const { redirectUrl } = result;
357      // Embedded and hosted widgets share this runtime. The donor must leave
358      // the partner page for the server-generated Every.org checkout, not
359      // navigate only the hosted iframe.
360      if (window.top) window.top.location.assign(redirectUrl);
361      else window.location.assign(redirectUrl);
362    } catch {
363      setStatus(
364        status,
365        'We could not reach secure checkout. Check your connection and try again.',
366      );
367    } finally {
368      submit.disabled = false;
369      submit.textContent =
370        amount > 0
371          ? `${checkoutButtonLabel || 'Donate'} ${money(amount)}${frequency === 'monthly' ? ' monthly' : ''}`
372          : `${checkoutButtonLabel || 'Donate'} with Every.org`;
373    }
374  };
375
376  const openDialog = () => {
377    const buttonColor = normalizeThemeColor(
378      configuration.appearance?.buttonColor,
379    );
380    const buttonTextColor = readableTextColor(buttonColor);
381    const presets = (configuration.amounts.presets || [])
382      .map(Number)
383      .filter((amount) => Number.isInteger(amount) && amount > 0);
384    const configuredDefault = Number(configuration.amounts.defaultAmount);
385    let selectedAmount = presets.includes(configuredDefault)
386      ? configuredDefault
387      : presets[0] || 10000;
388    const everyOrgSettings = configuration.everyOrgSettings || {};
389    const paymentMethods =
390      configuration.provider === 'EVERY_ORG'
391        ? Array.isArray(everyOrgSettings.allowedMethods) &&
392          everyOrgSettings.allowedMethods.length
393          ? everyOrgSettings.allowedMethods
394          : ['card', 'stocks']
395        : Array.isArray(configuration.paymentMethods)
396          ? configuration.paymentMethods
397          : [];
398    let selectedPaymentMethod =
399      configuration.provider === 'EVERY_ORG' &&
400      paymentMethods.includes(everyOrgSettings.defaultMethod)
401        ? everyOrgSettings.defaultMethod
402        : paymentMethods[0];
403    const checkoutButtonLabel =
404      configuration.provider === 'EVERY_ORG'
405        ? everyOrgSettings.checkoutButtonLabel || 'Donate'
406        : 'Donate';
407    let frequency = 'once';
408    // Temporary UI release hold; keep donation-frequency.tsx in sync when releasing.
409    const monthlyGivingComingSoon = true;
410    // Matches SHOW_MONTHLY_DONATION_UI in the React application.
411    const showMonthlyDonationUi = false;
412    const monthlyAvailable = () => showMonthlyDonationUi && !monthlyGivingComingSoon && configuration.provider === 'EVERY_ORG' && donationCapabilities?.everyOrg?.monthlyEnabled === true && donationCapabilities.everyOrg.monthlyMethods?.includes(selectedPaymentMethod);
413    let referralCode = configuration.attribution?.defaultReferralCode || '';
414
415    const dialog = document.createElement('dialog');
416    const supportsNativeDialog =
417      typeof dialog.showModal === 'function' &&
418      typeof dialog.close === 'function';
419    dialog.id = instanceId;
420    dialog.dataset.testid = 'widget-runtime-dialog';
421    dialog.dataset.widgetInstance = instanceId;
422    dialog.setAttribute('aria-labelledby', `${instanceId}-title`);
423    dialog.setAttribute('aria-modal', 'true');
424    if (!supportsNativeDialog) {
425      dialog.className = 'sir-widget-fallback-dialog';
426      dialog.setAttribute('role', 'dialog');
427    }
428    if (previewDevice) {
429      dialog.classList.add(`sir-preview-${previewDevice}`);
430      dialog.dataset.previewDevice = previewDevice;
431    }
432    dialog.style.cssText =
433      `width:680px;max-width:calc(100vw - 32px);width:min(680px,calc(100vw - 32px));height:auto;max-height:calc(100vh - 48px);max-height:min(720px,calc(100dvh - 48px));padding:0;border:1px solid #dceae6;border-radius:24px;color:#172e2d;background:#f9fffd;box-shadow:0 24px 64px rgba(15,42,40,.22);overflow:hidden;z-index:2147483647`;
434
435    const compactDialogStyles = (selector) => `
436      ${selector}{max-height:calc(100vh - 20px)!important;max-height:calc(100dvh - 20px)!important;border-radius:18px!important}
437      ${selector} .sir-panel{max-height:calc(100vh - 20px)!important;max-height:calc(100dvh - 20px)!important}
438      ${selector} .sir-header{gap:10px!important;padding:12px 16px!important}
439      ${selector} .sir-identity{width:42px!important;height:42px!important;border-radius:10px!important}
440      ${selector} .sir-eyebrow{display:none!important}
441      ${selector} .sir-title{font-size:17px!important;line-height:1.18!important;letter-spacing:-.005em!important}
442      ${selector} .sir-location{margin-top:2px!important;font-size:12px!important;line-height:1.3!important}
443      ${selector} .sir-body{padding:16px!important}
444      ${selector} .sir-email{gap:6px!important}
445      ${selector} .sir-field{min-height:48px!important;padding-right:12px!important;padding-left:12px!important}
446      ${selector} .sir-name-fields{gap:8px!important;margin-top:12px!important}
447      ${selector} .sir-methods{margin-top:16px!important}
448      ${selector} .sir-method-heading{margin-bottom:8px!important}
449      ${selector} .sir-method-choices{gap:8px!important;padding:0 0 4px!important;scrollbar-width:none!important}
450      ${selector} .sir-method{min-height:44px!important;padding:0 10px!important;font-size:15px!important}
451      ${selector} .sir-amount-heading{margin-top:20px!important}
452      ${selector} .sir-amount-help{margin-bottom:10px!important}
453      ${selector} .sir-amounts{gap:8px!important}
454      ${selector} .sir-amount{min-height:48px!important;padding:0 6px!important}
455      ${selector} .sir-custom{gap:6px!important;margin-top:10px!important}
456      ${selector} .sir-reward{margin-top:12px!important;padding:12px!important;font-size:13px!important}
457      ${selector} .sir-referral{margin-top:14px!important}
458      ${selector} .sir-action-tray{padding:10px 16px 0!important}
459      ${selector} .sir-primary{min-height:52px!important}
460      ${selector} .sir-status{margin-top:10px!important}
461      ${selector} .sir-trust-footer{column-gap:8px!important;row-gap:2px!important;margin:8px -16px 0!important;padding:6px 12px max(7px,env(safe-area-inset-bottom))!important;font-size:10px!important}
462      ${selector} .sir-trust-item{min-height:20px!important;gap:4px!important}
463      ${selector} .sir-trust-logo{max-height:24px!important}
464    `;
465    const narrowDialogStyles = (selector, maximumWidth) => `
466      ${selector}{width:calc(100vw - 20px)!important;max-width:${maximumWidth}!important}
467      ${selector} .sir-trust-divider{display:none!important}
468    `;
469
470    const styles = document.createElement('style');
471    styles.textContent = `
472      #${instanceId}{position:fixed!important;top:50%!important;right:auto!important;bottom:auto!important;left:50%!important;transform:translate(-50%,-50%)!important;margin:0!important;width:680px!important;width:min(680px,calc(100vw - 32px))!important;height:auto!important;max-width:calc(100vw - 32px)!important;max-height:calc(100vh - 48px)!important;max-height:min(720px,calc(100dvh - 48px))!important;z-index:2147483647!important;background:#f9fffd!important;background:color-mix(in srgb,${buttonColor} 4%,#f9fffd)!important;border-color:#dceae6!important;border-color:color-mix(in srgb,${buttonColor} 24%,#dceae6)!important}
473      #${instanceId}.sir-widget-fallback-dialog{display:block!important}
474      #${instanceId}::backdrop{background:color-mix(in srgb,${buttonColor} 12%,rgba(20,40,39,.68))}
475      #${instanceId},#${instanceId} *{box-sizing:border-box}
476      #${instanceId} button,#${instanceId} input,#${instanceId} select{font:inherit}
477      #${instanceId} button:focus-visible,#${instanceId} input:focus-visible,#${instanceId} select:focus-visible{outline:3px solid ${buttonColor};outline-offset:2px}
478      #${instanceId} .sir-panel{max-height:calc(100vh - 48px)!important;max-height:min(720px,calc(100dvh - 48px))!important;display:flex!important;flex-direction:column!important}
479      #${instanceId} .sir-header{display:flex!important;align-items:center!important}
480      #${instanceId}
480 .sir-body{display:block!important;min-height:0!important;overflow-y:auto!important}
481      #${instanceId} .sir-action-tray{display:block!important;flex:0 0 auto!important}
482      #${instanceId} .sir-amounts{display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr))!important}
483      #${instanceId} .sir-amount[aria-pressed="true"]{background:${buttonColor}!important;border-color:${buttonColor}!important;color:${buttonTextColor}!important;box-shadow:0 8px 18px color-mix(in srgb,${buttonColor} 20%,transparent)}
484      #${instanceId} .sir-amount:not([aria-pressed="true"]):hover{border-color:${buttonColor};background:color-mix(in srgb,${buttonColor} 7%,transparent)}
485      #${instanceId} .sir-field{border-color:color-mix(in srgb,${buttonColor} 22%,#cfdfdb)!important;background:color-mix(in srgb,${buttonColor} 2%,#fbfffe)!important}
486      #${instanceId} .sir-field:hover{border-color:color-mix(in srgb,${buttonColor} 45%,#cfdfdb)!important}
487      #${instanceId} .sir-primary{background:${buttonColor}!important;color:${buttonTextColor}!important;box-shadow:0 10px 24px color-mix(in srgb,${buttonColor} 24%,transparent)}
488      #${instanceId} .sir-primary:hover{filter:brightness(.94)}
489      ${narrowDialogStyles(`#${instanceId}.sir-preview-mobile`, '390px')}
490      ${compactDialogStyles(`#${instanceId}.sir-preview-mobile`)}
491      @media(max-width:640px){
492        ${narrowDialogStyles(`#${instanceId}`, 'calc(100vw - 20px)')}
493        ${compactDialogStyles(`#${instanceId}`)}
494      }
495      @media(min-width:641px) and (max-width:932px) and (max-height:500px){
496        ${compactDialogStyles(`#${instanceId}`)}
497      }
498      @media(max-width:360px){
499        #${instanceId} .sir-name-fields{grid-template-columns:1fr!important}
500      }
501      @media(max-width:420px){
502        #${instanceId} .sir-trust-divider{display:none}
503      }
504    `;
505
506    const panel = document.createElement('div');
507    panel.className = 'sir-panel';
508    panel.style.cssText =
509      'max-height:calc(100vh - 48px);max-height:min(720px,calc(100dvh - 48px));display:flex;flex-direction:column;font:16px/1.45 "Public Sans","Segoe UI",system-ui,sans-serif';
510
511    const header = document.createElement('header');
512    header.className = 'sir-header';
513    header.style.cssText =
514      `display:flex;align-items:center;gap:16px;flex:0 0 auto;padding:24px 28px;background:color-mix(in srgb,${buttonColor} 10%,#f5fffc);border-bottom:1px solid color-mix(in srgb,${buttonColor} 20%,#d9e9e5)`;
515
516    const identity = document.createElement('div');
517    identity.className = 'sir-identity';
518    identity.style.cssText =
519      `width:54px;height:54px;display:grid;place-items:center;flex:0 0 auto;border-radius:14px;background:#fff;border:1px solid color-mix(in srgb,${buttonColor} 20%,#d3e9e4);overflow:hidden;color:${buttonColor};font-weight:800`;
520    const logoUrl =
521      configuration.organization?.logoUrl || configuration.content.logoUrl;
522    if (logoUrl) {
523      const logo = document.createElement('img');
524      logo.src = logoUrl;
525      logo.alt = '';
526      logo.style.cssText = 'width:100%;height:100%;object-fit:contain';
527      identity.append(logo);
528    } else {
529      identity.textContent = (
530        configuration.organization?.name ||
531        configuration.content.nonprofitName ||
532        configuration.content.title ||
533        'D'
534      )
535        .slice(0, 1)
536        .toUpperCase();
537    }
538
539    const headerCopy = document.createElement('div');
540    headerCopy.className = 'sir-header-copy';
541    headerCopy.style.cssText = 'min-width:0;flex:1';
542    const eyebrow = document.createElement('p');
543    eyebrow.className = 'sir-eyebrow';
544    eyebrow.textContent = 'YOUR GIFT SUPPORTS';
545    eyebrow.style.cssText =
546      `margin:0 0 3px;color:${buttonColor};font-size:11px;line-height:1.2;font-weight:800;letter-spacing:.08em`;
547    const title = document.createElement('h2');
548    title.className = 'sir-title';
549    title.id = `${instanceId}-title`;
550    title.textContent =
551      configuration.organization?.name ||
552      configuration.content.nonprofitName ||
553      configuration.content.title;
554    title.style.cssText =
555      'margin:0;font-size:21px;line-height:1.2;font-weight:700;letter-spacing:-.01em';
556    const location = document.createElement('p');
557    location.className = 'sir-location';
558    location.textContent =
559      configuration.organization?.location ||
560      configuration.content.description ||
561      '';
562    location.style.cssText =
563      'margin:5px 0 0;color:#5c7471;font-size:14px;line-height:1.35';
564    headerCopy.append(eyebrow, title);
565    if (location.textContent) headerCopy.append(location);
566
567    const close = document.createElement('button');
568    close.className = 'sir-close';
569    close.type = 'button';
570    close.textContent = '×';
571    close.setAttribute('aria-label', 'Close donation');
572    close.style.cssText =
573      'width:44px;height:44px;display:grid;place-items:center;flex:0 0 auto;border:0;border-radius:50%;background:transparent;color:#395452;font-size:27px;line-height:1;cursor:pointer';
574    header.append(identity, headerCopy, close);
575
576    const body = document.createElement('div');
577    body.className = 'sir-body';
578    body.style.cssText =
579      'min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:28px';
580
581    const environmentNotice = document.createElement('p');
582    environmentNotice.dataset.testid = 'widget-runtime-environment';
583    environmentNotice.textContent =
584      configuration.provider === 'STRIPE'
585        ? stripeEnvironmentLabel(environment)
586        : '';
587    environmentNotice.hidden = configuration.provider !== 'STRIPE';
588    environmentNotice.style.cssText =
589      `margin:0 0 18px;padding:8px 10px;border-radius:8px;background:color-mix(in srgb,${buttonColor} 8%,#f4fbf9);color:#486763;font-size:12px;font-weight:700;text-align:center`;
590
591    const emailLabel = document.createElement('label');
592    emailLabel.className = 'sir-email';
593    emailLabel.textContent = 'Email for your receipt';
594    emailLabel.style.cssText =
595      'display:grid;gap:8px;font-weight:600;color:#243f3d';
596    const email = document.createElement('input');
597    email.type = 'email';
598    email.autocomplete = 'email';
599    email.placeholder = '[email protected]';
600    email.setAttribute('aria-label', 'Email for your donation');
601    email.required = true;
602    email.className = 'sir-field';
603    email.style.cssText =
604      'min-height:56px;border:1px solid #c5e1dc;border-radius:10px;padding:0 15px;background:transparent;color:#172e2d';
605    emailLabel.append(email);
606
607    let nameFields;
608    let firstName;
609    let lastName;
610    if (
611      configuration.provider === 'EVERY_ORG' &&
612      everyOrgSettings.collectOptionalNames
613    ) {
614      nameFields = document.createElement('div');
615      nameFields.className = 'sir-name-fields';
616      nameFields.style.cssText =
617        'display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:14px';
618      firstName = document.createElement('input');
619      firstName.type = 'text';
620      firstName.autocomplete = 'given-name';
621      firstName.placeholder = 'First name (optional)';
622      firstName.setAttribute('aria-label', 'First name (optional)');
623      firstName.className = 'sir-field';
624      lastName = document.createElement('input');
625      lastName.type = 'text';
626      lastName.autocomplete = 'family-name';
627      lastName.placeholder = 'Last name (optional)';
628      lastName.setAttribute('aria-label', 'Last name (optional)');
629      lastName.className = 'sir-field';
630      [firstName, lastName].forEach((field) => {
631        field.maxLength = 100;
632        field.style.cssText =
633          'width:100%;min-height:52px;border:1px solid #c5e1dc;border-radius:10px;padding:0 15px;background:transparent;color:#172e2d';
634      });
635      nameFields.append(firstName, lastName);
636    }
637
638    const amountHeading = document.createElement('p');
639    amountHeading.className = 'sir-amount-heading';
640    amountHeading.textContent = 'Choose your gift';
641    amountHeading.style.cssText =
642      'margin:28px 0 2px;font-size:17px;font-weight:750;letter-spacing:-.01em';
643
644    const amountHelp = document.createElement('p');
645    amountHelp.className = 'sir-amount-help';
646    amountHelp.textContent = 'Select an amount or enter your own.';
647    amountHelp.style.cssText =
648      'margin:0 0 12px;color:#607773;font-size:13px;line-height:1.45';
649
650    const amounts = document.createElement('div');
651    amounts.className = 'sir-amounts';
652    amounts.style.cssText =
653      'display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px';
654
655    const amountButtons = new Map();
656    const customInput = document.createElement('input');
657    const submit = document.createElement('button');
658    const reward = document.createElement('div');
659    const renderReward = (message) => {
660      clearChildren(reward);
661      if (!message) return;
662      const giftIcon = document.createElementNS(
663        'http://www.w3.org/2000/svg',
664        'svg',
665      );
666      giftIcon.setAttribute('viewBox', '0 0 24 24');
667      giftIcon.setAttribute('width', '19');
668      giftIcon.setAttribute('height', '19');
669      giftIcon.setAttribute('aria-hidden', 'true');
670      giftIcon.dataset.testid = 'widget-runtime-reward-gift';
671      giftIcon.style.cssText = 'flex:0 0 auto;fill:currentColor';
672      const giftPath = document.createElementNS(
673        'http://www.w3.org/2000/svg',
674        'path',
675      );
676      giftPath.setAttribute(
677        'd',
678        'M20 6h-2.18A3 3 0 0 0 12 4.5 3 3 0 0 0 6.18 6H4a2 2 0 0 0-2 2v3h9V8h2v3h9V8a2 2 0 0 0-2-2M9 6a1 1 0 1 1 0-2c1 0 2 2 2 2H9m6 0h-2s1-2 2-2a1 1 0 1 1 0 2M2 20a2 2 0 0 0 2 2h7v-9H2v7m11 2h7a2 2 0 0 0 2-2v-7h-9v9Z',
679      );
680      giftIcon.append(giftPath);
681      const copy = document.createElement('span');
682      copy.textContent = message;
683      reward.append(giftIcon, copy);
684    };
685
686    const refreshSelection = () => {
687      amountButtons.forEach((button, amount) => {
688        button.setAttribute('aria-pressed', String(amount === selectedAmount));
689      });
690      customInput.value = amountButtons.has(selectedAmount)
691        ? ''
692        : selectedAmount > 0
693          ? String(selectedAmount / 100)
694          : '';
695      submit.textContent =
696        selectedAmount > 0
697          ? `${checkoutButtonLabel} ${money(selectedAmount)}${frequency === 'monthly' ? ' monthly' : ''}`
698          : `${checkoutButtonLabel} with Every.org`;
699      const rewardAmount = calculateReward(selectedAmount);
700      reward.hidden = rewardAmount <= 0;
701      renderReward(
702        rewardAmount > 0
703          ? selectedPaymentMethod === 'stocks'
704            ? `Estimated reward: ${formatReward(rewardAmount)} SIR. The final reward uses Every.org’s settled sale value.`
705            : `Estimated reward: ${formatReward(rewardAmount)} SIR${frequency === 'monthly' ? ' per confirmed payment' : ' for future giving'}.`
706          : '',
707      );
708    };
709
710    presets.forEach((amount) => {
711      const choice = document.createElement('button');
712      choice.type = 'button';
713      choice.className = 'sir-amount';
714      choice.textContent = money(amount);
715      choice.setAttribute('aria-pressed', 'false');
716      choice.style.cssText =
717        'min-height:52px;border:1px solid #bfddd7;border-radius:10px;background:transparent;color:#172e2d;padding:0 10px;font-weight:700;cursor:pointer';
718      choice.addEventListener('click', () => {
719        selectedAmount = amount;
720        refreshSelection();
721      });
722      amountButtons.set(amount, choice);
723      amounts.append(choice);
724    });
725
726    const customLabel = document.createElement('label');
727    const refreshAmountCopy = () => {
728      const isStock = selectedPaymentMethod === 'stocks';
729      amountHeading.textContent = isStock
730        ? 'Estimated donation value'
731        : 'Choose your gift';
732      amountHelp.textContent = isStock
733        ? 'Optional. The final sold value reported by Every.org is authoritative.'
734        : 'Select an amount or enter your own.';
735      customLabel.firstChild.textContent = isStock
736        ? 'Estimated value (optional)'
737        : 'Or enter a different amount';
738      customInput.placeholder = isStock
739        ? 'Leave blank if unknown'
740        : 'Enter an amount';
741      customInput.min = String(
742        isStock ? 0 : (configuration.amounts.minimumAmount || 1) / 100,
743      );
744    };
745
746    customLabel.textContent = 'Or enter a different amount';
747    customLabel.className = 'sir-custom';
748    customLabel.style.cssText =
749      'display:grid;gap:8px;margin-top:12px;color:#48615e;font-size:13px';
750    customInput.type = 'number';
751    customInput.min = String(
752      (configuration.amounts.minimumAmount || 1) / 100,
753    );
754    customInput.max = configuration.amounts.maximumAmount
755      ? String(configuration.amounts.maximumAmount / 100)
756      : '';
757    customInput.step = '0.01';
758    customInput.inputMode = 'decimal';
759    customInput.className = 'sir-field';
760    customInput.placeholder = 'Enter an amount';
761    customInput.disabled = configuration.amounts.customAmountEnabled === false;
762    customInput.style.cssText =
763      'min-height:56px;border:1px solid #c5e1dc;border-radius:10px;padding:0 15px;background:transparent;color:#172e2d';
764    customInput.addEventListener('input', () => {
765      if (
766        customInput.value === '' &&
767        selectedPaymentMethod === 'stocks'
768      ) {
769        selectedAmount = 0;
770        refreshSelection();
771        return;
772      }
773      const amount = Math.round(Number(customInput.value) * 100);
774      if (Number.isInteger(amount) && amount > 0) {
775        selectedAmount = amount;
776        amountButtons.forEach((button) =>
777          button.setAttribute('aria-pressed', 'false'),
778        );
779        submit.textContent = `${checkoutButtonLabel} ${money(selectedAmount)}${frequency === 'monthly' ? ' monthly' : ''}`;
780        const rewardAmount = calculateReward(selectedAmount);
781        reward.hidden = rewardAmount <= 0;
782        renderReward(
783          rewardAmount > 0
784            ? selectedPaymentMethod === 'stocks'
785              ? `Estimated reward: ${formatReward(rewardAmount)} SIR. The final reward uses Every.org’s settled sale value.`
786              : `Estimated reward: ${formatReward(rewardAmount)} SIR${frequency === 'monthly' ? ' per confirmed payment' : ' for future giving'}.`
787            : '',
788        );
789      }
790    });
791    customLabel.append(customInput);
792
793    reward.style.cssText =
794      `margin-top:18px;padding:14px;border:1px solid color-mix(in srgb,${buttonColor} 25%,#cce8de);border-radius:10px;background:color-mix(in srgb,${buttonColor} 7%,#f6fffb);color:color-mix(in srgb,${buttonColor} 82%,#17302d);display:flex;align-items:center;justify-content:center;gap:7px;text-align:center;font-size:14px;font-weight:600`;
795    reward.hidden = true;
796    reward.className = 'sir-reward';
797
798    const frequencyGroup = document.createElement('fieldset');
799    frequencyGroup.style.cssText = 'border:0;padding:0;margin:20px 0 0';
800    const frequencyHeading = document.createElement('legend');
801    frequencyHeading.textContent = 'Donation frequency';
802    frequencyHeading.style.cssText = 'font-weight:700;margin-bottom:8px';
803    const frequencyButtons = new Map();
804    const frequencyHelp = document.createElement('p');
805    frequencyHelp.style.cssText = 'font-size:16px;line-height:1.5;margin:10px 0 0';
806    frequencyHelp.textContent = 'Give this amount each month. Change or cancel on Every.org. Rewards are calculated for each confirmed payment.';
807    const refreshFrequency = () => {
808      if (!monthlyAvailable()) frequency = 'once';
809      frequencyGroup.hidden = !showMonthlyDonationUi || (!monthlyGivingComingSoon && !monthlyAvailable());
810      frequencyHelp.hidden = frequency !== 'monthly';
811      frequencyButtons.forEach((button, value) => {
812        button.disabled = value === 'monthly' && !monthlyAvailable();
813        button.style.cursor = button.disabled ? 'not-allowed' : 'pointer';
814        button.setAttribute('aria-pressed', String(value === frequency));
815        button.style.background = value === frequency ? buttonColor : 'transparent';
816        button.style.color = value === frequency ? buttonTextColor : '#172e2d';
817        if (button.disabled) button.style.color = '#63716e';
818      });
819    };
820    frequencyGroup.append(frequencyHeading);
821    ['once', 'monthly'].forEach((value) => {
822      const button = document.createElement('button');
823      button.type = 'button';
824      button.textContent = value === 'monthly' ? 'Monthly' : 'One-time';
825      if (value === 'monthly' && monthlyGivingComingSoon) {
826        const soon = document.createElement('span');
827        soon.textContent = 'Soon';
828        soon.style.cssText = 'position:absolute;top:-6px;right:6px;padding:2px 4px;border-radius:4px;background:rgba(255,172,1,0.1);color:#ffac01;font-size:0.6rem;font-weight:600;line-height:1;text-transform:uppercase';
829        button.append(' ', soon);
830      }
831      button.style.cssText = 'position:relative;min-height:44px;width:50%;border:1px solid #bfddd7;border-radius:8px;font-size:16px;cursor:pointer';
832      button.addEventListener('click', () => {
833        if (value === 'monthly' && !monthlyAvailable()) return;
834        frequency = value;
835        refreshFrequency();
836        refreshSelection();
837      });
838      frequencyButtons.set(value, button);
839      frequencyGroup.append(button);
840    });
841    frequencyGroup.append(frequencyHelp);
842    refreshFrequency();
843
844    let paymentMethodControl;
845    if (paymentMethods.length > 1) {
846      const methodGroup = document.createElement('div');
847      methodGroup.className = 'sir-methods';
848      methodGroup.style.cssText = 'margin-top:20px';
849      const methodHeading = document.createElement('p');
850      methodHeading.className = 'sir-method-heading';
851      methodHeading.textContent = 'Choose how to give';
852      methodHeading.style.cssText =
853        'margin:0 0 10px;font-weight:700;color:#243f3d';
854      const choices = document.createElement('div');
855      choices.className = 'sir-method-choices';
856      choices.style.cssText =
857        'display:flex;flex-wrap:nowrap;gap:8px;overflow-x:auto;padding:2px 2px 6px;scrollbar-width:thin';
858      const methodButtons = new Map();
859      paymentMethods.forEach((value) => {
860        const choice = document.createElement('button');
861        choice.type = 'button';
862        choice.className = 'sir-method';
863        choice.textContent =
864          configuration.provider === 'EVERY_ORG'
865            ? everyOrgMethodLabels[value] || value
866            : value === 'us_bank_account'
867              ? 'US bank account'
868              : 'Card';
869        choice.style.cssText =
870          'min-height:42px;flex:0 0 auto;border:1px solid #bfddd7;border-radius:9px;background:transparent;color:#172e2d;padding:0 14px;font-weight:650;cursor:pointer';
871        const refreshMethods = () =>
872          methodButtons.forEach((button, method) => {
873            const selected = method === selectedPaymentMethod;
874            button.style.background = selected ? buttonColor : 'transparent';
875            button.style.borderColor = selected ? buttonColor : '#bfddd7';
876            button.style.color = selected ? buttonTextColor : '#172e2d';
877            button.setAttribute('aria-pressed', String(selected));
878          });
879        choice.addEventListener('click', () => {
880          selectedPaymentMethod = value;
881          refreshFrequency();
882          refreshMethods();
883          refreshAmountCopy();
884          refreshSelection();
885          if (!customInput.disabled) {
886            requestAnimationFrame(() => {
887              customInput.focus({ preventScroll: false });
888              customInput.select();
889            });
890          }
891        });
892        methodButtons.set(value, choice);
893        choices.append(choice);
894        defer(refreshMethods);
895      });
896      methodGroup.append(methodHeading, choices);
897      paymentMethodControl = methodGroup;
898    }
899    refreshAmountCopy();
900
901    const referral = document.createElement('div');
902    referral.className = 'sir-referral';
903    referral.style.cssText = 'margin-top:22px';
904    const referralToggle = document.createElement('button');
905    referralToggle.type = 'button';
906    referralToggle.textContent = referralCode
907      ? 'Referral code included'
908      : 'Have a referral code?';
909    referralToggle.setAttribute('aria-expanded', String(Boolean(referralCode)));
910    referralToggle.style.cssText =
911      'border:0;background:transparent;color:#5c7471;padding:0;font-size:13px;cursor:pointer';
912    const referralInput = document.createElement('input');
913    referralInput.type = 'text';
914    referralInput.autocomplete = 'off';
915    referralInput.placeholder = 'Referral code';
916    referralInput.setAttribute('aria-label', 'Referral Code (Optional)');
917    referralInput.className = 'sir-field';
918    referralInput.value = referralCode;
919    referralInput.hidden = !referralCode;
920    referralInput.style.cssText =
921      'width:100%;min-height:48px;margin-top:10px;border:1px solid #c5e1dc;border-radius:10px;padding:0 14px;background:transparent;color:#172e2d';
922    referralToggle.addEventListener('click', () => {
923      referralInput.hidden = !referralInput.hidden;
924      referralToggle.textContent = referralInput.hidden
925        ? referralInput.value
926          ? 'Referral code included'
927          : 'Have a referral code?'
928        : 'Hide referral code';
929      referralToggle.setAttribute(
930        'aria-expanded',
931        String(!referralInput.hidden),
932      );
933      if (!referralInput.hidden) referralInput.focus();
934    });
935    referralInput.addEventListener('input', () => {
936      referralCode = referralInput.value;
937    });
938    referral.append(referralToggle, referralInput);
939
940    const status = document.createElement('p');
941    status.className = 'sir-status';
942    status.setAttribute('aria-live', 'polite');
943    status.setAttribute('role', 'status');
944    status.hidden = true;
945    status.style.cssText =
946      'margin:14px 0 0;color:#a61b1b;font-size:14px;font-weight:600';
947
948    submit.type = 'button';
949    submit.className = 'sir-primary';
950    submit.style.cssText =
951      'width:100%;min-height:56px;margin:0;border:0;border-radius:10px;padding:0 18px;font-size:16px;font-weight:750;cursor:pointer;transition:filter 140ms ease,transform 140ms ease';
952    submit.addEventListener('click', () => {
953      const minimum = configuration.amounts.minimumAmount || 1;
954      const maximum = configuration.amounts.maximumAmount;
955      if (
956        !Number.isInteger(selectedAmount) ||
957        (selectedAmount < minimum &&
958          !(
959            selectedPaymentMethod === 'stocks' &&
960            selectedAmount === 0
961          )) ||
962        (maximum && selectedAmount > maximum)
963      ) {
964        setStatus(status, 'Enter a valid donation amount.');
965        customInput.focus();
966        return;
967      }
968      startDonation({
969        status,
970        amount: selectedAmount,
971        email,
972        paymentMethod: selectedPaymentMethod,
973        frequency,
974        referralCode,
975        firstName,
976        lastName,
977        submit,
978        checkoutButtonLabel,
979      });
980    });
981
982    const footer = document.createElement('footer');
983    footer.className = 'sir-trust-footer';
984    footer.dataset.testid = 'widget-runtime-trust-footer';
985    footer.style.cssText =
986      `display:flex;align-items:center;justify-content:center;flex-wrap:wrap;column-gap:12px;row-gap:4px;margin:10px -28px 0;padding:7px 20px 8px;border-top:1px solid color-mix(in srgb,${buttonColor} 16%,#d7ebe6);background:color-mix(in srgb,${buttonColor} 3%,#fbfffe);color:#617b77;text-align:center;font-size:11px;line-height:1.4`;
987
988    const brandRow = (label, logoSrc, logoAlt, logoWidth, href) => {
989      const row = document.createElement(href ? 'a' : 'div');
990      row.className = 'sir-trust-item';
991      row.style.cssText =
992        'min-height:24px;display:flex;align-items:center;justify-content:center;gap:6px;flex:0 0 auto;color:inherit;text-decoration:none;white-space:nowrap';
993      if (href) {
994        row.href = href;
995        row.target = '_blank';
996        row.rel = 'noopener noreferrer';
997        row.setAttribute('aria-label', 'Visit the SIR Giving homepage');
998        row.dataset.testid = 'widget-runtime-sir-link';
999      }
1000      const rowLabel = document.createElement('span');
1001      rowLabel.textContent = label;
1002      const logo = document.createElement('img');
1003      logo.className = 'sir-trust-logo';
1004      logo.src = logoSrc;
1005      logo.alt = logoAlt;
1006      logo.style.cssText =
1007        `display:block;width:${logoWidth}px;max-height:28px;object-fit:contain`;
1008      row.append(rowLabel, logo);
1009      return row;
1010    };
1011
1012    const powered = brandRow(
1013      'Powered by',
1014      `${runtimeOrigin}/assets/sir-svg.svg`,
1015      'SIR Giving',
1016      72,
1017      'https://www.sirgiving.org/'
1018    );
1019    const trustDivider = document.createElement('span');
1020    trustDivider.className = 'sir-trust-divider';
1021    trustDivider.setAttribute('aria-hidden', 'true');
1022    trustDivider.style.cssText =
1023      `width:1px;height:16px;flex:0 0 auto;background:color-mix(in srgb,${buttonColor} 18%,#c8dcd7)`;
1024    const processed =
1025      configuration.provider === 'EVERY_ORG'
1026        ? brandRow(
1027            'Processed by 501(c)(3)',
1028            `${runtimeOrigin}/assets/every-org-logo-wordmark.svg`,
1029            'Every.org',
1030            86
1031          )
1032        : brandRow(
1033            'Processed by',
1034            `${runtimeOrigin}/assets/stripe-wordmark.svg`,
1035            'Stripe',
1036            58
1037          );
1038    footer.append(powered, trustDivider, processed);
1039
1040    const actionTray = document.createElement('div');
1041    actionTray.className = 'sir-action-tray';
1042    actionTray.dataset.testid = 'widget-runtime-action-tray';
1043    actionTray.style.cssText =
1044      `flex:0 0 auto;padding:12px 28px 0;background:color-mix(in srgb,${buttonColor} 3%,#fbfffe);border-top:1px solid color-mix(in srgb,${buttonColor} 13%,#d7ebe6)`;
1045    actionTray.append(submit, status, footer);
1046
1047    if (!environmentNotice.hidden) body.append(environmentNotice);
1048    body.append(emailLabel);
1049    if (nameFields) body.append(nameFields);
1050    if (paymentMethodControl) body.append(paymentMethodControl);
1051    if (showMonthlyDonationUi) body.append(frequencyGroup);
1052    body.append(amountHeading, amountHelp, amounts);
1053    if (configuration.amounts.customAmountEnabled !== false) body.append(cu
1053stomLabel);
1054    body.append(reward);
1055    body.append(referral);
1056    panel.append(header, body, actionTray);
1057    dialog.append(styles, panel);
1058    let fallbackBackdrop;
1059    let closed = false;
1060    const teardownDialog = () => {
1061      if (closed) return;
1062      closed = true;
1063      removeNode(fallbackBackdrop);
1064      removeNode(dialog);
1065      if (document.body.contains(trigger)) {
1066        trigger.focus();
1067      }
1068      dispatchWidgetClosed();
1069      document.removeEventListener('keydown', handleFallbackKeydown, true);
1070    };
1071    const closeDialog = () => {
1072      if (supportsNativeDialog && dialog.open) {
1073        dialog.close();
1074        return;
1075      }
1076      teardownDialog();
1077    };
1078    const handleFallbackKeydown = (event) => {
1079      if (event.key === 'Escape') {
1080        event.preventDefault();
1081        closeDialog();
1082      }
1083    };
1084    close.addEventListener('click', closeDialog);
1085    dialog.addEventListener('close', teardownDialog);
1086    if (!supportsNativeDialog) {
1087      fallbackBackdrop = document.createElement('div');
1088      fallbackBackdrop.setAttribute('aria-hidden', 'true');
1089      fallbackBackdrop.style.cssText =
1090        'position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(20,40,39,.68);z-index:2147483646';
1091      document.body.append(fallbackBackdrop);
1092      document.addEventListener('keydown', handleFallbackKeydown, true);
1093    }
1094    document.body.append(dialog);
1095
1096    refreshSelection();
1097    if (supportsNativeDialog) {
1098      dialog.showModal();
1099    } else {
1100      dialog.setAttribute('open', '');
1101    }
1102    try {
1103      email.focus({ preventScroll: true });
1104    } catch {
1105      email.focus();
1106    }
1107  };
1108
1109  trigger = document.createElement('button');
1110  trigger.type = 'button';
1111  trigger.dataset.testid = 'widget-runtime-button';
1112  trigger.dataset.widgetInstance = instanceId;
1113  trigger.disabled = true;
1114  trigger.textContent = 'Loading donation option…';
1115  trigger.setAttribute('aria-busy', 'true');
1116  trigger.style.cssText =
1117    'min-height:48px;border:0;border-radius:8px;background:#0f766e;color:#fff;padding:0 20px;font:700 16px "Public Sans","Segoe UI",system-ui,sans-serif;cursor:pointer';
1118  script.insertAdjacentElement('afterend', trigger);
1119
1120  const loadDonationCapabilities = async () => {
1121    const controller = new AbortController();
1122    const timeout = window.setTimeout(() => controller.abort(), 5000);
1123    try {
1124      const response = await fetch(`${apiOrigin}/api/donations/capabilities`, { signal: controller.signal });
1125      return response.ok ? await response.json() : undefined;
1126    } catch {
1127      return undefined;
1128    } finally {
1129      window.clearTimeout(timeout);
1130    }
1131  };
1132
1133  Promise.all([
1134    fetch(
1135      previewToken ? `${api}/runtime` : api,
1136      previewToken
1137        ? {
1138            method: 'POST',
1139            headers: { 'Content-Type': 'application/json' },
1140            body: JSON.stringify({ token: previewToken }),
1141          }
1142        : undefined,
1143    ).then((response) => (response.ok ? response.json() : Promise.reject())),
1144    fetch(`${apiOrigin}/api/admin/configuration`)
1145      .then((response) => (response.ok ? response.json() : undefined))
1146      .catch(() => undefined),
1147    loadDonationCapabilities(),
1148  ])
1149    .then(([data, rewardData, capabilities]) => {
1150      donationCapabilities = capabilities;
1151      configuration = data.configuration;
1152      environment = data.environment;
1153      rewardConfiguration = readConfigurationDocument(rewardData);
1154      const embedOriginResult = resolveEmbedOrigin(
1155        configuration.embedOrigins || [],
1156      );
1157      embedOrigin = embedOriginResult.origin;
1158      trigger.textContent = configuration.content.buttonLabel || 'Donate now';
1159      trigger.setAttribute(
1160        'aria-label',
1161        configuration.content.accessibleButtonLabel ||
1162          configuration.content.buttonLabel ||
1163          'Donate now',
1164      );
1165      const triggerColor = normalizeThemeColor(
1166        configuration.appearance?.buttonColor,
1167      );
1168      trigger.style.background = triggerColor;
1169      trigger.style.color = readableTextColor(triggerColor);
1170      if (!embedOriginResult.allowed) {
1171        console.warn(
1172          '[SIR Giving Widget] Blocked widget load from an unapproved website origin.',
1173          {
1174            origin: embedOriginResult.origin,
1175            allowedOrigins: configuration.embedOrigins || [],
1176          },
1177        );
1178        trigger.textContent = 'Donation widget not approved for this website';
1179        trigger.setAttribute(
1180          'aria-label',
1181          'Donation widget not approved for this website',
1182        );
1183        trigger.removeAttribute('aria-busy');
1184        return;
1185      }
1186      if (
1187        configuration.provider === 'STRIPE' &&
1188        !isVerifiedStripeEnvironment(environment)
1189      ) {
1190        trigger.textContent = 'Stripe donations unavailable';
1191        trigger.setAttribute('aria-label', 'Stripe donations unavailable');
1192        trigger.removeAttribute('aria-busy');
1193        return;
1194      }
1195      trigger.disabled = false;
1196      trigger.removeAttribute('aria-busy');
1197      notifyVerifiedParent();
1198      if (script.dataset.autoOpen === 'true') {
1199        trigger.hidden = true;
1200        openDialog();
1201      }
1202    })
1203    .catch(() => {
1204      trigger.textContent = 'Donation widget unavailable';
1205      trigger.removeAttribute('aria-busy');
1206    });
1207
1208  trigger.addEventListener('click', () => {
1209    if (configuration) openDialog();
1210  });
1211})();

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.