Twiist Landing Page Script
Technical documentation for the client-side form and tracking scripts used on the Twiist landing page.
Getting Started
Overview
This page includes two script blocks:
- Form behavior script — handles hidden-field pre-fill, campaign-specific form changes, custom inline validation, reCAPTCHA gating, CAPI lead events, and anchor scrolling.
- Mobile sticky CTA script — shows and hides a sticky call-to-action element based on scroll position (mobile only).
External Dependencies
- Datepicker stylesheet from CDN
- Google reCAPTCHA loaded with
render=explicit - jQuery — used in the
$(document).ready()block for button state initialization
Initialization
Page Setup
On DOMContentLoaded, the script looks up the main form (#wf-form-webinar) and the submit control (#submitRequest). If the form is missing, an error is logged and the script halts.
Tracking & Campaign Data
A fixed set of keys is read from localStorage:
| Key | Purpose |
|---|---|
MarketingChannel | Attribution channel |
utm_source | Campaign source |
utm_medium | Campaign medium |
utm_campaign | Campaign name & variant logic |
utm_term | Paid search term |
utm_content | Creative differentiator |
form_audience | Audience segment |
form_campaign | Internal campaign ID |
entry_id | Entry identifier |
These values drive three behaviors: Version B form switching, redirect URL enrichment, and CAPI payload population.
Alternate Form — Version B
If utm_campaign contains versionb, the script:
- Shows all
.hcp-itemelements - Changes the submit button label to "Submit"
- Updates
retURLtohttps://www.twiist.com/thank-you-pwd - Marks all inputs inside
.hcp-itemas required
Because required fields are added dynamically, checkFormValidity() re-queries [required] on every call rather than relying on the initial NodeList.
Utility Functions
setIp()
Fetches the visitor's IP from https://api.ipify.org?format=json and writes it to the hidden field #00NWQ000004WOUj. Fails silently with a console warning.
setFieldValue(id, value)
Sets a form field's value by ID. Used for hidden marketing fields like LeadSource, LeadSourceDetail, MarketingChannel, and form_id.
allowOnlyNumbersSymbols(input)
Strips non-phone characters. Permits only digits, +, -, and whitespace. Bound to #mobile and [name="mobile"].
Form Validation & Submission
Validation System
Native validation disabled
form.noValidate = true and the invalid event is captured and suppressed so the browser never shows its default tooltip UI.
Injected error styles
A <style> tag with the ID wf-inline-validation-styles is injected once. It defines:
.field-error— red 2px border.checkbox-error— red outline.error-text— red italic message below the field- Positional variants for checkboxes and selects
Error helpers
| Function | Description |
|---|---|
showError(input, msg, isCheckbox) | Adds error class and inserts/updates the error message element |
clearError(input) | Removes error class and deletes the message element |
validateField(input) rules
- Checkbox — must be checked
- Radio — at least one in the
namegroup must be selected - Select — must have a non-empty value
- Text/other — must contain non-whitespace text
Live validity checks
input, change, and blur listeners are attached per required field. Delegated input/change listeners on the form catch dynamically-added fields. The submit button is enabled only when all required fields are valid and reCAPTCHA is complete.
reCAPTCHA Integration
State is tracked via a recaptchaComplete flag, updated by:
window.onRecaptchaCompletecallbackwindow.onRecaptchaExpirecallback- A
MutationObserverwatching theg-recaptcha-responsehidden field'svalueattribute
Submit Button Behavior
During setup:
- Starts disabled at 0.5 opacity
- Type is changed from
submittobutton
This ensures custom validation and the CAPI request run before form.submit() is called programmatically.
The script captures the original button label and an optional loading label from data-wait to toggle between idle and waiting states.
Submit Flow
- Prevent duplicates — exits if
window.__capiInFlightis true. - Require reCAPTCHA — checks for a token in the hidden field or via
grecaptcha.getResponse(). Exits if missing. - Run validation — sets
hasSubmitted = true, validates all required fields, focuses the first invalid one, exits early on failure. - Send CAPI event — POSTs to the Lambda endpoint (see below).
- Guarantee submission — a 10-second timeout ensures
form.submit()fires even if CAPI hangs. The.finally()block also callsform.submit(). - Failure reset — on
w-form-fail, button state is recalculated.
Marketing & Attribution
Hidden Field Population
form_id
If form_mlr_number, form_mlr_version, and form_mlr_approval_date all exist, a composite value is built:
NA-NA-{mlr_number}-{mlr_version}-{mlr_approval_date}
IP address
setIp() runs at the end of initialization to populate the IP field asynchronously.
Redirect URL Enrichment
If any UTM value is stored, the script parses retURL as a URL and appends all stored tracking values as query parameters. This preserves attribution through the thank-you page redirect.
Campaign-Specific Lead Source Mapping
When utm_source = peelz
| Field | Value |
|---|---|
LeadSource | Partner |
LeadSourceDetail | Peelz |
MarketingChannel is then mapped by campaign:
utm_campaign | MarketingChannel |
|---|---|
peelzmarketing | partner-email |
peelzmarketingsocial | facebook-organic |
versionb_peelzleadpilot | partner-referral |
| anything else | Partner-Referral |
When utm_campaign = senseonics
| Field | Value |
|---|---|
LeadSource | Partner |
LeadSourceDetail | Senseonics |
MarketingChannel | Partner-Referral |
Analytics & Infrastructure
CAPI in AWS
The CAPI Lambda function relays form-submit events to Meta's Conversions API.
| Detail | Value |
|---|---|
| Location | AWS Console → Lambda → twiistCAPI |
| Region | us-east-1 |
| Access | Niko's account (credentials in Bitwarden) |
| Pixel & Token | Stored as environment variables under Configuration |
| Meta Dashboard | Requires Sequel team access (tied to a Facebook account) |
Credentials vault: All account information (API keys, vendor logins, AWS credentials, etc.) can be accessed via the dev Bitwarden account at https://banners.culthealth.com/vault/#/login.
Lambda logic
const enrichedData = body.data.map(evt => ({
...evt,
event_time: evt.event_time || Math.floor(Date.now() / 1000),
action_source: evt.action_source || 'website',
event_source_url: evt.event_source_url || event.headers?.referer || '',
user_data: {
client_ip_address: event.headers['x-forwarded-for'],
client_user_agent: event.headers['user-agent']
}
}));
const payload = {
data: enrichedData,
...(body.test_event_code ? { test_event_code: body.test_event_code } : {})
};
const response = await fetch(
`https://graph.facebook.com/v18.0/${process.env.PIXEL}/events?access_token=${process.env.ACCESS_TOKEN}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}
);
Testing: Append ?test_event_code=VALUE to the page URL and the event will be submitted to Meta as a test instead of live data.
Accessing & Deploying the Lambda
How to find the Lambda in AWS
- Sign in to the AWS Console at
https://us-east-1.console.aws.amazon.com/— use Niko's account (credentials in Bitwarden). - Make sure the region selector in the top-right shows US East (N. Virginia) — us-east-1.
- In the search bar at the top, type Lambda and select the Lambda service.
- In the Functions list, search for
twiistCAPIand click it.
Viewing and editing the code
- On the function page, scroll down to the Code source panel. If the deployment package is small enough, you'll see an inline editor.
- Edit directly in the browser, or download the code with Actions → Export function (.zip) to work locally.
- If you edit in the browser, click Deploy (the orange button above the editor) to publish. The new version goes live immediately.
Deploying an update from a local .zip
If you prefer working locally:
# 1. Zip your updated code (index.mjs or index.js + any dependencies)
zip -r twiistCAPI.zip .
# 2. Deploy via the AWS CLI
aws lambda update-function-code \
--function-name twiistCAPI \
--zip-file fileb://twiistCAPI.zip \
--region us-east-1
# 3. (Optional) Publish a version snapshot
aws lambda publish-version \
--function-name twiistCAPI \
--region us-east-1
Or upload through the console: Code source → Upload from → .zip file.
Environment variables (Pixel ID & access token)
- On the Lambda page, go to Configuration → Environment variables.
- You'll see
PIXEL(the Meta Pixel ID) andACCESS_TOKEN(the Conversions API token). Click Edit to view or rotate them. - After changing a value, click Save. Changes take effect on the next invocation — no redeploy needed.
Checking logs when something breaks
- On the Lambda page, go to the Monitor tab → View CloudWatch logs.
- Click the most recent log stream.
- Look for the enrichment step and the
fetchtograph.facebook.com. If Meta rejected the event, the response body will say why (invalid pixel, expired token, malformed payload, etc.).
Testing without affecting live data
Two options:
- From the form: Append
?test_event_code=TEST12345to the page URL before submitting. The Lambda forwards that code to Meta, which routes the event to the Test Events tab in Events Manager instead of counting it as real data. - From the Lambda console: Go to the Test tab, create a test event with a sample payload (include
test_event_code), and click Test. Results and logs appear inline.
Meta Events Manager access
To verify events are arriving and inspect payloads on the Meta side, you need access to the Pixel dashboard in Meta Events Manager. This requires a Facebook account with permissions granted by the Sequel team — the account can't be shared, so request access directly from them.
The Lambda is live immediately on deploy. There's no separate staging version — changes hit production as soon as you click Deploy or run update-function-code. Always test with test_event_code first.
jQuery Ready Handler
A separate $(document).ready() block disables the submit button and sets opacity to 0.5. This is defensive — it ensures the button starts disabled even if initialization timing changes.
UX Enhancements
Anchor Scrolling Behavior
Generic hash-link scrolling
Intercepts any a[href^="#"] click and smooth-scrolls to the target with a 200px top offset. Updates the URL hash via history.pushState.
Special handling for #pwd-form
A second handler targets links resolving to #pwd-form on the same page. It also sets tabindex="-1" and calls focus({ preventScroll: true }) on the target for accessibility.
Mobile Sticky CTA Script
An IIFE that manages a sticky call-to-action element on mobile viewports.
Configuration
| Property | Default | Purpose |
|---|---|---|
stickyElementSelector | .bg-full-tertiary.is-mobile | The sticky CTA element |
formSelector | #wf-form-webinar | Form to stop near |
footerSelector | [class*="footer"] | Footer to stop near |
stickyTopOffset | 200 | Top offset while sticky |
hideEarlyOffset | 350 | Extra distance before form where CTA hides |
mobileBreakpoint | 768 | Max viewport width for behavior |
Behavior
- Becomes sticky after scrolling 300px past its original position
- Hides when approaching the form or footer
- Removes sticky/hidden classes on non-mobile viewports
Performance
Scroll events are throttled with requestAnimationFrame via a ticking flag. Subscribes to both scroll and resize.
Reference
Required Elements & IDs
| Selector / ID | Required | Notes |
|---|---|---|
#wf-form-webinar | Yes | Script halts without it |
#submitRequest | Yes | Submit button |
#retURL | Yes | Redirect URL field |
#form_id | Yes | Composite ID field |
#form_mlr_number | Yes | MLR number |
#form_mlr_version | Yes | MLR version |
#form_mlr_approval_date | Yes | MLR approval date |
#00NWQ000004WOUj | Yes | IP address field |
.hcp-item | Optional | Version B extra fields |
.part2-formdropdown | Optional | Select wrapper for error placement |
#pwd-form | Optional | Scroll target for accessibility handler |
.bg-full-tertiary.is-mobile | Optional | Sticky CTA element |
Full Code
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/datepicker/1.0.10/datepicker.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://www.google.com/recaptcha/api.js?render=explicit" async defer></script>
<script>
function setIp() {
fetch('https://api.ipify.org?format=json')
.then(response => response.json())
.then(data => {
const ipField = document.getElementById('00NWQ000004WOUj');
if (ipField) {
ipField.value = data.ip;
console.log('IP address set to:', data.ip);
} else {
console.warn('IP field with ID "00NWQ000004WOUj" not found.');
}
})
.catch(error => console.warn('Could not fetch IP:', error));
}
function setFieldValue(id, value) {
const element = document.getElementById(id);
if (element) {
element.value = value;
} else {
console.warn(`Form element with ID "${id}" not found.`);
}
}
function allowOnlyNumbersSymbols(input) {
input.value = input.value.replace(/[^0-9+\-\s]/g, '');
}
function enableBtn(){
$("#submitRequest").attr('disabled', false);
$("#submitRequest").css({opacity: '1'})
}
document.addEventListener('click', (e) => {
const a = e.target.closest('a[href^="#"]');
if (!a) return;
const hash = a.getAttribute('href');
if (!hash || hash === '#') return;
const target = document.querySelector(hash);
if (!target) return;
e.preventDefault();
const offset = 200;
const top = target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top, behavior: 'smooth' });
history.pushState(null, '', hash);
});
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('wf-form-webinar');
const submit = document.getElementById('submitRequest');
const emailField = document.getElementById('email');
if (!form) {
console.error('Form with ID "wf-form-webinar" not found.');
return;
}
// ALT FORM LOGIC
const params = new URLSearchParams(window.location.search);
const TRACKING_KEYS = ['MarketingChannel', 'utm_source', 'utm_medium', 'utm_campaign',
'utm_term', 'utm_content', 'form_audience', 'form_campaign', 'entry_id'];
const storedTrackingValues = {};
TRACKING_KEYS.forEach((key) => {
try {
const value = window.localStorage.getItem(key);
if (value) storedTrackingValues[key] = value;
} catch (e) {}
});
const retURLInput = document.getElementById('retURL');
var HCPitems = document.getElementsByClassName('hcp-item');
var utmCampaign = storedTrackingValues.utm_campaign || '';
const matchesVersionB = utmCampaign.toLowerCase().includes('versionb');
if (matchesVersionB) {
Array.from(HCPitems).forEach(item => {
item.style.display = "block";
var inputs = item.getElementsByTagName('input');
submit.value = "Submit";
retURLInput.value = "https://www.twiist.com/thank-you-pwd";
Array.from(inputs).forEach(input => {
input.setAttribute('required', 'true');
});
});
}
// ... (validation, reCAPTCHA, campaign mapping, submit handler)
// See full source in repository
});
</script>
<script>
(function() {
const CONFIG = {
stickyElementSelector: '.bg-full-tertiary.is-mobile',
formSelector: '#wf-form-webinar',
footerSelector: '[class*="footer"]',
stickyTopOffset: 200,
hideEarlyOffset: 350,
mobileBreakpoint: 768
};
function getAbsoluteTop(element) {
return element.getBoundingClientRect().top + window.scrollY;
}
function isMobile() {
return window.innerWidth < CONFIG.mobileBreakpoint;
}
const stickyElement = document.querySelector(CONFIG.stickyElementSelector);
const formElement = document.querySelector(CONFIG.formSelector);
const footerElement = document.querySelector(CONFIG.footerSelector);
if (!stickyElement) return;
const elementTop = stickyElement.offsetTop;
const elementHeight = stickyElement.offsetHeight;
function handleScroll() {
if (!isMobile()) {
stickyElement.classList.remove('is-sticky', 'is-hidden');
return;
}
const scrollPosition = window.scrollY;
const activationThreshold = elementTop + 300;
if (scrollPosition >= activationThreshold) {
stickyElement.classList.add('is-sticky');
let shouldHide = false;
if (formElement) {
const formTop = getAbsoluteTop(formElement);
if (scrollPosition >= formTop - CONFIG.stickyTopOffset - elementHeight - CONFIG.hideEarlyOffset) {
shouldHide = true;
}
}
if (footerElement) {
const footerTop = getAbsoluteTop(footerElement);
if (scrollPosition >= footerTop - CONFIG.stickyTopOffset - elementHeight) {
shouldHide = true;
}
}
stickyElement.classList.toggle('is-hidden', shouldHide);
} else {
stickyElement.classList.remove('is-sticky', 'is-hidden');
}
}
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => { handleScroll(); ticking = false; });
ticking = true;
}
});
window.addEventListener('resize', handleScroll);
handleScroll();
})();
</script>