| @@ -5,8 +5,9 @@ | ||
| 5 | 5 | use Better_Payment\Lite\Admin\DB; |
| 6 | 6 | use Better_Payment\Lite\Campaign\CampaignStats; |
| 7 | 7 | use Better_Payment\Lite\Campaign\MetaBox; |
| 8 | 8 | use Better_Payment\Lite\Campaign\Templates\TemplateManager; |
| 9 | +use Better_Payment\Lite\Campaign\Support\Money; | |
| 9 | 10 | use Better_Payment\Lite\Campaign\Elements\ElementRegistry; |
| 10 | 11 | use Better_Payment\Lite\Campaign\Elements\ProElementPreview; |
| 11 | 12 | |
| 12 | 13 | if ( ! defined( 'ABSPATH' ) ) { |
| @@ -318,9 +319,9 @@ | ||
| 318 | 319 | $display_progress = (int) ceil( $goal > 0 ? min( 100, ( $raised / $goal ) * 100 ) : 0 ); |
| 319 | 320 | $goal_fmt = number_format( (int) ceil( $goal ) ); |
| 320 | 321 | } else { |
| 321 | 322 | $display_progress = $progress; // 1 decimal float from CampaignStats |
| 322 | - $goal_fmt = number_format( $goal, 2 ); | |
| 323 | + $goal_fmt = Money::format( $goal ); | |
| 323 | 324 | } |
| 324 | 325 | |
| 325 | 326 | ?> |
| 326 | 327 | <div class="bp-campaign-progress" |
| @@ -374,9 +375,9 @@ | ||
| 374 | 375 | <?php endif; ?> |
| 375 | 376 | <div class="bp-campaign-summary"> |
| 376 | 377 | <?php if ( $show_raised ) : ?> |
| 377 | 378 | <div class="bp-summary-item"> |
| 378 | - <strong><?php echo esc_html( $sm_currency_sym . number_format( (float) $stats['total_raised'], 2 ) ); ?></strong> | |
| 379 | + <strong><?php echo esc_html( Money::with_symbol( $sm_currency_sym, $stats['total_raised'] ) ); ?></strong> | |
| 379 | 380 | <span><?php esc_html_e( 'Raised', 'better-payment' ); ?></span> |
| 380 | 381 | </div> |
| 381 | 382 | <?php endif; ?> |
| 382 | 383 | <?php if ( $show_donors ) : ?> |
| @@ -460,9 +461,9 @@ | ||
| 460 | 461 | <?php |
| 461 | 462 | printf( |
| 462 | 463 | /* translators: %s: formatted minimum amount with currency symbol */ |
| 463 | 464 | esc_html__( 'The minimum donation for this campaign is %s.', 'better-payment' ), |
| 464 | - esc_html( $currency_symbol . number_format( $min_amount, 2 ) ) | |
| 465 | + esc_html( Money::with_symbol( $currency_symbol, $min_amount ) ) | |
| 465 | 466 | ); |
| 466 | 467 | ?> |
| 467 | 468 | </p> |
| 468 | 469 | <?php endif; ?> |
| @@ -565,9 +566,9 @@ | ||
| 565 | 566 | <?php checked( $is_default ); ?> |
| 566 | 567 | hidden |
| 567 | 568 | /> |
| 568 | 569 | <label for="<?php echo esc_attr( $uid ); ?>" class="bp-amount-label"> |
| 569 | - <?php echo esc_html( $currency_symbol . $amt ); ?> | |
| 570 | + <?php echo esc_html( Money::with_symbol( $currency_symbol, $amt ) ); ?> | |
| 570 | 571 | </label> |
| 571 | 572 | <?php endforeach; ?> |
| 572 | 573 | </div> |
| 573 | 574 | <?php if ( $allow_custom ) : ?> |
| @@ -1142,28 +1143,11 @@ | ||
| 1142 | 1143 | 'post_status' => 'publish', |
| 1143 | 1144 | 'post_name' => $key, |
| 1144 | 1145 | ] ); |
| 1145 | 1146 | |
| 1146 | - $meta = [ | |
| 1147 | - 'bpc_goal_amount' => 10000, | |
| 1148 | - 'bpc_color_primary' => $template['preview_color'] ?? '#6b63f6', | |
| 1149 | - 'bpc_color_background' => '', | |
| 1150 | - 'bpc_suggested_amounts' => [], | |
| 1151 | - 'bpc_allow_custom_amount' => true, | |
| 1152 | - 'bpc_minimum_amount' => '', | |
| 1153 | - 'bpc_form_page_id' => 0, | |
| 1154 | - 'bpc_status' => 'active', | |
| 1155 | - 'bpc_template_key' => $key, | |
| 1156 | - 'bpc_css_class' => '', | |
| 1157 | - ]; | |
| 1147 | + $meta = self::template_preview_meta( $key, $template ); | |
| 1148 | + $stats = self::template_preview_stats(); | |
| 1158 | 1149 | |
| 1159 | - $stats = [ | |
| 1160 | - 'total_raised' => 3750, | |
| 1161 | - 'progress' => 37.5, | |
| 1162 | - 'donor_count' => 42, | |
| 1163 | - 'days_remaining' => 18, | |
| 1164 | - ]; | |
| 1165 | - | |
| 1166 | 1150 | $theme_class = isset( $template['theme_class'] ) ? ' ' . sanitize_html_class( $template['theme_class'] ) : ''; |
| 1167 | 1151 | |
| 1168 | 1152 | ob_start(); |
| 1169 | 1153 | ?> |
| @@ -1190,8 +1174,93 @@ | ||
| 1190 | 1174 | return ob_get_clean(); |
| 1191 | 1175 | } |
| 1192 | 1176 | |
| 1193 | 1177 | /** |
| 1178 | + * Demo meta for a template preview — no campaign exists behind it. | |
| 1179 | + * | |
| 1180 | + * Shared by the fragment renderer (render_template_preview) and the full | |
| 1181 | + * document one (build_template_preview_document) so the two can never show a | |
| 1182 | + * template with different figures or a different accent colour. | |
| 1183 | + * | |
| 1184 | + * @param string $key Template key. | |
| 1185 | + * @param array $template Template definition from TemplateManager. | |
| 1186 | + * @return array | |
| 1187 | + */ | |
| 1188 | + private static function template_preview_meta( string $key, array $template ): array { | |
| 1189 | + return [ | |
| 1190 | + 'title' => $template['default_title'] ?? $template['label'] ?? 'Campaign Preview', | |
| 1191 | + 'bpc_goal_amount' => 10000, | |
| 1192 | + 'bpc_color_primary' => $template['preview_color'] ?? '#6b63f6', | |
| 1193 | + 'bpc_color_background' => '', | |
| 1194 | + 'bpc_suggested_amounts' => [], | |
| 1195 | + 'bpc_allow_custom_amount' => true, | |
| 1196 | + 'bpc_minimum_amount' => '', | |
| 1197 | + 'bpc_form_page_id' => 0, | |
| 1198 | + 'bpc_status' => 'active', | |
| 1199 | + 'bpc_template_key' => $key, | |
| 1200 | + 'bpc_css_class' => '', | |
| 1201 | + ]; | |
| 1202 | + } | |
| 1203 | + | |
| 1204 | + /** | |
| 1205 | + * Demo stats for a template preview. | |
| 1206 | + * | |
| 1207 | + * A template has no transactions, so the real figures are all zero — which | |
| 1208 | + * renders an empty progress bar and "$0.00 raised" on every card, i.e. the | |
| 1209 | + * one state that shows least about the design. These are obviously-illustrative | |
| 1210 | + * round numbers, never presented as a real campaign's record. | |
| 1211 | + * | |
| 1212 | + * @return array | |
| 1213 | + */ | |
| 1214 | + private static function template_preview_stats(): array { | |
| 1215 | + return [ | |
| 1216 | + 'total_raised' => 3750, | |
| 1217 | + 'progress' => 37.5, | |
| 1218 | + 'donor_count' => 42, | |
| 1219 | + 'days_remaining' => 18, | |
| 1220 | + ]; | |
| 1221 | + } | |
| 1222 | + | |
| 1223 | + /** | |
| 1224 | + * Build a full, standalone HTML document for one template — the picker's | |
| 1225 | + * "Preview" lightbox. | |
| 1226 | + * | |
| 1227 | + * This is the whole design, top to bottom, rendered by the same code the | |
| 1228 | + * frontend uses; the lightbox scrolls it. A static screenshot cannot do that | |
| 1229 | + * (it is a fixed crop of the top of the page) and drifts from the layout the | |
| 1230 | + * moment a template is redesigned. | |
| 1231 | + * | |
| 1232 | + * Only a template with a layout of its own previews live. A locked Pro card | |
| 1233 | + * (ProTemplateCatalog, free install only) carries `columns => []`, and Lite | |
| 1234 | + * ships no copy of Pro's layouts, so it returns '' and the lightbox shows the | |
| 1235 | + * card's screenshot — the builder does not even request it. With Pro active | |
| 1236 | + * the same key resolves to Pro's real template and renders like any other. | |
| 1237 | + * Falling through to `default_layout()` instead would advertise Pro's design | |
| 1238 | + * as a generic campaign. | |
| 1239 | + * | |
| 1240 | + * @param string $key Template key from TemplateManager. | |
| 1241 | + * @return string Full HTML document, or '' when the key is unknown or has no layout. | |
| 1242 | + */ | |
| 1243 | + public static function build_template_preview_document( string $key ): string { | |
| 1244 | + $templates = TemplateManager::get_all(); | |
| 1245 | + if ( empty( $templates[ $key ]['columns'] ) ) { | |
| 1246 | + return ''; | |
| 1247 | + } | |
| 1248 | + | |
| 1249 | + $template = $templates[ $key ]; | |
| 1250 | + | |
| 1251 | + return self::build_preview_document( | |
| 1252 | + [ | |
| 1253 | + 'layout' => $template['layout'] ?? '1-column', | |
| 1254 | + 'columns' => $template['columns'], | |
| 1255 | + ], | |
| 1256 | + self::template_preview_meta( $key, $template ), | |
| 1257 | + 0, | |
| 1258 | + self::template_preview_stats() | |
| 1259 | + ); | |
| 1260 | + } | |
| 1261 | + | |
| 1262 | + /** | |
| 1194 | 1263 | * Build a full HTML preview document for the builder's live-preview iframe. |
| 1195 | 1264 | * |
| 1196 | 1265 | * Each element is wrapped in a <div data-bp-element-id> so the JS overlay |
| 1197 | 1266 | * can measure positions and wire up hover/drag interactions. Works for both |
| @@ -1196,17 +1265,23 @@ | ||
| 1196 | 1265 | * Each element is wrapped in a <div data-bp-element-id> so the JS overlay |
| 1197 | 1266 | * can measure positions and wire up hover/drag interactions. Works for both |
| 1198 | 1267 | * new unsaved campaigns (campaign_id = 0) and existing ones. |
| 1199 | 1268 | * |
| 1200 | - * @param array $layout_data Builder layout: { layout, columns }. | |
| 1201 | - * @param array $meta_input Campaign meta from the builder store (includes 'title'). | |
| 1202 | - * @param int $campaign_id 0 for new campaigns; real ID to pull live stats. | |
| 1269 | + * @param array $layout_data Builder layout: { layout, columns }. | |
| 1270 | + * @param array $meta_input Campaign meta from the builder store (includes 'title'). | |
| 1271 | + * @param int $campaign_id 0 for new campaigns; real ID to pull live stats. | |
| 1272 | + * @param array $stats_override Demo figures merged over the computed stats, for | |
| 1273 | + * previews that have no campaign behind them at all | |
| 1274 | + * (the template picker). Applied LAST, after the | |
| 1275 | + * goal/end-date recomputations below, so it wins — | |
| 1276 | + * that is the whole point of an override. | |
| 1203 | 1277 | * @return string Full HTML document string. |
| 1204 | 1278 | */ |
| 1205 | 1279 | public static function build_preview_document( |
| 1206 | 1280 | array $layout_data, |
| 1207 | 1281 | array $meta_input, |
| 1208 | - int $campaign_id = 0 | |
| 1282 | + int $campaign_id = 0, | |
| 1283 | + array $stats_override = [] | |
| 1209 | 1284 | ): string { |
| 1210 | 1285 | // ── Post + stats ────────────────────────────────────────────────────── |
| 1211 | 1286 | $post = null; |
| 1212 | 1287 | $stats = null; |
| @@ -1252,8 +1327,12 @@ | ||
| 1252 | 1327 | if ( $preview_goal > 0 ) { |
| 1253 | 1328 | $stats['progress'] = min( 100.0, round( ( $stats['total_raised'] / $preview_goal ) * 100, 1 ) ); |
| 1254 | 1329 | } |
| 1255 | 1330 | |
| 1331 | + if ( ! empty( $stats_override ) ) { | |
| 1332 | + $stats = array_merge( $stats, $stats_override ); | |
| 1333 | + } | |
| 1334 | + | |
| 1256 | 1335 | // ── Meta ───────────────────────────────────────────────────────────── |
| 1257 | 1336 | $meta_defaults = [ |
| 1258 | 1337 | 'bpc_goal_amount' => 0, |
| 1259 | 1338 | 'bpc_color_primary' => '#6b63f6', |
| @@ -1329,9 +1408,9 @@ | ||
| 1329 | 1408 | // hotspot, an element that cannot be selected or removed. |
| 1330 | 1409 | // |
| 1331 | 1410 | // Show a mock preview instead so the element is visible, |
| 1332 | 1411 | // selectable, and demonstrates what Pro would do with the |
| 1333 | - // settings shown (disabled) in the sidebar. | |
| 1412 | + // upgrade banner shown in the sidebar. | |
| 1334 | 1413 | // |
| 1335 | 1414 | // This is the ONLY call site — the public render path has no |
| 1336 | 1415 | // reference to ProElementPreview, and ProElementPreview::render() |
| 1337 | 1416 | // re-checks the preview flag itself. Its data is fabricated; |
| @@ -1376,8 +1455,16 @@ | ||
| 1376 | 1455 | $extra_link = file_exists( BETTER_PAYMENT_ASSETS_PATH . '/css/fundraising-campaign.min.css' ) |
| 1377 | 1456 | ? '<link rel="stylesheet" href="' . esc_url( $fundraising_css ) . '?v=' . $v . '">' . "\n" |
| 1378 | 1457 | : ''; |
| 1379 | 1458 | |
| 1459 | + // Template theme CSS from an add-on — Pro links its themes through | |
| 1460 | + // `better_payment/campaign/preview_stylesheets`. A free install links no | |
| 1461 | + // Pro design. See Campaign\CampaignStyles. | |
| 1462 | + $theme_links = ''; | |
| 1463 | + foreach ( \Better_Payment\Lite\Campaign\CampaignStyles::preview_stylesheets() as $theme_css ) { | |
| 1464 | + $theme_links .= '<link rel="stylesheet" href="' . esc_url( $theme_css ) . '">' . "\n"; | |
| 1465 | + } | |
| 1466 | + | |
| 1380 | 1467 | return '<!DOCTYPE html>' . "\n" |
| 1381 | 1468 | . '<html>' . "\n" |
| 1382 | 1469 | . '<head>' . "\n" |
| 1383 | 1470 | . '<meta charset="utf-8">' . "\n" |
| @@ -1383,8 +1470,9 @@ | ||
| 1383 | 1470 | . '<meta charset="utf-8">' . "\n" |
| 1384 | 1471 | . '<meta name="viewport" content="width=device-width, initial-scale=1">' . "\n" |
| 1385 | 1472 | . '<base href="' . esc_url( home_url( '/' ) ) . '">' . "\n" |
| 1386 | 1473 | . '<link rel="stylesheet" href="' . esc_url( $display_css ) . '?v=' . $v . '">' . "\n" |
| 1474 | + . $theme_links | |
| 1387 | 1475 | . $extra_link |
| 1388 | 1476 | . '<style>' . "\n" |
| 1389 | 1477 | . '*, *::before, *::after { box-sizing: border-box; }' . "\n" |
| 1390 | 1478 | . 'html, body { margin: 0; padding: 0; background: #fff; }' . "\n" |
| @@ -1422,44 +1510,57 @@ | ||
| 1422 | 1510 | . '</html>'; |
| 1423 | 1511 | } |
| 1424 | 1512 | |
| 1425 | 1513 | /** |
| 1514 | + * Per-request memo for pro_locked_types(). A property rather than a | |
| 1515 | + * function-local static so tests can reset it. | |
| 1516 | + * | |
| 1517 | + * @var array<string, array>|null | |
| 1518 | + */ | |
| 1519 | + private static $pro_locked_types_cache = null; | |
| 1520 | + | |
| 1521 | + /** | |
| 1426 | 1522 | * Element types that are Pro-only AND currently locked (Pro inactive). |
| 1427 | 1523 | * |
| 1428 | 1524 | * Returns a map of type => schema (label/icon) for elements that carry the |
| 1429 | 1525 | * `pro` flag in the filtered registry while `better_payment/pro_enabled` is |
| 1430 | - * false. When Pro is active these are overridden by the full schema (no `pro` | |
| 1431 | - * flag) so the map is empty and nothing is locked. Result is cached per | |
| 1432 | - * request. | |
| 1526 | + * false. With Pro active the map is empty and nothing is locked. Result is | |
| 1527 | + * cached per request. | |
| 1433 | 1528 | * |
| 1434 | 1529 | * @return array<string, array> |
| 1435 | 1530 | */ |
| 1436 | 1531 | private static function pro_locked_types(): array { |
| 1437 | - static $cache = null; | |
| 1438 | - | |
| 1439 | - if ( null !== $cache ) { | |
| 1440 | - return $cache; | |
| 1532 | + if ( null !== self::$pro_locked_types_cache ) { | |
| 1533 | + return self::$pro_locked_types_cache; | |
| 1441 | 1534 | } |
| 1442 | 1535 | |
| 1443 | - $cache = []; | |
| 1536 | + self::$pro_locked_types_cache = []; | |
| 1444 | 1537 | |
| 1445 | 1538 | if ( apply_filters( 'better_payment/pro_enabled', false ) ) { |
| 1446 | - return $cache; | |
| 1539 | + return self::$pro_locked_types_cache; | |
| 1447 | 1540 | } |
| 1448 | 1541 | |
| 1449 | 1542 | foreach ( ElementRegistry::get_all() as $type => $schema ) { |
| 1450 | 1543 | if ( ! empty( $schema['pro'] ) ) { |
| 1451 | - $cache[ $type ] = $schema; | |
| 1544 | + self::$pro_locked_types_cache[ $type ] = $schema; | |
| 1452 | 1545 | } |
| 1453 | 1546 | } |
| 1454 | 1547 | |
| 1455 | - return $cache; | |
| 1548 | + return self::$pro_locked_types_cache; | |
| 1456 | 1549 | } |
| 1457 | 1550 | |
| 1458 | 1551 | /** |
| 1459 | - * Editor-only placeholder banner shown in the builder preview for a | |
| 1460 | - * Pro-locked element (a Pro element left over from when Pro was active). | |
| 1461 | - * Never emitted on the public frontend. | |
| 1552 | + * Generic builder-only "PRO" banner for a locked element that has no mock. | |
| 1553 | + * | |
| 1554 | + * Lite's own three Pro elements always get a ProElementPreview mock, so this | |
| 1555 | + * is reached only by some other `pro`-flagged type — a third party's, added | |
| 1556 | + * through `better_payment/campaign_elements`, or a future Pro element whose | |
| 1557 | + * mock has not been written. It is lock chrome, not Pro functionality: it | |
| 1558 | + * renders a label and an upgrade line, nothing of the element itself. Without | |
| 1559 | + * it the element's canvas wrapper would be empty and zero-height, so the canvas | |
| 1560 | + * could place no hotspot and the element could not be selected or deleted. | |
| 1561 | + * Only build_preview_document() calls it, only while Pro is inactive | |
| 1562 | + * (pro_locked_types() is empty otherwise). Never emitted on the public frontend. | |
| 1462 | 1563 | * |
| 1463 | 1564 | * @param array $schema Element schema (label, icon). |
| 1464 | 1565 | * @return string |
| 1465 | 1566 | */ |