Skip to content

Commit bdf5265

Browse files
release: fixes
- Added title for Close button from the Popup block - Fixed issue when form validation was not appearing when you click on the Submit button - Fixed issue with Team Members Pattern having errors in the mobile preview - Fixed issue with assets not being enqueued for block template parts - Fixed issue where WP Enlarge on click option was not working when using Otter's animations - Fixed Visibility conditions that were not applied to 767px and 768px screens - Fixed tabs broken on mobile view - Enhanced security
2 parents 1e273e1 + 411862a commit bdf5265

11 files changed

Lines changed: 317 additions & 108 deletions

File tree

.distignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ tsconfig.json
4141
/artifact
4242
/plugins
4343
/docs
44-
/build/pro/
44+
/build/pro/
45+
AGENTS.md

AGENTS.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Agent Workflow
2+
3+
## Project Overview
4+
5+
Otter Blocks is a WordPress Gutenberg blocks page builder plugin. It's a monorepo containing:
6+
- **Otter Blocks** (main free plugin) — `otter-blocks.php`
7+
- **Otter Pro** (premium extension) — `plugins/otter-pro/`
8+
- **Blocks Animation**`plugins/blocks-animation/`
9+
- **Blocks CSS**`plugins/blocks-css/`
10+
- **Blocks Export/Import**`plugins/blocks-export-import/`
11+
12+
Namespace: `ThemeIsle\GutenbergBlocks`. Requires WordPress 6.2+, PHP 7.4+ (platform target).
13+
14+
## Build & Development Commands
15+
16+
```bash
17+
# Setup
18+
npm ci && composer install
19+
20+
# Development (watch mode)
21+
npm run start # All configs with watch
22+
npm run dev:lite # Watch lite blocks only
23+
npm run dev:pro # Watch pro blocks only
24+
25+
# Production build
26+
npm run build # Full production build (all configs in parallel)
27+
npm run prod:lite # Lite blocks only
28+
npm run prod:pro # Pro blocks only
29+
npm run prod:grunt # SASS compilation via Grunt
30+
31+
# Sister plugins build
32+
npm run plugins # Build blocks-animation, blocks-css, blocks-export-import
33+
```
34+
35+
## Testing
36+
37+
```bash
38+
# JavaScript unit tests (Jest)
39+
npm run test:unit
40+
npm run test:unit:watch
41+
42+
# PHP unit tests (requires wp-env)
43+
npm run test:unit:php # Starts wp-env + runs PHPUnit
44+
npm run test:unit:php:base # PHPUnit only (if wp-env already running)
45+
npm run test:unit:php:multisite # Multisite PHPUnit tests
46+
47+
# E2E tests (Playwright)
48+
npm run test:e2e:playwright
49+
npm run test:e2e:playwright-ui # With UI
50+
51+
# Performance tests
52+
npm run test:performance
53+
```
54+
55+
PHPUnit config: `phpunit.xml` (single-site), `phpunit/multisite.xml`.
56+
Playwright config: `src/blocks/test/e2e/playwright.config.js`.
57+
58+
## Linting & Formatting
59+
60+
```bash
61+
# JavaScript
62+
npm run lint # ESLint check
63+
npm run format # ESLint autofix
64+
65+
# PHP
66+
composer run lint # PHPCS
67+
composer run format # PHPCBF
68+
composer run phpstan # PHPStan static analysis (uses phpstan.neon + baseline)
69+
```
70+
71+
ESLint: WordPress preset with TypeScript (`.eslintrc`). PHPCS: WordPress-Core/Docs/Extra + VIP-Go (`phpcs.xml`).
72+
73+
## Architecture
74+
75+
### Webpack Build Pipeline
76+
77+
Three webpack configs, all extending `@wordpress/scripts`:
78+
- `webpack.config.js` — Lite: dashboard, onboarding, animation frontend, and all free blocks
79+
- `webpack.config.pro.js` — Pro blocks and features
80+
- `webpack.config.plugins.js` — Sister plugins (animation, CSS, export-import)
81+
82+
Grunt handles SASS compilation and version bumping (`Gruntfile.js`).
83+
84+
### Block Metadata Registry
85+
86+
`blocks.json` is the central manifest mapping every block to its `block.json` path and SCSS asset paths. Both webpack and Grunt read this file. Pro blocks are marked with `isPro: true`.
87+
88+
### PHP Structure (`inc/`)
89+
90+
- `class-main.php` — Singleton bootstrap, hooks, autoloading, SVG/MIME handling
91+
- `class-registration.php` — Block registration (WordPress native), asset enqueue, block categories (`themeisle-blocks`, `themeisle-woocommerce-blocks`), lazy-load dependencies
92+
- `class-pro.php` — Pro plugin loader
93+
- `class-patterns.php` — Pattern library
94+
- `css/` — CSS generation classes (`Block_Frontend`, `CSS_Handler`)
95+
- `plugins/` — Feature modules: Block_Conditions, Dynamic_Content, Dashboard, Stripe_API, Template_Cloud
96+
- `render/` — Dynamic block server-side renderers
97+
- `server/` — REST API endpoints
98+
- `integrations/` — Form provider integrations (email services)
99+
100+
### JavaScript Structure (`src/`)
101+
102+
- `src/blocks/blocks/` — Individual block implementations (edit.js, index.js, inspector.js, block.json)
103+
- `src/blocks/components/` — Shared React components
104+
- `src/blocks/frontend/` — Block frontend scripts (loaded on visitor-facing pages)
105+
- `src/blocks/plugins/` — Editor plugins (conditions, CSS, animations, copy-paste styles)
106+
- `src/blocks/helpers/` — Utility functions
107+
- `src/pro/` — Pro block implementations
108+
- `src/dashboard/` — Otter settings dashboard
109+
- `src/onboarding/` — Onboarding wizard
110+
- `src/animation/`, `src/css/`, `src/export-import/` — Sister plugin sources
111+
112+
### Development Environment
113+
114+
Uses `@wordpress/env` (wp-env). Config: `.wp-env.override.json`. Start with `npm run test:unit:php:setup` or `npx wp-env start`.
115+
116+
## Key Conventions
117+
118+
- Text domains: `otter-blocks`, `otter-pro`, `blocks-animation`, `blocks-css`, `blocks-export-import`
119+
- Tab indentation (JS and PHP), single quotes, semicolons required (JS)
120+
- Block namespace: `themeisle-blocks/<block-name>` (e.g., `themeisle-blocks/accordion`)
121+
- PHP autoloading follows class file naming: `class-<name>.php`
122+
- Distribution via `npm run dist` (runs `bin/dist.sh`, creates ZIP artifacts filtered by `.distignore`)

inc/class-registration.php

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ function ( $content ) {
386386

387387
}
388388

389-
if ( function_exists( 'get_block_templates' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && current_theme_supports( 'block-templates' ) ) {
389+
if ( function_exists( 'get_block_templates' ) && ( current_theme_supports( 'block-templates' ) || current_theme_supports( 'block-template-parts' ) ) ) {
390390
$this->enqueue_dependencies( 'block-templates' );
391391
}
392392
}
@@ -406,25 +406,42 @@ public function enqueue_dependencies( $post = null ) {
406406
} elseif ( 'block-templates' === $post ) {
407407
global $_wp_current_template_content;
408408

409-
$slugs = array();
410-
$template_blocks = parse_blocks( $_wp_current_template_content );
411-
412-
foreach ( $template_blocks as $template_block ) {
413-
if ( 'core/template-part' === $template_block['blockName'] ) {
414-
$slugs[] = $template_block['attrs']['slug'];
409+
$content = '';
410+
$slugs = array();
411+
412+
// If we have template content (full block templates), extract template part slugs.
413+
if ( ! empty( $_wp_current_template_content ) ) {
414+
$template_blocks = parse_blocks( $_wp_current_template_content );
415+
416+
foreach ( $template_blocks as $template_block ) {
417+
if ( 'core/template-part' === $template_block['blockName'] && isset( $template_block['attrs']['slug'] ) ) {
418+
$slugs[] = $template_block['attrs']['slug'];
419+
}
415420
}
416-
}
417-
418-
$templates_parts = get_block_templates( array( 'slugs__in' => $slugs ), 'wp_template_part' );
419-
420-
foreach ( $templates_parts as $templates_part ) {
421-
if ( ! empty( $templates_part->content ) && ! empty( $templates_part->slug ) && in_array( $templates_part->slug, $slugs ) ) {
422-
$content .= $templates_part->content;
421+
422+
// Get the specific template parts referenced in the template.
423+
$templates_parts = get_block_templates( array( 'slug__in' => $slugs ), 'wp_template_part' );
424+
425+
foreach ( $templates_parts as $templates_part ) {
426+
if ( ! empty( $templates_part->content ) && ! empty( $templates_part->slug ) && in_array( $templates_part->slug, $slugs ) ) {
427+
$content .= $templates_part->content;
428+
}
429+
}
430+
431+
$content .= $_wp_current_template_content;
432+
} else {
433+
// Fallback for classic themes with block-template-parts only.
434+
// Get all template parts since we can't determine which ones are used.
435+
$templates_parts = get_block_templates( array(), 'wp_template_part' );
436+
437+
foreach ( $templates_parts as $templates_part ) {
438+
if ( ! empty( $templates_part->content ) ) {
439+
$content .= $templates_part->content;
440+
}
423441
}
424442
}
425443

426-
$content .= $_wp_current_template_content;
427-
$post = $content;
444+
$post = $content;
428445
} else {
429446
$content = get_the_content( null, false, $post );
430447
}
@@ -1065,7 +1082,7 @@ public function load_condition_hide_on_styles( $block_content, $block ) {
10651082
* @access public
10661083
*/
10671084
public static function condition_hide_on_style() {
1068-
echo '<style id="o-condition-hide-inline-css">@media (max-width:768px){.o-hide-on-mobile{display:none!important}}@media (min-width:767px) and (max-width:1024px){.o-hide-on-tablet{display:none!important}}@media (min-width:1023px){.o-hide-on-desktop{display:none!important}}</style>';
1085+
echo '<style id="o-condition-hide-inline-css">@media (max-width:768px){.o-hide-on-mobile{display:none!important}}@media (min-width:769px) and (max-width:1024px){.o-hide-on-tablet{display:none!important}}@media (min-width:1025px){.o-hide-on-desktop{display:none!important}}</style>';
10691086
}
10701087

10711088
/**

inc/css/class-block-frontend.php

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -577,32 +577,47 @@ public function enqueue_widgets_css() {
577577
* @access public
578578
*/
579579
public function enqueue_fse_css() {
580-
if ( ! ( function_exists( 'get_block_templates' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && current_theme_supports( 'block-templates' ) ) ) {
580+
if ( ! ( function_exists( 'get_block_templates' ) && ( current_theme_supports( 'block-templates' ) || current_theme_supports( 'block-template-parts' ) ) ) ) {
581581
return;
582582
}
583583

584584
global $_wp_current_template_content;
585585

586-
$content = '';
587-
$slugs = array();
588-
$template_blocks = parse_blocks( $_wp_current_template_content );
586+
$content = '';
587+
$slugs = array();
589588

590-
foreach ( $template_blocks as $template_block ) {
591-
if ( 'core/template-part' === $template_block['blockName'] ) {
592-
$slugs[] = $template_block['attrs']['slug'];
589+
// If we have template content (full block templates), extract template part slugs.
590+
if ( ! empty( $_wp_current_template_content ) ) {
591+
$template_blocks = parse_blocks( $_wp_current_template_content );
592+
593+
foreach ( $template_blocks as $template_block ) {
594+
if ( 'core/template-part' === $template_block['blockName'] && isset( $template_block['attrs']['slug'] ) ) {
595+
$slugs[] = $template_block['attrs']['slug'];
596+
}
593597
}
594-
}
595-
596-
$templates_parts = get_block_templates( array( 'slugs__in' => $slugs ), 'wp_template_part' );
597-
598-
foreach ( $templates_parts as $templates_part ) {
599-
if ( ! empty( $templates_part->content ) && ! empty( $templates_part->slug ) && in_array( $templates_part->slug, $slugs ) ) {
600-
$content .= $templates_part->content;
598+
599+
// Get the specific template parts referenced in the template.
600+
$templates_parts = get_block_templates( array( 'slug__in' => $slugs ), 'wp_template_part' );
601+
602+
foreach ( $templates_parts as $templates_part ) {
603+
if ( ! empty( $templates_part->content ) && ! empty( $templates_part->slug ) && in_array( $templates_part->slug, $slugs ) ) {
604+
$content .= $templates_part->content;
605+
}
606+
}
607+
608+
$content .= $_wp_current_template_content;
609+
} else {
610+
// Fallback for classic themes with block-template-parts only.
611+
// Get all template parts since we can't determine which ones are used.
612+
$templates_parts = get_block_templates( array(), 'wp_template_part' );
613+
614+
foreach ( $templates_parts as $templates_part ) {
615+
if ( ! empty( $templates_part->content ) ) {
616+
$content .= $templates_part->content;
617+
}
601618
}
602619
}
603620

604-
$content .= $_wp_current_template_content;
605-
606621
$blocks = parse_blocks( $content );
607622

608623
if ( ! is_array( $blocks ) || empty( $blocks ) ) {

inc/plugins/class-stripe-api.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,11 @@ public function save_customer_data( $session_id ) {
239239
array_push( $data, $object );
240240

241241
if ( defined( 'COOKIEPATH' ) && defined( 'COOKIE_DOMAIN' ) && ! headers_sent() && ! $user_id ) {
242-
setcookie( 'o_stripe_data', wp_json_encode( $data ), strtotime( '+1 week' ), COOKIEPATH, COOKIE_DOMAIN, false ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie
242+
$data = wp_json_encode( $data );
243+
$hmac_data = hash_hmac( 'sha256', $data, wp_salt() );
244+
$secure = function_exists( 'is_ssl' ) ? is_ssl() : ( ! empty( $_SERVER['HTTPS'] ) && strtolower( $_SERVER['HTTPS'] ) !== 'off' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
245+
setcookie( 'o_stripe_data', $data, strtotime( '+1 week' ), COOKIEPATH, COOKIE_DOMAIN, $secure, true ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie
246+
setcookie( 'o_stripe_hmac_data', $hmac_data, strtotime( '+1 week' ), COOKIEPATH, COOKIE_DOMAIN, $secure, true ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie
243247
return;
244248
}
245249

@@ -258,8 +262,13 @@ public function get_customer_data() {
258262
$user_id = get_current_user_id();
259263

260264
if ( ! $user_id ) {
261-
if ( isset( $_COOKIE['o_stripe_data'] ) && ! empty( $_COOKIE['o_stripe_data'] ) ) { // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
262-
$data = json_decode( stripcslashes( $_COOKIE['o_stripe_data'] ), true ); // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
265+
if ( isset( $_COOKIE['o_stripe_data'] ) && ! empty( $_COOKIE['o_stripe_data'] ) && isset( $_COOKIE['o_stripe_hmac_data'] ) && ! empty( $_COOKIE['o_stripe_hmac_data'] ) ) { // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
266+
$data_raw = stripcslashes( $_COOKIE['o_stripe_data'] ); // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
267+
$hmac_data = sanitize_text_field( $_COOKIE['o_stripe_hmac_data'] ); // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
268+
269+
if ( hash_equals( hash_hmac( 'sha256', $data_raw, wp_salt() ), $hmac_data ) ) {
270+
$data = json_decode( $data_raw, true );
271+
}
263272
}
264273

265274
return $data;

0 commit comments

Comments
 (0)