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.
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.)
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.
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.
| Page (PHP) | Engine it loads (webpack SPA) | engine index.js | Prod 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.
Two pages, two complete engine forks. Every shared fix (e.g. the Mixpanel-parity tweak) must be applied — and rebuilt — twice.
One engine core, fed by real shared modules. Pages become thin adapters that pass config through a written contract. A fix lands once.
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).
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 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.
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.
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.
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.
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.
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.
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.
"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.
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.
| Path | Change | Risk |
|---|---|---|
p/static/shared/ (new) | Create it; move cp9's 6 extracted modules here | Low — webpack already resolves the path |
checkout-eet-digital-inline-form-cp9/ | Import the moved modules from shared/; becomes the cp10 core | Low — same code, new import sites |
checkout-eet-digital-inline-form/ (cp8 engine) | Retired in Phase 3 after traffic moves | Deferred — deletion only |
cp-eet_9-layout-update/index.php:26 | Repoint $cpEngineUrl; adopt cp8's cached-versioned delivery | Low — one-line, A/B-gated |
prime-cp9.md / new prime-cp10.md | Document the load contract; make it the engine area index | None — docs |
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.
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.
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.
| Method | Why it matters (US) | Effort | Verdict |
|---|---|---|---|
| Venmo | Big US mindshare; one-tap for app users | Low — same provider as PayPal (Braintree) | Easiest high-impact add |
| Stripe Link | One-click for returning customers — the SMS one-time-code prompt you see on the EpochTV / donation / AE-magazine checkouts is Link | Medium — already in-house on the Stripe gateway, but this engine runs Braintree, so it means wiring a Stripe path | Proven internally; not on this engine yet |
| Cash App Pay | Younger US demographic | Low–Med — Braintree-supported | Reasonable add |
| ACH bank debit | Lower fees, fewer declines, better recurring retention | Medium — Braintree US bank account; a GoCardless direct-debit gateway is already in constants/checkout.js:3 | Strategic for subscriptions |
| BNPL (Klarna / Afterpay) | Popular generally | Med | Skip — poor fit for a $4/mo subscription |
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.)
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"
cp-eet_8 campaign family has moved to the cp9 page. Is that on the roadmap, or
will cp8 live indefinitely? If indefinitely, the win is the shared core (Phase 0–1), not retirement.pci_monitoring.php:13 tag map but absent from the $ferootPages
Feroot-injection list (:48, which lists only cp-eet_8 + checkout-v4-11b).
Decide whether cp10 should inherit Feroot client-side monitoring. (Flagged for
confirmation — not yet established whether this is intentional.)
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.