| @@ -37,9 +37,18 @@ | ||
| 37 | 37 | * @since 2.8.0 |
| 38 | 38 | */ |
| 39 | 39 | public function __construct() { |
| 40 | 40 | add_shortcode( self::SHORTCODE_TAG, [ $this, 'render' ] ); |
| 41 | - add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] ); | |
| 41 | + // Register the handles early (priority 1) — before Elementor/Bricks enqueue | |
| 42 | + // their widget assets on wp_enqueue_scripts — so the page-builder widgets can | |
| 43 | + // enqueue the stylesheet by handle in the <head> via get_style_depends() / | |
| 44 | + // enqueue_scripts(). Registration is unconditional and cheap; the actual | |
| 45 | + // enqueue below stays gated on the block/shortcode being present. | |
| 46 | + add_action( 'wp_enqueue_scripts', [ $this, 'register_assets' ], 1 ); | |
| 47 | + // accepted_args 0: WordPress fires this hook with an empty-string sentinel arg, | |
| 48 | + // which would land in $from_render (falsy, so harmless, but it contradicts the | |
| 49 | + // bool contract). Capping to 0 args means the bool default (false) is used. | |
| 50 | + add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ], 10, 0 ); | |
| 42 | 51 | |
| 43 | 52 | // Frontend AJAX handlers. |
| 44 | 53 | add_action( 'wp_ajax_srfm_frontend_cancel_subscription', [ $this, 'ajax_cancel_subscription' ] ); |
| 45 | 54 | } |
| @@ -44,31 +53,27 @@ | ||
| 44 | 53 | add_action( 'wp_ajax_srfm_frontend_cancel_subscription', [ $this, 'ajax_cancel_subscription' ] ); |
| 45 | 54 | } |
| 46 | 55 | |
| 47 | 56 | /** |
| 48 | - * Conditionally enqueue assets when the shortcode is present on the page. | |
| 57 | + * Register the payment-history stylesheet and script handles unconditionally so | |
| 58 | + * they can later be enqueued by handle. This exists for the Elementor | |
| 59 | + * (get_style_depends()) and Bricks (enqueue_scripts()) payment-history widgets, | |
| 60 | + * whose content lives in postmeta and so isn't caught by the has_block() / | |
| 61 | + * has_shortcode() gate in enqueue_assets(); declaring the style as a widget | |
| 62 | + * dependency lets those builders load it in the <head> and avoid a FOUC. | |
| 49 | 63 | * |
| 50 | - * Also called at render time (shortcode/block callback) to support | |
| 51 | - * FSE themes and page builders where global $post is unavailable. | |
| 64 | + * Registering (not enqueuing) keeps the assets off pages that don't use the | |
| 65 | + * feature — nothing is printed until something enqueues the handle. | |
| 52 | 66 | * |
| 53 | - * CSS is always enqueued during wp_enqueue_scripts (lightweight, prevents FOUC | |
| 54 | - * on page builders like Elementor/Bricks that store content in postmeta). | |
| 55 | - * JS + localized data are only enqueued when the shortcode/block is detected. | |
| 56 | - * | |
| 57 | - * @since 2.8.0 | |
| 67 | + * @since 2.12.3 | |
| 58 | 68 | * @return void |
| 59 | 69 | */ |
| 60 | - public function enqueue_assets() { | |
| 70 | + public function register_assets() { | |
| 61 | 71 | $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min'; |
| 62 | 72 | $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified'; |
| 63 | 73 | |
| 64 | - // Always enqueue CSS during wp_enqueue_scripts to ensure it loads in <head>. | |
| 65 | - // WordPress silently ignores late-enqueued styles (after wp_head), so page builders | |
| 66 | - // like Elementor, Bricks, and Beaver Builder (which store content in postmeta, not | |
| 67 | - // post_content) would get no CSS at all if we only enqueued conditionally. | |
| 68 | - // The CSS file is lightweight — one small stylesheet on frontend pages is acceptable. | |
| 69 | - if ( doing_action( 'wp_enqueue_scripts' ) && ! wp_style_is( 'srfm-payment-history', 'enqueued' ) ) { | |
| 70 | - wp_enqueue_style( | |
| 74 | + if ( ! wp_style_is( 'srfm-payment-history', 'registered' ) ) { | |
| 75 | + wp_register_style( | |
| 71 | 76 | 'srfm-payment-history', |
| 72 | 77 | SRFM_URL . 'assets/css/' . $dir_name . '/payment-history' . $file_prefix . '.css', |
| 73 | 78 | [], |
| 74 | 79 | SRFM_VER |
| @@ -74,17 +79,52 @@ | ||
| 74 | 79 | SRFM_VER |
| 75 | 80 | ); |
| 76 | 81 | } |
| 77 | 82 | |
| 78 | - // JS + localized data: only enqueue when the shortcode/block is actually present. | |
| 79 | - // JS can be late-enqueued (footer scripts) but CSS cannot, hence the split above. | |
| 80 | - if ( wp_script_is( 'srfm-payment-history', 'enqueued' ) ) { | |
| 81 | - return; | |
| 83 | + if ( ! wp_script_is( 'srfm-payment-history', 'registered' ) ) { | |
| 84 | + wp_register_script( | |
| 85 | + 'srfm-payment-history', | |
| 86 | + SRFM_URL . 'assets/js/payment-history.js', | |
| 87 | + [], | |
| 88 | + SRFM_VER, | |
| 89 | + true | |
| 90 | + ); | |
| 82 | 91 | } |
| 92 | + } | |
| 83 | 93 | |
| 84 | - // When called from the wp_enqueue_scripts hook, check if the shortcode/block is present. | |
| 85 | - // When called from render(), we know it's needed — skip the check. | |
| 86 | - if ( doing_action( 'wp_enqueue_scripts' ) ) { | |
| 94 | + /** | |
| 95 | + * Conditionally enqueue assets only when the payment history block/shortcode is present. | |
| 96 | + * | |
| 97 | + * Runs on wp_enqueue_scripts (where the global $post is available for detection) and | |
| 98 | + * again at render time (shortcode/block callback). Elementor and Bricks store their | |
| 99 | + * content in postmeta rather than post_content, so has_block()/has_shortcode() can't | |
| 100 | + * detect them here — those widgets instead declare the (pre-registered) stylesheet as | |
| 101 | + * a dependency so it loads in the <head> (see register_assets() + the widget classes). | |
| 102 | + * | |
| 103 | + * Both the stylesheet and the script are gated on the block/shortcode actually being | |
| 104 | + * present, so the CSS is no longer loaded on every frontend page. When enqueued from | |
| 105 | + * render() the stylesheet is printed with the footer styles, which is acceptable for the | |
| 106 | + * rare case of the block placed via an FSE template part or block widget. | |
| 107 | + * | |
| 108 | + * The stylesheet is always enqueued for a detected placement (logged in or out) so the | |
| 109 | + * login message stays styled; the script + localized nonce are enqueued only for | |
| 110 | + * logged-in users, since the cancel handler re-checks auth server-side and there is no | |
| 111 | + * `wp_ajax_nopriv_` endpoint — a logged-out visitor would only receive an inert script. | |
| 112 | + * | |
| 113 | + * @since 2.8.0 | |
| 114 | + * @since 2.12.3 Enqueue the stylesheet only when the block/shortcode is present instead of on every frontend page; withhold the script + nonce from logged-out visitors. | |
| 115 | + * @param bool $from_render Whether this is the render()-time fallback call. When | |
| 116 | + * true the block/shortcode presence gate is skipped | |
| 117 | + * because render() only runs when the widget is on the | |
| 118 | + * page. Passed explicitly rather than sniffed via | |
| 119 | + * doing_action(), which would also match a nested | |
| 120 | + * do_shortcode() invoked inside a wp_enqueue_scripts callback. | |
| 121 | + * @return void | |
| 122 | + */ | |
| 123 | + public function enqueue_assets( $from_render = false ) { | |
| 124 | + // On the wp_enqueue_scripts hook, confirm the shortcode/block is present on the | |
| 125 | + // current page. From render() we already know it is needed. | |
| 126 | + if ( ! $from_render ) { | |
| 87 | 127 | global $post; |
| 88 | 128 | |
| 89 | 129 | if ( ! $post instanceof \WP_Post ) { |
| 90 | 130 | return; |
| @@ -97,25 +137,46 @@ | ||
| 97 | 137 | return; |
| 98 | 138 | } |
| 99 | 139 | } |
| 100 | 140 | |
| 101 | - wp_enqueue_script( | |
| 102 | - 'srfm-payment-history', | |
| 103 | - SRFM_URL . 'assets/js/payment-history.js', | |
| 104 | - [], | |
| 105 | - SRFM_VER, | |
| 106 | - true | |
| 107 | - ); | |
| 141 | + // Ensure both handles exist (register_assets() is idempotent). Guarding on only | |
| 142 | + // the style handle would miss a script handle that was separately deregistered | |
| 143 | + // (asset-optimisation plugins do this by handle), leaving wp_localize_script() | |
| 144 | + // below with nothing to attach to and silently dropping the nonce. | |
| 145 | + $this->register_assets(); | |
| 108 | 146 | |
| 109 | - wp_localize_script( | |
| 110 | - 'srfm-payment-history', | |
| 111 | - 'srfm_payment_history', | |
| 112 | - [ | |
| 113 | - 'ajax_url' => admin_url( 'admin-ajax.php' ), | |
| 114 | - 'nonce' => wp_create_nonce( 'srfm_frontend_payment_nonce' ), | |
| 115 | - 'i18n' => $this->get_i18n_strings(), | |
| 116 | - ] | |
| 117 | - ); | |
| 147 | + // Enqueue the stylesheet only for pages that actually use payment history. | |
| 148 | + if ( ! wp_style_is( 'srfm-payment-history', 'enqueued' ) ) { | |
| 149 | + wp_enqueue_style( 'srfm-payment-history' ); | |
| 150 | + } | |
| 151 | + | |
| 152 | + // JS + localized data are only useful to logged-in users: the cancel handler | |
| 153 | + // re-checks authentication server-side and there is no `wp_ajax_nopriv_` | |
| 154 | + // registration, so an anonymous visitor (who only ever sees the login message) | |
| 155 | + // would receive an inert script and a pointless nonce. The CSS above still loads | |
| 156 | + // so the login message stays styled; only the script + localize are gated here. | |
| 157 | + if ( ! is_user_logged_in() ) { | |
| 158 | + return; | |
| 159 | + } | |
| 160 | + | |
| 161 | + if ( ! wp_script_is( 'srfm-payment-history', 'enqueued' ) ) { | |
| 162 | + wp_enqueue_script( 'srfm-payment-history' ); | |
| 163 | + } | |
| 164 | + | |
| 165 | + // Gate the localize on whether the data is already attached, not on the enqueued | |
| 166 | + // state: the handle is now registered on every frontend page, so a foreign | |
| 167 | + // enqueue-by-handle before this runs must not cause the nonce to be skipped. | |
| 168 | + if ( ! wp_scripts()->get_data( 'srfm-payment-history', 'data' ) ) { | |
| 169 | + wp_localize_script( | |
| 170 | + 'srfm-payment-history', | |
| 171 | + 'srfm_payment_history', | |
| 172 | + [ | |
| 173 | + 'ajax_url' => admin_url( 'admin-ajax.php' ), | |
| 174 | + 'nonce' => wp_create_nonce( 'srfm_frontend_payment_nonce' ), | |
| 175 | + 'i18n' => $this->get_i18n_strings(), | |
| 176 | + ] | |
| 177 | + ); | |
| 178 | + } | |
| 118 | 179 | } |
| 119 | 180 | |
| 120 | 181 | /** |
| 121 | 182 | * Render the payment history shortcode. |
| @@ -138,15 +199,19 @@ | ||
| 138 | 199 | if ( $per_page <= 0 ) { |
| 139 | 200 | $per_page = 10; |
| 140 | 201 | } |
| 141 | 202 | |
| 203 | + // Enqueue assets at render time — the last-resort fallback for FSE template | |
| 204 | + // parts / block widgets where $post can't be detected on wp_enqueue_scripts and | |
| 205 | + // there is no builder style-dependency API. Elementor/Bricks widgets enqueue the | |
| 206 | + // stylesheet in the <head> via their own dependency hooks, so this mainly serves | |
| 207 | + // the genuinely rare FSE case (footer-loaded CSS, acceptable there). Runs before | |
| 208 | + // the logged-out early return so the login message is styled too. | |
| 209 | + $this->enqueue_assets( true ); | |
| 210 | + | |
| 142 | 211 | if ( ! is_user_logged_in() ) { |
| 143 | 212 | return $this->get_login_message(); |
| 144 | 213 | } |
| 145 | - | |
| 146 | - // Enqueue assets at render time — handles FSE themes, Elementor, and | |
| 147 | - // other page builders where global $post is unavailable during wp_enqueue_scripts. | |
| 148 | - $this->enqueue_assets(); | |
| 149 | 214 | |
| 150 | 215 | $user_id = get_current_user_id(); |
| 151 | 216 | $where = $this->build_where_conditions( $user_id, $atts ); |
| 152 | 217 | |