Developers

Architecture

Last updated July 7, 2026

This is a developer-facing reference for how Super Speedy Coming Soon Products is put together. It covers the major components, how a request flows through them, the parts that are unusual or easy to get wrong, and then the detailed design of each notable component.

The plugin is deliberately small and procedural — a single main file (super-speedy-coming-soon.php), one front-end template, one JavaScript file, and one stylesheet. There are no classes; everything is plain functions wired to WordPress/WooCommerce hooks.


Component summary

Component Where Purpose
Product admin fields sscs_add_custom_fields / sscs_save_custom_fields Per-product configuration (coming-soon flag, arrival date, offer HTML, provider, MailerLite group / SSE form), stored as post meta.
Add-to-cart replacement sscs_maybe_replace_add_to_cart + helpers Swaps WooCommerce’s add-to-cart output for the coming-soon template, theme-agnostically, by discovering the hook at runtime.
Coming-soon template templates/sscs-coming-soon-template.php The front-end UI: countdown, offer description, signup form (MailerLite or SSE), share buttons.
Provider abstraction template branch + class_exists('SSE_Forms') Each product collects signups via MailerLite (built-in) or Super Speedy Emails (optional sibling plugin).
AJAX signup handler sscs_ajax_signup_handler The live signup endpoint used by the MailerLite form. Public (nopriv).
Legacy POST handler sscs_process_signup_form An older non-AJAX signup path on init. Effectively dead code (see below).
Abuse-protection layer honeypot + sscs_check_rate_limit + sscs_verify_turnstile Defends the public endpoint: honeypot, per-IP rate limit, optional Cloudflare Turnstile.
MailerLite client sscs_add_subscriber_to_mailerlite Three-step MailerLite REST flow: upsert subscriber, then assign to group.
Referral system JS (sscs-ajax-signup.js) + referrer_id column Cookie + URL-param sharing scheme that records who referred whom.
Countdown timer sscs-ajax-signup.js Pure client-side countdown to the arrival date.
Settings page sscs_settings_page Global options: MailerLite API key + the spam/abuse controls.
Signups table wp_sscs_signups + sscs_create_custom_table Local ledger of every signup (email, MailerLite id, referrer, WP user).
Test suite .tests/ wp eval-file harness.

Unusual or interesting aspects

A few things here are worth knowing before you change anything:

  1. The add-to-cart replacement discovers its hook at runtime. Rather than assuming WooCommerce’s default hook, it checks a list of known locations and, failing that, walks the entire $wp_filter graph looking for woocommerce_template_single_add_to_cart and rebinds the coming-soon template to the same hook/priority. This makes it theme-independent but is the single most surprising piece of the codebase. See Add-to-cart replacement.

  2. There are two signup code paths, and one is dead. The live path is the AJAX handler. The older sscs_process_signup_form() on init still runs on every request but is effectively unreachable, because the form it expects (with the sscs_signup nonce) is never rendered by the current template. Keep this in mind so you don’t “fix a bug” in code that never executes — or, better, delete it. See The two signup paths.

  3. The nopriv AJAX nonce is not a security boundary. Because the endpoint is public, WordPress hands a valid sscs_signup_nonce to every visitor via wp_localize_script. The nonce only prevents trivially-replayed cross-site posts; it does not stop abuse. That is why the abuse-protection layer (rate limit + honeypot + optional CAPTCHA) exists and is the real defence.

  4. The referral identity is the visitor’s own MailerLite subscriber id. After signup, the handler returns the MailerLite id, stores it in the sscs_mailerlite_user_id cookie, and the JS builds share links carrying it as ?shared_by=<id>. The next visitor’s browser turns that into the sscs_shared_by_id cookie, which is posted back and saved as referrer_id. It’s an entirely client-mediated loop. See Referral system.

  5. Provider selection is per-product and the SSE provider is optional. The Super Speedy Emails option only appears (and only renders) when the SSE_Forms class exists. There is no hard dependency.

  6. Watch the CSS class typo. The body class added for styling is scss-coming-soon-product (note scss, not sscs). It’s inconsistent with the rest of the sscs_/sscs- naming but is load-bearing for the stylesheet — don’t “correct” it without updating the CSS.

  7. Security posture is documented separately. The H1/M1 fixes and the L1 secret-masking are implemented; a couple of lower-severity items (e.g. the MailerLite-id leak) remain open.


Request / data flow

Rendering a coming-soon product page:

wp (is_product + _sscs_coming_soon=yes)
  └─ sscs_maybe_replace_add_to_cart()
        ├─ fast path: known hook locations
        └─ slow path: scan $wp_filter for woocommerce_template_single_add_to_cart
              └─ remove_action(core) + add_action(sscs_render_coming_soon_template)
wp_enqueue_scripts
  └─ enqueue sscs-ajax-signup.js + css (+ Turnstile if enabled)
  └─ wp_localize_script → sscs_ajax { ajax_url, nonce, post_id, arrival_date, captcha }
template renders countdown + offer + (MailerLite form | SSE form) + share buttons

Submitting the MailerLite signup form (AJAX):

POST admin-ajax.php?action=sscs_ajax_signup
  └─ sscs_ajax_signup_handler()
        1. honeypot       (sscs_honeypot_enabled + $_POST['name'])
        2. nonce          (check_ajax_referer)
        3. rate limit     (sscs_check_rate_limit, per-IP transient)
        4. CAPTCHA        (sscs_captcha_enabled + sscs_verify_turnstile)
        5. email valid?   (is_email)
        6. product valid? (product + _sscs_coming_soon=yes + group set)
        7. dedupe + insert into wp_sscs_signups
        8. sscs_add_subscriber_to_mailerlite() → MailerLite REST
        9. set sscs_mailerlite_user_id cookie
       10. wp_send_json_success({ mailerlite_id })

The numbered order in step list matters: the cheap, abuse-blocking checks run before any database query or outbound HTTP.


Component details

Product admin fields

Two functions on WooCommerce product hooks:

  • sscs_add_custom_fields() on woocommerce_product_options_general_product_data renders the fields in the Product data → General tab using the woocommerce_wp_* helpers. An inline jQuery snippet toggles the MailerLite vs SSE fields based on the selected provider.
  • sscs_save_custom_fields( $post_id ) on woocommerce_process_product_meta persists them as post meta.

Stored meta keys:

Meta key Sanitisation on save Notes
_sscs_coming_soon 'yes'/'no' Master switch; gates almost everything else.
_sscs_arrival_date sanitize_text_field YYYY-MM-DD; consumed by the JS countdown.
_sscs_offer_description wp_kses_post HTML allowed; sanitised again on output.
_sscs_email_provider sanitize_text_field mailerlite (default) or sse.
_sscs_mailerlite_group_id sanitize_text_field Target MailerLite group.
_sscs_sse_form_id absint Only shown/used when SSE is active.

Capability and nonce handling are inherited from WooCommerce’s product save flow, so the plugin doesn’t re-check them here. The offer description is the only field that accepts HTML, and it’s filtered through wp_kses_post on both save and render, so it can’t carry script.

Add-to-cart replacement

This is the cleverest and most fragile part. The goal: replace WooCommerce’s add-to-cart output with the coming-soon template without depending on a specific theme’s hook layout.

sscs_maybe_replace_add_to_cart() runs on wp. After confirming we’re on a coming-soon single-product page, it:

  1. Fast path — iterates $known_locations (currently just woocommerce_single_product_summary @ 30, with commented examples for themes like REHub). For each, sscs_try_replace_add_to_cart() checks whether woocommerce_template_single_add_to_cart is actually bound there at that priority; if so it removes the core callback and binds sscs_render_coming_soon_template in its place, then returns.

  2. Slow path — if no known location matched, it walks the entire $wp_filter array (every hook, every priority, every callback) to find where woocommerce_template_single_add_to_cart is registered, and rebinds there.

sscs_render_coming_soon_template() simply includes the template file.

Design implications: – It works across themes that move the add-to-cart to a custom hook — the slow path finds it wherever it lives. – It only recognises the standard woocommerce_template_single_add_to_cart callback. A theme that renders add-to-cart through a fully custom mechanism won’t be detected. The remedy is to add the theme’s hook to $known_locations. – The slow path is O(all registered hooks); the fast path exists precisely to avoid paying that on every coming-soon page view.

Coming-soon template

templates/sscs-coming-soon-template.php reads the product meta and renders:

  • A countdown scaffold (#days/#hours/#minutes/#seconds) populated by JS.
  • The offer description, run through wp_kses_post + wpautop.
  • A provider branch:
  • If provider is sse, a form id is set, and SSE_Forms exists → SSE_Forms::render( (int) $sse_form_id, 'inline' ) delegates the entire form (markup, fields, AJAX) to Super Speedy Emails.
  • Otherwise → the built-in MailerLite form: a hidden honeypot name field, the email input, the optional Turnstile widget (only when sscs_captcha_enabled()), and a submit button. Below it sit the share buttons (hidden until signup succeeds).

The template is pure output; all behaviour lives in the JS.

Provider abstraction

There is no formal interface — provider choice is a per-product meta value plus a runtime capability check (class_exists('SSE_Forms')). MailerLite is always available and is the default; SSE is offered only when the sibling plugin is active. When SSE is selected the plugin steps out of the way entirely and lets SSE_Forms::render() own the form, so the AJAX handler, honeypot, rate limit and MailerLite client in this plugin are only exercised on the MailerLite path.

The two signup paths

There are two server-side entry points for a signup, and only one is live:

  • AJAX (sscs_ajax_signup_handler, live). Registered on both wp_ajax_nopriv_sscs_ajax_signup and wp_ajax_sscs_ajax_signup. This is what the template’s MailerLite form posts to. See below.
  • init POST (sscs_process_signup_form, legacy/dead). Listens for $_POST['sscs_submit'] and a sscs_signup nonce, then redirects with WC notices. The current template never renders a form with that field or nonce, so this path doesn’t fire in normal use. It’s retained history; treat it as dead code (a candidate for removal) rather than an active surface.

AJAX signup handler

sscs_ajax_signup_handler() is the heart of the live flow. Its checks run in a deliberate cheap-to-expensive order so abuse is rejected before any DB/HTTP work:

  1. Honeypot — if sscs_honeypot_enabled() and the hidden name field is non-empty, return a “Spam detected” error.
  2. Noncecheck_ajax_referer('sscs_signup_nonce','nonce'). (Public nonce; see the interesting-aspects note — not a real abuse control.)
  3. Rate limitsscs_check_rate_limit() per IP.
  4. CAPTCHA — if sscs_captcha_enabled(), require sscs_verify_turnstile().
  5. Emailsanitize_email + is_email.
  6. Productpost_id must resolve to a product post type marked _sscs_coming_soon = yes and have a MailerLite group configured. This is the M1 fix; it blocks blind hammering against arbitrary post IDs.
  7. Dedupe + persist — look up the email in wp_sscs_signups; insert a row if new (the unique email key also enforces this at the DB level).
  8. MailerLitesscs_add_subscriber_to_mailerlite().
  9. Cookie + response — set sscs_mailerlite_user_id and return the MailerLite id in the JSON success payload (feeds the referral system).

Every failure exits via wp_send_json_error; success via wp_send_json_success.

Abuse-protection layer

Three independent, settings-driven controls, all defined alongside the handler:

  • Honeypot (sscs_honeypot_enabled, option sscs_honeypot_enabled, default on). A hidden name field; if a bot fills it, the request is dropped.
  • Per-IP rate limit (sscs_check_rate_limit, always on). A transient keyed sscs_rl_<md5(ip)> counts submissions; defaults to 5 per 10 minutes, tunable via the sscs_rate_limit_max and sscs_rate_limit_window filters. Fails open when no REMOTE_ADDR is available (won’t lock out real users). Note: transient-backed, so a persistent object cache makes it effective under load.
  • Cloudflare Turnstile (sscs_verify_turnstile, option-gated, default off). sscs_captcha_enabled() returns true only when the toggle is on and both site and secret keys are set. The front-end renders the widget and loads Cloudflare’s script; the handler verifies the token server-side against challenges.cloudflare.com/turnstile/v0/siteverify and fails closed on any error. This is the only layer that defeats a distributed, IP-rotating attacker.

MailerLite client

sscs_add_subscriber_to_mailerlite( $email, $group_id ) talks to the MailerLite v2 (“connect”) REST API using the global API key from sscs_mailerlite_api_key. If no key is set it no-ops (signups still land in the local table). The flow:

  1. POST /api/subscribers with the email and groups: [group_id] — upserts the subscriber. 200 = already existed, 201 = created.
  2. Read data.id from the response → this becomes the returned subscriber id.
  3. POST /api/subscribers/{id}/groups/{group_id} — belt-and-braces group assignment in case step 1 didn’t attach it.

Errors are logged via error_log and swallowed (the user still sees success). The returned subscriber id flows back into the handler’s response, the cookie, and the mailerlite_id column.

Referral system

Entirely client-mediated, implemented in js/sscs-ajax-signup.js plus the referrer_id column:

  • On a successful signup the handler returns mailerlite_id and sets the sscs_mailerlite_user_id cookie. The JS then reveals the share buttons and rewrites the Facebook/X share URLs to include ?shared_by=<that id>.
  • When a referred visitor lands with ?shared_by=<id> in the URL, the JS stores it in the sscs_shared_by_id cookie.
  • On that visitor’s own signup, the JS posts shared_by back; the handler sanitises it and stores it as referrer_id.

So referrer_id records which existing subscriber’s share link brought this person in. The plugin stores the relationship but does not itself compute any “priority” from it — that’s left to whatever you do with the data downstream.

Countdown timer

Pure JavaScript. arrival_date is passed to the page via wp_localize_script (sscs_ajax.arrival_date) and parsed with new Date(...). A 1-second interval updates the day/hour/minute/second boxes and hides the timer once the date passes. An empty/invalid arrival date yields NaN (documented in troubleshooting) — there is no server-side guard for that.

Settings page

sscs_settings_page() under WooCommerce → Super Speedy Settings (registered with the manage_options capability). It’s a single nonce-protected form that saves five options:

Option Default Purpose
sscs_mailerlite_api_key '' Global MailerLite key.
sscs_honeypot_enabled yes Honeypot toggle.
sscs_captcha_enabled no Turnstile toggle.
sscs_turnstile_site_key '' Turnstile front-end key.
sscs_turnstile_secret_key '' Turnstile server key.

(The MailerLite API key and the Turnstile secret key are write-only in the UI: they render as masked password fields with an empty value, the stored secret is never sent to the browser, and a blank submission keeps the existing value. The Turnstile site key stays a plain text field — it’s public by design.)

Signups table

sscs_create_custom_table() runs on activation (register_activation_hook) and builds wp_sscs_signups via dbDelta:

id            BIGINT UNSIGNED  PK, auto-increment
email         VARCHAR(255)     NOT NULL, UNIQUE
mailerlite_id VARCHAR(255)     NOT NULL
referrer_id   VARCHAR(255)     NULL      -- the shared_by id, if referred
wp_user_id    BIGINT UNSIGNED  NULL      -- logged-in user, if any
created_at    TIMESTAMP        DEFAULT CURRENT_TIMESTAMP

It is a local ledger independent of MailerLite — the source of truth for “who signed up, when, and who referred them.” The UNIQUE email key is what makes the handler’s dedupe safe even under races.


Asset loading

sscs_enqueue_scripts() and sscs_enqueue_styles() both early-return unless the current page is a coming-soon single product, so nothing is loaded site-wide. The script enqueue also conditionally loads Cloudflare’s Turnstile script and localises the sscs_ajax object (ajax url, public nonce, post id, arrival date, captcha flag). dashicons is enqueued for the share-button icons.

Where to look first

  • Changing what’s collected per product → Product admin fields.
  • The form not appearing on a theme → Add-to-cart replacement ($known_locations).
  • Signup behaviour, validation, spam → AJAX signup handler + Abuse-protection layer.
  • MailerLite quirks → MailerLite client.
  • Sharing/referrals → Referral system (mostly JS).

Leave a Reply

Your email address will not be published. Required fields are marked *

×
1/1