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.