feat(forms): add isolated Form A iframe adapter

This commit is contained in:
DeCentN2Madness committed 2026-10-28 17:19:56 -07:00
1 parent a635a5f55c
commit 61b2bd0f1e
18 files changed
+1345 -170

No files matched your search

+149
View File
@@ -0,0 +1,149 @@
---
import type { FormAIframeConfig } from '../utils/form-integration';
interface Props {
config: FormAIframeConfig;
heading: string;
description?: string;
variant?: 'standalone' | 'embedded';
}
const { config, heading, description, variant = 'standalone' } = Astro.props;
if (!config.enabled || !config.url || !config.title || !config.fallbackText || !config.fallbackLinkText) {
throw new Error('JotformIframe requires an enabled, fully validated Form A iframe configuration.');
}
const escapeHtml = (value: string) => value.replace(/[&<>"']/g, (character) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
})[character] ?? character);
// Astro treats iframe descendants as raw text. set:html emits standards-based
// iframe fallback markup while preserving the fixed, server-validated URL.
const fallbackMarkup = `<p class="jotform-iframe__fallback">${escapeHtml(config.fallbackText)} <a href="${escapeHtml(config.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(config.fallbackLinkText)}</a></p>`;
---
<section
class:list={['jotform-iframe', { 'jotform-iframe--embedded': variant === 'embedded' }]}
role="group"
aria-labelledby="jotform-iframe-heading"
data-jotform-form-a
data-form-mode="isolated-test"
data-form-environment="isolated-test"
data-logical-form="general"
data-form-context={config.context}
data-jotform-source="approved-form-a"
>
<h3 id="jotform-iframe-heading" class="jotform-iframe__heading">{heading}</h3>
{description && <p class="jotform-iframe__description">{description}</p>}
<iframe
class="jotform-iframe__frame"
src={config.url}
title={config.title}
loading="eager"
referrerpolicy="no-referrer"
sandbox="allow-forms allow-scripts allow-same-origin"
set:html={fallbackMarkup}
></iframe>
</section>
<!-- Keep adapter styling route-local: the campaign and main-site bundles must not share campaign CSS. -->
<style is:inline>
.jotform-iframe {
/*
* Fixed heights keep the parent independent of cross-origin Jotform
* messaging. The fixed allowance is measured from the rendered Builder
* form at desktop widths, then includes explicit room for contact
* validation errors and Spanish wrapping. The extra final allowance
* prevents a Jotform blur-validation message from creating an internal
* scrollbar. Landing uses only compact contact fields, so it retains a
* smaller allowance.
*/
--jotform-frame-height: 60rem;
background: white;
border-radius: 4px;
box-shadow: 0 4px 4px rgb(81 81 81 / 4%), 0 4px 16px rgb(81 81 81 / 8%);
color: var(--color-primary);
margin: 0;
max-width: none;
min-width: 0;
overflow: hidden;
padding: 1.5rem;
width: 100%;
}
.jotform-iframe__heading {
border-bottom: 1px solid #f1f1f1;
margin: -1.5rem -1.5rem 1.25rem;
padding: 1.2rem 1.5rem;
text-align: center;
}
.jotform-iframe__frame {
border: 0;
display: block;
height: var(--jotform-frame-height);
max-width: 100%;
min-height: var(--jotform-frame-height);
width: 100%;
}
.jotform-iframe__fallback {
color: var(--color-muted);
font-size: .9rem;
line-height: 1.6;
margin: 0 0 1rem;
}
.jotform-iframe__description {
color: var(--color-muted);
font-size: .9rem;
line-height: 1.6;
margin: 0 0 1rem;
}
.jotform-iframe__fallback a {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: .15em;
}
.jotform-iframe__fallback a:focus-visible {
outline: 3px solid var(--color-focus, #115bbd);
outline-offset: 3px;
}
.jotform-iframe--embedded {
--jotform-frame-height: 42rem;
background: transparent;
border-radius: 0;
box-shadow: none;
margin: 0;
max-width: none;
overflow: visible;
padding: 0;
}
.jotform-iframe--embedded .jotform-iframe__heading {
border: 0;
margin: 0 0 1rem;
padding: 0;
text-align: left;
}
@media (max-width: 640px) {
.jotform-iframe__frame {
height: 68rem;
min-height: 68rem;
}
.jotform-iframe--embedded .jotform-iframe__frame {
height: 50rem;
min-height: 50rem;
}
}
</style>
@@ -1,7 +1,8 @@
---
import Icon from './Icon.astro';
import FormAPreview from '../FormAPreview.astro';
import { formAPreviewEnabled, formModeForRuntime, resolveFormMode } from '../../utils/form-integration';
import JotformIframe from '../JotformIframe.astro';
import { formAPreviewEnabled, formModeForRuntime, resolveFormAIframe, resolveFormMode } from '../../utils/form-integration';
interface Props {
sourcePage: 'general' | 'commercial' | 'ahcccs' | 'private_pay';
title: string;
@@ -13,6 +14,23 @@ const phone = import.meta.env.PUBLIC_AIA_PHONE || '480-687-7099';
const tel = phone.replace(/[^0-9+]/g, '');
const formConfig = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, formModeForRuntime(formConfig));
const campaignRoute = sourcePage === 'general'
? '/lp/aba-therapy'
: sourcePage === 'commercial'
? '/lp/aba-insurance'
: sourcePage === 'ahcccs'
? '/lp/aba-ahcccs'
: '/lp/aba-private-pay';
const iframeDescription = 'Use this form to request a conversation. An AIA team member can explain the next steps for your family.';
const formAIframe = resolveFormAIframe({
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
route: campaignRoute,
locale: 'en',
});
---
<section class="section consultation" id="consultation" aria-labelledby="consultation-heading">
<div class="container consultation__panel grid-2">
@@ -22,7 +40,9 @@ const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIE
<p class="lede">{intro}</p>
</div>
<div class="form-wrap">
{formPreview ? <FormAPreview lang="en" variant="compact" submitText={buttonText} /> : <div id="lead-form" role="group" aria-labelledby="campaign-form-heading" data-forms-mode="off">
{formAIframe.enabled
? <JotformIframe config={formAIframe} heading="Start with your contact details" description={iframeDescription} variant="embedded" />
: formPreview ? <FormAPreview lang="en" variant="compact" submitText={buttonText} /> : <div id="lead-form" role="group" aria-labelledby="campaign-form-heading" data-forms-mode="off">
<!-- TODO: Replace this inactive presentation with the approved Jotform integration under P9-03. -->
<!-- TODO: Verify privacy, spam protection and notifications before production activation. -->
<h3 id="campaign-form-heading">Start with your contact details</h3>
@@ -1,5 +1,20 @@
---
import JotformIframe from '../JotformIframe.astro';
import { resolveFormAIframe } from '../../utils/form-integration';
const formAIframe = resolveFormAIframe({
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
route: '/lp/early-intervention',
locale: 'en',
});
---
<!-- Source: public /lp/early-intervention, 2026-10-20 (cache-qualified). -->
<!-- TODO: Approved Jotform integration and spam protection require separate authorization. -->
<!-- TODO: Production Jotform activation and spam protection require separate authorization. -->
<div class="qr-landing">
<section class="qr-hero" aria-labelledby="qr-hero-heading">
<div class="qr-hero-copy">
@@ -23,17 +38,19 @@
</div>
</section>
<div class="qr-process-band">
<section class="qr-section qr-contact-section" aria-label="Call AIA about early intervention"><div id="consultation" class="lead-form" aria-labelledby="consultation-heading" role="group" data-forms-mode="off">
<section class="qr-section qr-contact-section" aria-label="Contact AIA about early intervention"><div id="consultation" class="lead-form" aria-labelledby="consultation-heading" role="group" data-forms-mode={formAIframe.enabled ? 'isolated-test' : 'off'}>
<div class="form-heading">
<h2 id="consultation-heading">Call AIA about early intervention</h2>
<h2 id="consultation-heading">Contact AIA about early intervention</h2>
<p>Speak with an AIA team member about your questions and possible next steps.</p>
</div>
{formAIframe.enabled ? <JotformIframe config={formAIframe} heading="Start with your contact details" variant="embedded" /> : <>
<div class="field"><label for="parent-name">Parent or caregiver name</label><input id="parent-name" autocomplete="name" required="" type="text" disabled=""></div>
<div class="field"><label for="phone">Phone number</label><input id="phone" autocomplete="tel" required="" type="tel" disabled=""></div>
<div class="field"><label for="email">Email address<span class="field-optional"> (optional)</span></label><input id="email" autocomplete="email" type="email" disabled=""></div>
<button class="button button-primary" type="button" disabled="">Request a consultation</button></div>
<button class="button button-primary" type="button" disabled="">Request a consultation</button></>}
</div>
<aside class="qr-call-card">
<p class="eyebrow">Prefer to talk?</p>
<h2>Call AIA</h2>
+17 -2
View File
@@ -1,8 +1,9 @@
---
import ContactForm from '../ContactForm.astro';
import FormAPreview from '../FormAPreview.astro';
import JotformIframe from '../JotformIframe.astro';
import { formMessages } from '../../utils/form-messages';
import { formAPreviewEnabled, formModeForRuntime, resolveFormMode } from '../../utils/form-integration';
import { formAPreviewEnabled, formModeForRuntime, resolveFormAIframe, resolveFormMode } from '../../utils/form-integration';
interface Props {
title: string;
@@ -13,6 +14,16 @@ const { title, lang } = Astro.props;
const text = formMessages(lang);
const formConfig = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, formModeForRuntime(formConfig));
const route = lang === 'es' ? '/es/contact' : '/contact';
const formAIframe = resolveFormAIframe({
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
route,
locale: lang,
});
---
<section class="contact-page">
@@ -20,7 +31,11 @@ const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIE
<div class="contact-page__copy">
<slot />
</div>
{formPreview ? <FormAPreview lang={lang as 'en' | 'es'} variant="full" submitText={lang === 'es' ? 'Enviar' : 'Submit'} /> : <ContactForm title={title} lang={lang} />}
{formAIframe.enabled
? <JotformIframe config={formAIframe} heading={title} />
: formPreview
? <FormAPreview lang={lang as 'en' | 'es'} variant="full" submitText={lang === 'es' ? 'Enviar' : 'Submit'} />
: <ContactForm title={title} lang={lang} />}
</div>
</section>