Twiist Landing Page Script

Technical documentation for the client-side form and tracking scripts used on the Twiist landing page.

Overview

This page includes two script blocks:

  1. Form behavior script — handles hidden-field pre-fill, campaign-specific form changes, custom inline validation, reCAPTCHA gating, CAPI lead events, and anchor scrolling.
  2. Mobile sticky CTA script — shows and hides a sticky call-to-action element based on scroll position (mobile only).

External Dependencies

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:

KeyPurpose
MarketingChannelAttribution channel
utm_sourceCampaign source
utm_mediumCampaign medium
utm_campaignCampaign name & variant logic
utm_termPaid search term
utm_contentCreative differentiator
form_audienceAudience segment
form_campaignInternal campaign ID
entry_idEntry 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:

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"].

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:

Error helpers

FunctionDescription
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

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:

Submit Button Behavior

During setup:

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

  1. Prevent duplicates — exits if window.__capiInFlight is true.
  2. Require reCAPTCHA — checks for a token in the hidden field or via grecaptcha.getResponse(). Exits if missing.
  3. Run validation — sets hasSubmitted = true, validates all required fields, focuses the first invalid one, exits early on failure.
  4. Send CAPI event — POSTs to the Lambda endpoint (see below).
  5. Guarantee submission — a 10-second timeout ensures form.submit() fires even if CAPI hangs. The .finally() block also calls form.submit().
  6. Failure reset — on w-form-fail, button state is recalculated.

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

FieldValue
LeadSourcePartner
LeadSourceDetailPeelz

MarketingChannel is then mapped by campaign:

utm_campaignMarketingChannel
peelzmarketingpartner-email
peelzmarketingsocialfacebook-organic
versionb_peelzleadpilotpartner-referral
anything elsePartner-Referral

When utm_campaign = senseonics

FieldValue
LeadSourcePartner
LeadSourceDetailSenseonics
MarketingChannelPartner-Referral

CAPI in AWS

The CAPI Lambda function relays form-submit events to Meta's Conversions API.

DetailValue
LocationAWS Console → Lambda → twiistCAPI
Regionus-east-1
AccessNiko's account (credentials in Bitwarden)
Pixel & TokenStored as environment variables under Configuration
Meta DashboardRequires 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

  1. Sign in to the AWS Console at https://us-east-1.console.aws.amazon.com/ — use Niko's account (credentials in Bitwarden).
  2. Make sure the region selector in the top-right shows US East (N. Virginia) — us-east-1.
  3. In the search bar at the top, type Lambda and select the Lambda service.
  4. In the Functions list, search for twiistCAPI and click it.

Viewing and editing the code

  1. On the function page, scroll down to the Code source panel. If the deployment package is small enough, you'll see an inline editor.
  2. Edit directly in the browser, or download the code with Actions → Export function (.zip) to work locally.
  3. 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)

  1. On the Lambda page, go to Configuration → Environment variables.
  2. You'll see PIXEL (the Meta Pixel ID) and ACCESS_TOKEN (the Conversions API token). Click Edit to view or rotate them.
  3. After changing a value, click Save. Changes take effect on the next invocation — no redeploy needed.

Checking logs when something breaks

  1. On the Lambda page, go to the Monitor tab → View CloudWatch logs.
  2. Click the most recent log stream.
  3. Look for the enrichment step and the fetch to graph.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:

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.

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

PropertyDefaultPurpose
stickyElementSelector.bg-full-tertiary.is-mobileThe sticky CTA element
formSelector#wf-form-webinarForm to stop near
footerSelector[class*="footer"]Footer to stop near
stickyTopOffset200Top offset while sticky
hideEarlyOffset350Extra distance before form where CTA hides
mobileBreakpoint768Max viewport width for behavior

Behavior

Performance

Scroll events are throttled with requestAnimationFrame via a ticking flag. Subscribes to both scroll and resize.

Required Elements & IDs

Selector / IDRequiredNotes
#wf-form-webinarYesScript halts without it
#submitRequestYesSubmit button
#retURLYesRedirect URL field
#form_idYesComposite ID field
#form_mlr_numberYesMLR number
#form_mlr_versionYesMLR version
#form_mlr_approval_dateYesMLR approval date
#00NWQ000004WOUjYesIP address field
.hcp-itemOptionalVersion B extra fields
.part2-formdropdownOptionalSelect wrapper for error placement
#pwd-formOptionalScroll target for accessibility handler
.bg-full-tertiary.is-mobileOptionalSticky 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>