Update — 2026-06-08, supersedes the DRY recommendation in this report. Product decided cp10 will be a standalone engine: fully isolated from cp8/cp9, with its own copy of every module and no shared code. The goal is fault isolation — a break in cp8/cp9 can't affect cp10, and a break in cp10 can't affect cp8/cp9 — chosen deliberately over deduplication. So the "consolidate the forks / create static/shared/" thesis below is no longer the plan; read the duplication analysis as background, not a recommendation. The load contract, the no-hidden-genius cleanups, the page↔engine split, and the payment-method signal all still stand.
Proposal · not shipped · AI-drafted

cp10: a clean, standalone checkout engine

The architectural upgrade is a standalone engine the cp9 page adopts once it proves itself — its own build, its own copy of every module, zero shared code with cp8 or cp9, so the blast radius of any change stays contained to one engine. cp8 and cp9 remain untouched and independent. The bar is explicit — tested, documented, and intuitive, with the clever-but-fragile bits removed (no hidden genius). (The duplication/DRY framing in the sections below predates this decision — see the update banner above.)

Read this as a design proposal, not a changelog. Everything in the Verified blocks is checked against the code at commit c9f58e92 with file:line citations. Everything in the Proposal blocks is a recommended direction that has not been built or approved — cp-eet_10 exists nowhere in the repo today.
Verified against code

What exists today

A checkout page (layout, offers, pricing — plain PHP) does not contain the payment engine. It fetches it at runtime and injects the HTML into its own DOM:

// page picks the engine URL by environment
cp-eet_9-layout-update/index.php:26   $cpEngineUrl = ".../p/?page=checkout-eet-digital-inline-form-cp9"
cp-eet_9-layout-update/index.php:1069 ETUtils.epCheckout.load({ cp_engine_url: cpEngineUrl, ... })

// the loader: same-origin AJAX, injected into the parent document (NOT an iframe)
utils/et_utils.js:1195  $.ajax({ url: cpEngineUrl,
utils/et_utils.js:1198    success: res => $('.epcheckout-content').html(res) })

So the page↔engine boundary is a runtime fetch, not a security sandbox. PCI scope reduction comes from Braintree/Stripe hosted fields inside the engine bundle, not from this split. The split exists for lazy-loading, caching, and build isolation — which is worth keeping. What is not worth keeping is how the engine is duplicated per page.

Two engines, ~75–80% identical

Page (PHP)Engine it loads (webpack SPA)engine index.jsProd delivery
cp-eet_8
+ ~19 campaign aliases
checkout-eet-digital-inline-form 2,506 lines CDN-cached .html?v=
cp-eet_9-layout-update checkout-eet-digital-inline-form-cp9 3,022 lines live PHP route

Both live simultaneously: cp9 is launch-gated at 100% of cp-eet_9 traffic; cp8 still serves the cp-eet/cp-eet_5 family. Engine sizes from wc -l; overlap estimate from a structural diff (see Validate). The getErrMsg() decline-mapping block, for example, is word-for-word identical in both.

Architecture — today vs. cp10

cp-eet_8 (page) + ~19 aliases cp-eet_9 (page) layout-update engine: inline-form 2,506 lines · own webpack own node_modules · own dist/ engine: inline-form-cp9 3,022 lines · own webpack own node_modules · own dist/ load() ~75–80% copy-paste overlap

Two pages, two complete engine forks. Every shared fix (e.g. the Mixpanel-parity tweak) must be applied — and rebuilt — twice.

Verified

Why change it — the friction today

Duplication that must be maintained twice

Two engines, two node_modules trees, two webpack builds, two checked-in dist/bundle.js artifacts — so two separate deploy-whitelist rules (the prime warns a pipeline that filters dist/ silently breaks the form).

The shared module is half-built

cp9's webpack.config.js:11 already resolves ../../shared — but static/shared/ does not exist. The 6 modules cp9 extracted (forter-card-params.js, subscribe-button-text.js, three-d-secure-result.js, …) still live only inside cp9.

The contract is implicit

The page↔engine handshake — cp_engine_url, window.epCheckoutParams, postMessage events, window.newSubData — is undocumented. New pages copy cp9 by osmosis, which is how forks start.

Inconsistent delivery

cp8 ships its engine as a CDN-cached versioned .html?v=; cp9 serves a live PHP route (index.php:26). Two delivery models, two cache stories, for the same kind of asset.

Proposal

Design principles — what "no longer legacy" means

These are the non-negotiables that make cp10 an upgrade rather than a third fork. Each one is concrete, with a real thing in today's code it is meant to fix.

1 · No dead weight

One engine, no parallel forks. Clear the genuinely dead: the commented-out loadStripe blocks in checkout-v4-11/d and the Google-Pay path scaffolded then parity-removed. (Stripe stays a deliberate option — unused in this engine but a live gateway elsewhere; see Payment methods.) If it is not wired and reachable, it either ships working or it is removed — not left half-in.

2 · Actually tested

The tooling is already there — the engine's package.json wires jest + @playwright/test, and the 6 extracted modules are pure functions built to be testable. cp10's bar: real unit coverage on static/shared/ and an e2e test that exercises the load contract, card + PayPal.

3 · Documented contract

The page↔engine handshake is written down (see next section) and the prime becomes the engine area index. A new page should be addable by reading one doc, not by reverse-engineering cp9.

4 · Intuitive — no hidden-genius

Kill the clever-but-fragile bits. Three real examples to replace: substring error-mapping (getErrMsg() does res.includes('Declined') — the exact reason our PayPal 400 showed the wrong banner); the checkoutEngineVersion += … string-mutation that grows on every offer switch; and the vestigial postMessage(…, '*') from a former iframe design. Explicit codes, immutable config, one messaging path.

Proposal

How cp10 works — document the load contract

"More intuitive and better documented" mostly means writing down the handshake that already exists, then making the engine a single artifact behind it. This is the runtime flow cp10 would formalize (it is what the code does today — cp10 names and documents it):

flowchart TD
    P["Page (PHP)
cp-eet_9 / future"] -->|"1 · cp_engine_url + epCheckoutParams"| L["ETUtils.epCheckout.load()
utils/et_utils.js:1120"] L -->|"2 · same-origin AJAX GET"| E["cp10 engine core
(one build, one cache key)"] E -->|"3 · HTML injected into .epcheckout-content"| P E -->|"4 · braintree / stripe hosted fields = PCI boundary"| GW["Payment gateway"] E -->|"5 · postMessage + window.newSubData"| P P -->|"6 · SUBSCRIPTION_CREATED"| OUT["onboarding / analytics"]

Steps 1–3 are the load; 4 is where PCI scope actually lives (inside the engine, unchanged by cp10); 5–6 are the result handshake the contract must spell out. Derived from et_utils.js:1120–1202 and cp-eet_9-layout-update/index.php.

Proposal

"Rolling" — how cp10 replaces cp9 without a big-bang

flowchart LR
    A["Phase 0
Create static/shared/
move cp9's 6 modules"] --> B["Phase 1
cp10 engine core
= cp9 engine + shared imports"] B --> C["Phase 2
cp9 page repoints
cp_engine_url → cp10"] C --> D["Phase 3
cp8 engine retired
once cp-eet_8 traffic moves"] D --> E["End state
one engine · one build
documented contract"] style A fill:#fef3c7,stroke:#b45309 style E fill:#dcfce7,stroke:#15803d

Each phase is independently shippable and reversible. Phase 0–1 change nothing a user sees (pure refactor behind the same bundle). Phase 2 is a one-line URL repoint behind the existing cp-eet-9-layout-update-launch-gated A/B gate. Phase 3 is deletion, gated on cp8's campaign family having moved to the cp9 page — so it is the last step, not a precondition.

Proposal

What would change

PathChangeRisk
p/static/shared/ (new)Create it; move cp9's 6 extracted modules hereLow — webpack already resolves the path
checkout-eet-digital-inline-form-cp9/Import the moved modules from shared/; becomes the cp10 coreLow — same code, new import sites
checkout-eet-digital-inline-form/ (cp8 engine)Retired in Phase 3 after traffic movesDeferred — deletion only
cp-eet_9-layout-update/index.php:26Repoint $cpEngineUrl; adopt cp8's cached-versioned deliveryLow — one-line, A/B-gated
prime-cp9.md / new prime-cp10.mdDocument the load contract; make it the engine area indexNone — docs
Naming note: cp-eet_N is the repo's page-variant namespace. The upgrade is an engine, so "cp10" reads naturally as the next page but would be a mis-named engine. Recommend a real engine name (e.g. checkout-eet-engine) and keep "cp10" for the page that adopts it, if a new page is even needed.
Forward signal · US · not in scope

Payment methods — what we have, what's an easy add (US)

Requested as a signal, not a work item — and scoped to the US market. Today the ET digital-subscription engine offers Card, PayPal, and Apple Pay (Google Pay is scaffolded but parity-removed), all on the Braintree gateway — specifically the digital gateway (constants/checkout.js:20), the same 4aAkvOxZ… id seen in the PayPal 400. Stripe is unused in this engine.

The cheap win: Venmo

Venmo is a Braintree/PayPal product. With PayPal + Braintree already wired, it's the lowest-effort new method by far, and large with younger US buyers. Nowhere in the code today. If you add one thing, add this.

MethodWhy it matters (US)EffortVerdict
VenmoBig US mindshare; one-tap for app usersLow — same provider as PayPal (Braintree)Easiest high-impact add
Stripe LinkOne-click for returning customers — the SMS one-time-code prompt you see on the EpochTV / donation / AE-magazine checkouts is LinkMedium — already in-house on the Stripe gateway, but this engine runs Braintree, so it means wiring a Stripe pathProven internally; not on this engine yet
Cash App PayYounger US demographicLow–Med — Braintree-supportedReasonable add
ACH bank debitLower fees, fewer declines, better recurring retentionMedium — Braintree US bank account; a GoCardless direct-debit gateway is already in constants/checkout.js:3Strategic for subscriptions
BNPL (Klarna / Afterpay)Popular generallyMedSkip — poor fit for a $4/mo subscription
The Stripe Link SMS you noticed is real and in-house: it runs on the org's Stripe-gateway checkouts (EpochTV, donation, American Essence magazine — constants/checkout.js:28–48, via shared/common/checkout/src/index.ts:171). The ET digital subscription cp10 targets runs on Braintree, so Link isn't available there yet. cp10's documented single-engine contract is what makes adding Venmo — or wiring a Stripe/Link path — a contained change instead of a two-fork edit. (Effort ratings are an informed first pass, not vendor-confirmed.)

Validate the current-state claims yourself

Nothing here is shipped, so "verify" means reproducing the facts the proposal rests on:

# the page fetches the engine at runtime (same-origin, injected — not an iframe)
grep -n 'epcheckout-content' web/checkout-eet/utils/et_utils.js        # → :1198 .html(res)

# two engine forks, their sizes
wc -l web/checkout-eet/p/static/checkout-eet-digital-inline-form/src/index.js \
      web/checkout-eet/p/static/checkout-eet-digital-inline-form-cp9/src/index.js

# how much overlaps (structural diff)
diff web/checkout-eet/p/static/checkout-eet-digital-inline-form/src/index.js \
     web/checkout-eet/p/static/checkout-eet-digital-inline-form-cp9/src/index.js | wc -l

# the shared-module path is referenced but the dir is absent
grep -n shared web/checkout-eet/p/static/checkout-eet-digital-inline-form-cp9/webpack.config.js
ls    web/checkout-eet/p/static/shared    # → No such file or directory

# two delivery models for the same asset
grep -n 'cpEngineUrl =' web/checkout-eet/p/static/cp-eet_8/index.php             # → cached .html?v=
grep -n 'cpEngineUrl =' web/checkout-eet/p/static/cp-eet_9-layout-update/index.php  # → live PHP route

# cp-eet_10 / engine-update exists nowhere yet
grep -rn 'cp-eet_10\|engine-update' web/ || echo "none found"

Open questions before this becomes a plan

Report generated 2026-06-08 19:02 · repo @ c9f58e92 · current-state claims verified against code, cp10 design is an unreviewed proposal. Companion analysis from this session's architecture exploration; no REQ exists yet.