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 +130 -16 2.2.2 → 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;
@@ -120,8 +122,13 @@
120 122 $campaign = null;
121 123 $campaign_id = 0;
122 124 }
123 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 +
124 131 wp_enqueue_script( 'bp-campaign-builder' );
125 132 wp_enqueue_style( 'bp-campaign-builder' );
126 133
127 134 $nonce = wp_create_nonce( 'wp_rest' );
@@ -128,8 +135,9 @@
128 135
129 136 ?>
130 137 <div id="bp-campaign-builder"
131 138 data-campaign-id="<?php echo esc_attr( $campaign_id ); ?>"
139 + data-start="<?php echo esc_attr( $start ); ?>"
132 140 data-rest-url="<?php echo esc_url( rest_url( 'better-payment/v1/' ) ); ?>"
133 141 data-nonce="<?php echo esc_attr( $nonce ); ?>"
134 142 data-admin-url="<?php echo esc_url( admin_url() ); ?>"
135 143 data-campaigns-url="<?php echo esc_url( admin_url( 'admin.php?page=better-payment-admin&tab=campaigns' ) ); ?>"
@@ -240,16 +248,52 @@
240 248 $version
241 249 );
242 250
243 251 wp_localize_script( 'bp-campaign-list', 'betterPaymentCampaignData', [
244 - 'templates' => array_values( TemplateManager::get_all() ),
245 - 'restUrl' => rest_url( 'better-payment/v1/' ),
246 - 'nonce' => wp_create_nonce( 'wp_rest' ),
247 - '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(),
248 259 ] );
249 260 }
250 261
251 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 + /**
252 296 * Enqueue campaign builder assets on the builder page only.
253 297 */
254 298 public function enqueue_builder_assets() {
255 299 $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
@@ -257,15 +301,13 @@
257 301 if ( $page !== 'bp-campaign-builder' ) {
258 302 return;
259 303 }
260 304
261 - $version = defined( 'WP_DEBUG' ) && WP_DEBUG ? time() : BETTER_PAYMENT_VERSION;
262 -
263 305 wp_register_script(
264 306 'bp-campaign-builder',
265 307 BETTER_PAYMENT_ASSETS . '/admin/campaign-builder/campaign-builder.min.js',
266 308 [ 'react', 'react-dom', 'wp-element', 'wp-api-fetch', 'wp-i18n' ],
267 - $version,
309 + self::asset_version( '/assets/admin/campaign-builder/campaign-builder.min.js' ),
268 310 true
269 311 );
270 312
271 313 wp_register_style(
@@ -271,22 +313,30 @@
271 313 wp_register_style(
272 314 'bp-campaign-builder',
273 315 BETTER_PAYMENT_ASSETS . '/admin/campaign-builder/campaign-builder.min.css',
274 316 [],
275 - $version
317 + self::asset_version( '/assets/admin/campaign-builder/campaign-builder.min.css' )
276 318 );
277 319
278 - // Load campaign display CSS so the preview modal renders correctly.
279 - $display_css = BETTER_PAYMENT_PATH . '/assets/blocks/campaign-display/style.min.css';
280 - 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 ) ) ) {
281 326 wp_enqueue_style(
282 327 'better-payment-campaign-display-style',
283 328 BETTER_PAYMENT_ASSETS . '/blocks/campaign-display/style.min.css',
284 329 [],
285 - filemtime( $display_css )
330 + self::asset_version( $display_css )
286 331 );
287 332 }
288 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 +
289 339 wp_enqueue_media();
290 340
291 341 // Zero out all WP admin chrome spacing so the builder fills edge-to-edge.
292 342 // The footer styling mirrors the Better Payment admin footer (see
@@ -323,15 +373,79 @@
323 373 if ( ! is_string( $global_currency ) || $global_currency === '' ) {
324 374 $global_currency = 'USD';
325 375 }
326 376
327 - // Localize element registry, templates, and global settings to JS builder.
328 - wp_localize_script( 'bp-campaign-builder', 'betterPaymentCampaignData', [
377 + $data = [
329 378 'elements' => array_values( ElementRegistry::get_all() ),
330 - '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(),
331 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 ),
332 396 'restUrl' => rest_url( 'better-payment/v1/' ),
333 397 'nonce' => wp_create_nonce( 'wp_rest' ),
334 398 'pluginUrl' => plugins_url( '', BETTER_PAYMENT_BASENAME ),
335 - ] );
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 + ];
336 450 }
337 451 }