Skip to content

Implement server-side gated content with HubSpot verification#14

Open
roborourke wants to merge 3 commits into
mainfrom
claude/magical-ptolemy-TlYyP
Open

Implement server-side gated content with HubSpot verification#14
roborourke wants to merge 3 commits into
mainfrom
claude/magical-ptolemy-TlYyP

Conversation

@roborourke

@roborourke roborourke commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the client-side <template> mechanism for inline success messages with a server-side gated content system. Success message content is no longer embedded in the page source — it is fetched from a new REST endpoint (/hubspot-form-block/v1/unlock) only after form submission. When a HubSpot private app token is configured, submissions are verified server-side against HubSpot before the content is released (strong mode); without a token, content is served immediately (best-effort mode).

Key Changes

  • New REST endpoint (inc/inline-message.php): Registers /hubspot-form-block/v1/unlock POST endpoint that:

    • Locates the form block instance by post ID, form ID, and instance index
    • Verifies HubSpot submissions against the HubSpot API (when a private app token is configured)
    • Returns server-rendered gated content HTML only after verification succeeds
    • Implements rate limiting and signed repeat-visit tokens to avoid repeated verification
  • Client-side polling (src/view.js):

    • Replaces template cloning with async unlock() function that polls the endpoint
    • Implements exponential backoff for pending submissions (up to 5 retries)
    • Extracts email from form field values and sends to endpoint for submission matching
    • Persists unlock tokens in localStorage for returning visitors
    • Strips first-submission-only content (.is-hubspot-form-first-submission) on repeat visits
    • Shows a graceful "available shortly" message if verification times out
  • Server-side rendering (src/render.php):

    • Removes <template> element emission
    • Adds gated content metadata to config: gated, postId, formId, instance, restUrl, pendingMessage
    • Removes inline message HTML rewriting (now handled server-side in the endpoint)
  • Plugin setup (hubspot-form-block.php):

    • Requires new inc/inline-message.php module
    • Registers hubspot_embed_private_app_token option for storing HubSpot private app token
  • Styling (src/style.scss):

    • Hides form content while gated content is being fetched (.is-unlocking)
    • Adds fade-in animation for pending message
  • Tests: Updated E2E tests to mock the unlock endpoint and verify the new gated content flow, including repeat-visit token persistence and first-submission group stripping

Implementation Details

  • Submission verification: Uses HubSpot's submissions API to find recent submissions matching the form ID, page URL, and (optionally) email address. Results are cached briefly (30s default) to avoid hammering the API during client polling.
  • Token signing: Repeat-visit unlock tokens are HMAC-SHA256 signed with an expiry (1 month default) to prevent tampering and allow graceful expiry.
  • Block location: Recursively collects all hubspot/form blocks in render order, expanding synced patterns (core/block), to locate the specific instance by form ID and per-form-ID instance counter.
  • Content security: Gated content is never emitted into the page source — it is only returned from the endpoint after verification, preventing unauthorized access via page inspection.

https://claude.ai/code/session_01JMzDkj2EyyGLRiQDxMVZnj

Open WordPress Playground Preview

claude added 2 commits June 3, 2026 15:57
…n page

Gated success-message content was previously rendered into a <template>
element in the page source and cloned on a client-fired event, so it could
be read via View Source or by dispatching the event from the console without
ever submitting the form.

Content is now fetched from a new POST /hubspot-form-block/v1/unlock endpoint
only after submission. The endpoint re-parses the specific post and locates
the specific form instance (per-page, per-instance), renders its inner blocks
server-side, and returns the HTML:

- Strong mode: when a HubSpot private app token is configured, the server
  verifies a real recent submission exists via HubSpot's Submissions API
  (matched by page URL, recency and email) before releasing content.
- Best-effort mode (default, no token): content is kept out of the page and
  served from the endpoint.

Repeat visits with gated content enabled replay a signed, expiring HMAC
unlock token (no HubSpot call). The endpoint is per-IP rate-limited and caches
the submissions lookup. view.js fetches/polls with backoff and shows a
graceful pending message on failure. Updated docs and E2E tests.
REMOTE_ADDR alone is unreliable behind reverse proxies, load balancers and
CDNs. Add get_client_ip() which checks common forwarded headers
(CF-Connecting-IP, True-Client-IP, X-Real-IP, X-Forwarded-For) before falling
back to REMOTE_ADDR, with hubspot_form_block_client_ip_headers and
hubspot_form_block_client_ip filters so hosts can pin resolution to their
trusted proxy setup. Used only for rate limiting, not access control.
github-actions Bot added a commit that referenced this pull request Jul 16, 2026
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Playwright E2E Test Results (PHP 8.4, WP latest)

passed  12 passed
flaky  1 flaky

Details

stats  13 tests across 4 suites
duration  2 minutes, 58 seconds
commit  5852238

Flaky tests

chromium › inline-message.spec.js › HubSpot Form — gated success message › does not emit gated content into the page source, but injects it after submission

Start and stop the Playground server programmatically via @wp-playground/cli's
runCLI in Playwright global setup/teardown, instead of spawning it as a
separate process. This follows the WordPress Playground E2E guide and removes
the brittle 'start server in the background, poll a URL, hope it's ready'
steps from the CI workflow.

- tests/global-setup.js boots the server (blueprint + auto-mounted plugin),
  honoring PLAYGROUND_PHP / PLAYGROUND_WP for the CI version matrix.
- tests/global-teardown.js disposes it.
- playwright.config.js wires globalSetup/globalTeardown and drops webServer.
- CI passes the PHP/WP matrix via env and no longer manages the server.
- Add @wp-playground/cli as a dev dependency; playground:start uses it.
github-actions Bot added a commit that referenced this pull request Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants