| @@ -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 | } |