PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.4.1
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.4.1
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
← All changes | includes/Campaign/CPT.php +170 -25 2.2.0 → 2.4.1 View file →
@@ -2,10 +2,12 @@
2 2
3 3 namespace Better_Payment\Lite\Campaign;
4 4
5 5 use Better_Payment\Lite\Admin\DB;
6 +use Better_Payment\Lite\AI\AIManager;
6 7 use Better_Payment\Lite\Controller;
7 8 use Better_Payment\Lite\Campaign\Elements\ElementRegistry;
9 +use Better_Payment\Lite\Campaign\Templates\CategoryRegistry;
8 10 use Better_Payment\Lite\Campaign\Templates\TemplateManager;
9 11
10 12 if ( ! defined( 'ABSPATH' ) ) {
11 13 exit;
@@ -49,13 +51,18 @@
49 51 register_post_type( 'bp_campaign', $args );
50 52 }
51 53
52 54 /**
53 - * Register the hidden campaign builder page under the Better Payment menu.
55 + * Register the campaign builder page under the Better Payment menu.
56 + *
57 + * Registered as a real submenu of 'better-payment-admin' (not orphaned with
58 + * empty parent) so WordPress keeps the sidebar open and the Campaigns item
59 + * highlighted automatically. The submenu link is hidden via CSS so it doesn't
60 + * appear as a visible menu entry.
54 61 */
55 62 public function register_builder_page() {
56 63 add_submenu_page(
57 - '',
64 + 'better-payment-admin',
58 65 __( 'Campaign Builder', 'better-payment' ),
59 66 __( 'Campaign Builder', 'better-payment' ),
60 67 'manage_options',
61 68 'bp-campaign-builder',
@@ -61,16 +68,20 @@
61 68 'bp-campaign-builder',
62 69 [ $this, 'render_builder_page' ]
63 70 );
64 71
65 - // Hidden pages (parent = '') are not found by get_admin_page_title(), leaving
66 - // $title null and causing a strip_tags() deprecation in admin-header.php.
67 - // Set the global before the header loads.
68 - add_action( 'current_screen', static function ( $screen ) {
69 - if ( 'admin_page_bp-campaign-builder' === $screen->id ) {
70 - global $title;
71 - $title = __( 'Campaign Builder', 'better-payment' );
72 + // Hide the submenu link — it should never appear in the sidebar.
73 + add_action( 'admin_head', static function () {
74 + echo '<style>#adminmenu a[href="admin.php?page=bp-campaign-builder"]{display:none!important}</style>';
75 + } );
76 +
77 + // Redirect the active-submenu highlight from the hidden "Campaign Builder"
78 + // entry to the visible "Campaigns" tab so it appears selected in the sidebar.
79 + add_filter( 'submenu_file', static function ( $submenu_file ) {
80 + if ( isset( $_GET['page'] ) && 'bp-campaign-builder' === $_GET['page'] ) {
81 + return 'better-payment-admin&tab=campaigns';
72 82 }
83 + return $submenu_file;
73 84 } );
74 85 }
75 86
76 87 /**
@@ -111,8 +122,13 @@
111 122 $campaign = null;
112 123 $campaign_id = 0;
113 124 }
114 125
126 + // Optional start mode for a new campaign: `start=ai` opens straight into the
127 + // editor with the AI Assistant ready (from the "Campaign With AI" button).
128 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
129 + $start = isset( $_GET['start'] ) ? sanitize_key( wp_unslash( $_GET['start'] ) ) : '';
130 +
115 131 wp_enqueue_script( 'bp-campaign-builder' );
116 132 wp_enqueue_style( 'bp-campaign-builder' );
117 133
118 134 $nonce = wp_create_nonce( 'wp_rest' );
@@ -119,8 +135,9 @@
119 135
120 136 ?>
121 137 <div id="bp-campaign-builder"
122 138 data-campaign-id="<?php echo esc_attr( $campaign_id ); ?>"
139 + data-start="<?php echo esc_attr( $start ); ?>"
123 140 data-rest-url="<?php echo esc_url( rest_url( 'better-payment/v1/' ) ); ?>"
124 141 data-nonce="<?php echo esc_attr( $nonce ); ?>"
125 142 data-admin-url="<?php echo esc_url( admin_url() ); ?>"
126 143 data-campaigns-url="<?php echo esc_url( admin_url( 'admin.php?page=better-payment-admin&tab=campaigns' ) ); ?>"
@@ -231,16 +248,52 @@
231 248 $version
232 249 );
233 250
234 251 wp_localize_script( 'bp-campaign-list', 'betterPaymentCampaignData', [
235 - 'templates' => array_values( TemplateManager::get_all() ),
236 - 'restUrl' => rest_url( 'better-payment/v1/' ),
237 - 'nonce' => wp_create_nonce( 'wp_rest' ),
238 - 'adminUrl' => admin_url(),
252 + // Picker-facing — retired designs are filtered out here, never in
253 + // get_all(), which the renderer reads on every campaign pageview.
254 + 'templates' => array_values( TemplateManager::get_for_picker() ),
255 + 'categories' => CategoryRegistry::for_client(),
256 + 'restUrl' => rest_url( 'better-payment/v1/' ),
257 + 'nonce' => wp_create_nonce( 'wp_rest' ),
258 + 'adminUrl' => admin_url(),
239 259 ] );
240 260 }
241 261
242 262 /**
263 + * Cache-busting version for a built asset — its mtime, not the plugin version.
264 + *
265 + * The builder bundle is rebuilt far more often than BETTER_PAYMENT_VERSION is
266 + * bumped, so versioning on the plugin version pinned every developer, and every
267 + * site updated in place, to whichever bundle their browser cached first. That is
268 + * how a Pro install kept rendering the *free* palette — crowned, dashed, amber
269 + * icons — and the free upgrade banner long after Pro was active: the localized
270 + * `proEnabled` is printed inline and was always correct, only the JS that reads
271 + * it was months stale.
272 + *
273 + * Falls back to the plugin version if the file is missing (an incomplete build),
274 + * which is no worse than the old behaviour.
275 + *
276 + * @param string $relative_path Path below the plugin root, with a leading slash.
277 + * @return string|int
278 + */
279 + private static function asset_version( $relative_path ) {
280 + $file = self::asset_path( $relative_path );
281 +
282 + return file_exists( $file ) ? filemtime( $file ) : BETTER_PAYMENT_VERSION;
283 + }
284 +
285 + /**
286 + * Absolute path to a built asset.
287 + *
288 + * @param string $relative_path Path below the plugin root, with a leading slash.
289 + * @return string
290 + */
291 + private static function asset_path( $relative_path ) {
292 + return BETTER_PAYMENT_PATH . $relative_path;
293 + }
294 +
295 + /**
243 296 * Enqueue campaign builder assets on the builder page only.
244 297 */
245 298 public function enqueue_builder_assets() {
246 299 $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
@@ -248,15 +301,13 @@
248 301 if ( $page !== 'bp-campaign-builder' ) {
249 302 return;
250 303 }
251 304
252 - $version = defined( 'WP_DEBUG' ) && WP_DEBUG ? time() : BETTER_PAYMENT_VERSION;
253 -
254 305 wp_register_script(
255 306 'bp-campaign-builder',
256 307 BETTER_PAYMENT_ASSETS . '/admin/campaign-builder/campaign-builder.min.js',
257 308 [ 'react', 'react-dom', 'wp-element', 'wp-api-fetch', 'wp-i18n' ],
258 - $version,
309 + self::asset_version( '/assets/admin/campaign-builder/campaign-builder.min.js' ),
259 310 true
260 311 );
261 312
262 313 wp_register_style(
@@ -262,30 +313,60 @@
262 313 wp_register_style(
263 314 'bp-campaign-builder',
264 315 BETTER_PAYMENT_ASSETS . '/admin/campaign-builder/campaign-builder.min.css',
265 316 [],
266 - $version
317 + self::asset_version( '/assets/admin/campaign-builder/campaign-builder.min.css' )
267 318 );
268 319
269 - // Load campaign display CSS so the preview modal renders correctly.
270 - $display_css = BETTER_PAYMENT_PATH . '/assets/blocks/campaign-display/style.min.css';
271 - if ( file_exists( $display_css ) ) {
320 + // Load campaign display CSS so the preview modal renders correctly. Still
321 + // guarded on existence — this one is optional (an unbuilt blocks directory
322 + // is a normal dev state), and enqueuing a URL that 404s is worse than
323 + // skipping it.
324 + $display_css = '/assets/blocks/campaign-display/style.min.css';
325 + if ( file_exists( self::asset_path( $display_css ) ) ) {
272 326 wp_enqueue_style(
273 327 'better-payment-campaign-display-style',
274 328 BETTER_PAYMENT_ASSETS . '/blocks/campaign-display/style.min.css',
275 329 [],
276 - filemtime( $display_css )
330 + self::asset_version( $display_css )
277 331 );
278 332 }
279 333
334 + // Template theme CSS for the preview modal: fires
335 + // `better_payment/campaign/enqueue_styles` ('builder'), where Pro loads its
336 + // themes. A free install loads no Pro design here. See CampaignStyles.
337 + CampaignStyles::enqueue_theme_styles( 'builder' );
338 +
280 339 wp_enqueue_media();
281 340
282 341 // Zero out all WP admin chrome spacing so the builder fills edge-to-edge.
342 + // The footer styling mirrors the Better Payment admin footer (see
343 + // ReactAdmin::get_footer_version) so the builder page\'s branded footer
344 + // looks identical to the other dashboard pages — the heavy React admin
345 + // stylesheet is not loaded here, so the rules are inlined.
283 346 wp_add_inline_style( 'bp-campaign-builder', '
284 347 #wpcontent { padding-left: 0 !important; }
285 348 #adminmenushadow { display: none !important; }
349 + /* Match the WP admin surfaces to the builder background ($bg
350 + #f4f5f8) so the empty band below a short builder blends in
351 + instead of showing WordPress\'s default #f0f0f1. body/#wpwrap are
352 + the elements that stay full-height — the content-column elements
353 + (#wpcontent/#wpbody/#wpbody-content) collapse to the builder
354 + height, so recoloring only those leaves the body grey exposed. */
355 + body.wp-admin, #wpwrap, #wpcontent, #wpbody, #wpbody-content { background: #f4f5f8 !important; }
356 + /* Editor tab is a uniform white workspace (white canvas + white
357 + sidebar), so whiten the backstop too — App.js toggles
358 + body.bp-cb-tab-editor with the active tab. */
359 + body.bp-cb-tab-editor, body.bp-cb-tab-editor #wpwrap, body.bp-cb-tab-editor #wpcontent, body.bp-cb-tab-editor #wpbody, body.bp-cb-tab-editor #wpbody-content { background: #fff !important; }
286 360 #wpbody-content { overflow: hidden !important; padding: 0 !important; }
287 361 #wpbody-content .wrap { margin: 0 !important; padding: 0 !important; max-width: none !important; }
362 + #wpfooter .alignright { gap: 30px; display: flex; }
363 + #wpfooter .alignright, #wpfooter .alignleft { color: #6a758c; font-weight: 400; font-size: 14px; }
364 + #wpfooter .alignright a, #wpfooter .alignleft a { font-weight: 500; color: #6b59ee; }
365 + #wpfooter .alignright .bp-footer-version, #wpfooter .alignleft .bp-footer-version { padding: 4px 8px; border-radius: 20px; margin: 0 8px; color: #6b59ee; background-color: #fcfcfc; }
366 + #wpfooter .alignright .bp-footer-version-divider, #wpfooter .alignleft .bp-footer-version-divider { position: relative; }
367 + #wpfooter .alignright .bp-footer-version-divider::after, #wpfooter .alignleft .bp-footer-version-divider::after { position: absolute; content: ""; background-color: #b9bfca; padding: 1px; top: 2px; bottom: 2px; right: -11px; }
368 + #wpfooter .bp-free-version { display: flex; align-items: center; gap: 2px; }
288 369 ' );
289 370
290 371 // Global currency from plugin settings — builder uses this everywhere.
291 372 $global_currency = DB::get_settings( 'better_payment_settings_general_general_currency' );
@@ -292,15 +373,79 @@
292 373 if ( ! is_string( $global_currency ) || $global_currency === '' ) {
293 374 $global_currency = 'USD';
294 375 }
295 376
296 - // Localize element registry, templates, and global settings to JS builder.
297 - wp_localize_script( 'bp-campaign-builder', 'betterPaymentCampaignData', [
377 + $data = [
298 378 'elements' => array_values( ElementRegistry::get_all() ),
299 - 'templates' => array_values( TemplateManager::get_all() ),
379 + // Picker-facing — retired designs are filtered out here, never in
380 + // get_all(), which the renderer reads on every campaign pageview.
381 + 'templates' => array_values( TemplateManager::get_for_picker() ),
382 + // `template key => theme_class`, over the WHOLE registry rather than
383 + // the picker: a campaign on a retired design still renders under its
384 + // theme class, and the Advanced tab's Background control reads this
385 + // to resolve what that theme actually paints (utils/themeBackground.js).
386 + // Keyed, not a list — the builder looks a single key up.
387 + 'templateThemes' => TemplateManager::theme_classes(),
388 + // The category taxonomy, shared by the template picker's sidebar and
389 + // the AI wizard's "What are you raising funds for?" tiles. Both used
390 + // to hardcode their own list and drifted apart; this is the one list.
391 + 'categories' => CategoryRegistry::for_client(),
300 392 'globalCurrency' => $global_currency,
393 + // Which weekday the AI wizard's calendar starts on (0 = Sunday), per
394 + // Settings → General. Without it the grid would always be Sunday-first.
395 + 'startOfWeek' => (int) get_option( 'start_of_week', 0 ),
301 396 'restUrl' => rest_url( 'better-payment/v1/' ),
302 397 'nonce' => wp_create_nonce( 'wp_rest' ),
303 398 'pluginUrl' => plugins_url( '', BETTER_PAYMENT_BASENAME ),
304 - ] );
399 + 'proEnabled' => (bool) apply_filters( 'better_payment/pro_enabled', false ),
400 + 'upgradeUrl' => 'https://wpdeveloper.com/in/upgrade-better-payment-pro',
401 + // Seed the AI enabled/configured flags synchronously so the AI panel's
402 + // "disabled" / "add an API key" notice paints immediately instead of
403 + // flickering in after the async /ai/config round-trip resolves. The
404 + // panel still fetches the full config (providers, operations) after
405 + // mount; this only pre-answers the two flags the notice reads.
406 + 'aiConfig' => self::ai_config_seed(),
407 + ];
408 +
409 + /*
410 + * NOT wp_localize_script(). That function was built for L10n strings and
411 + * casts every *scalar* in the array to a string on the way out
412 + * (`$l10n[ $key ] = html_entity_decode( (string) $value, ... )` in
413 + * WP_Scripts::localize). Arrays survive; booleans and ints do not.
414 + *
415 + * `proEnabled => true` therefore reached JS as the string "1", and App.js
416 + * tested it with `=== true`. That comparison was never once true on any
417 + * install — which is why an active Pro licence still drew crowns on the
418 + * palette, disabled every control in the settings panel, and showed the
419 + * free upgrade banner. PHP was right the whole way down; the boolean died
420 + * in transport.
421 + *
422 + * wp_add_inline_script + wp_json_encode preserves real types, and is what
423 + * Blocks\BlockManager already does for window.betterPaymentBlockData —
424 + * which is precisely why the identical `=== true` check works over there.
425 + * Position 'before' puts it ahead of the bundle, same ordering as
426 + * wp_localize_script gave us.
427 + */
428 + wp_add_inline_script(
429 + 'bp-campaign-builder',
430 + 'window.betterPaymentCampaignData = ' . wp_json_encode( $data ) . ';',
431 + 'before'
432 + );
433 + }
434 +
435 + /**
436 + * The two AI flags the builder's AI panel needs at first paint: whether the
437 + * feature is enabled, and whether the active provider has an API key. Mirrors
438 + * the `enabled` / `configured` fields of the `/ai/config` REST response so the
439 + * seeded value is drop-in compatible with what the async fetch returns later.
440 + *
441 + * @return array{ enabled: bool, configured: bool }
442 + */
443 + private static function ai_config_seed(): array {
444 + $provider = AIManager::active_provider();
445 +
446 + return [
447 + 'enabled' => AIManager::is_enabled(),
448 + 'configured' => null !== $provider && $provider->is_configured(),
449 + ];
305 450 }
306 451 }