Global Settings. Plugin-wide defaults — per-form settings * always override these. Sub-navigated into sections (?section=x) rather * than one long page, same convention (and same nav grouping) as the reference product * Forms' own Global Settings screen. * * Two sections the reference product gates behind its paid tier — Integration * Failure Email and Double Opt-in — are deliberately left as real, free, * working features here instead of being paywalled, since ChargeForms's * whole premise is a genuinely free, fully-functional plugin. Everything * else mirrors their layout as closely as the underlying functionality * allows. */ class Settings_Page { /* Five named groups, matching design_handoff_chargeforms/README.md's Global Settings spec exactly: General / Payments / Security / Email / Advanced. License is deliberately left ungrouped (TOPLEVEL_SECTIONS) — the design's own screen map lists License as its own separate numbered admin screen, not a member of any Settings group; it still renders inside this page (moving it to a real standalone top-level menu entry is a bigger navigation change than this reorganization covers), but sits outside the five groups the same way the design's own sidebar mockup shows it as a standalone entry. */ const GENERAL_SECTIONS = [ 'layout', 'validation-messages', 'style-template', 'miscellaneous' ]; const PAYMENT_SECTIONS = [ 'payment-settings', 'payment-methods' ]; const SECURITY_SECTIONS = [ 'recaptcha', 'hcaptcha', 'turnstile', 'cleantalk' ]; const EMAIL_SECTIONS = [ 'email-delivery', 'email-summaries', 'integration-failure', 'double-optin' ]; const ADVANCED_SECTIONS = [ 'permissions', 'save-resume', 'mcp', 'address-autocomplete', 'configure-integrations' ]; const TOPLEVEL_SECTIONS = [ 'license' ]; private $license_page; public function __construct() { add_action( 'admin_menu', [ $this, 'register' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'maybe_enqueue_email_delivery_assets' ] ); add_action( 'admin_post_chargeforms_save_settings', [ $this, 'handle_save' ] ); // License_Page owns its own admin-post handlers (activate/deactivate // need a real API round trip before deciding what to store — see // its class doc comment for why that can't go through // handle_save()'s generic Settings::update() flow); it's // instantiated here purely so those handlers register, then reused // by render_license() below to render the section itself. $this->license_page = new License_Page(); } public function register() { add_submenu_page( 'chargeforms', __( 'Global Settings', 'chargeforms' ), __( 'Global Settings', 'chargeforms' ), 'chargeforms_manage_forms', 'chargeforms-settings', [ $this, 'render' ] ); } private function all_sections() { return array_merge( self::GENERAL_SECTIONS, self::PAYMENT_SECTIONS, self::SECURITY_SECTIONS, self::EMAIL_SECTIONS, self::ADVANCED_SECTIONS, self::TOPLEVEL_SECTIONS ); } /** Loaded on every Global Settings section, not just Email Delivery — the CRUD/test UI is REST-driven vanilla JS that mounts into server-rendered markup, and admin.js's no-reload nav can turn ANY section page into the Email Delivery screen without a full page reload (it swaps #chargeforms-ajax-region via innerHTML, which never re-executes scripts). So this file has to already be present on whatever section the user started on; it self-guards when its root element isn't in the DOM and mounts when one appears (see email-delivery.js's chargeforms:ajax-region-swapped listener). Same reasoning as any other per-screen script this plugin loads where it's actually used (see Menu::enqueue_assets()'s own builder-only conditional) — here "where it's used" spans every section of this one page. */ public function maybe_enqueue_email_delivery_assets() { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $section = isset( $_GET['section'] ) ? sanitize_key( wp_unslash( $_GET['section'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'chargeforms-settings' !== $page ) { return; } // Real live-site report: a normal refresh of this page kept serving a // stale copy of this script even after an update, while a hard // refresh (which bypasses the browser's own cache but still hits // whatever cached this page/its assets upstream) got the current // one. Two independent hardenings, since either layer alone can be // the culprit depending on the host: (1) filemtime() as the cache- // busting version instead of the plugin-wide CHARGEFORMS_VERSION — // changes the instant this exact file's bytes change, not just on a // version bump, so a stale-but-same-version cache entry can't // masquerade as current; (2) explicit no-store headers on the PAGE // itself below, so even a host/proxy layer that ignores the script // URL's own querystring can't serve a cached copy of the HTML that // still points at an old version. $script_path = CHARGEFORMS_PATH . 'assets/js/email-delivery.js'; $script_ver = file_exists( $script_path ) ? filemtime( $script_path ) : CHARGEFORMS_VERSION; wp_enqueue_script( 'chargeforms-email-delivery', CHARGEFORMS_URL . 'assets/js/email-delivery.js', [], $script_ver, true ); // Deliberately NOT wp_localize_script() — a real live-site report // (v0.148.0) traced back to a caching/JS-optimization plugin // stripping or reordering the inline