From da29a27bfc55bad1b9354b9eca3812450e998885 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Tue, 7 Jul 2026 16:29:35 +0530 Subject: [PATCH 01/19] feat: per-form option to disable default SureForms styling + fix Custom CSS on single form view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 'Disable SureForms Styling' toggle (Style tab > Advanced) that renders the form without the SureForms opinion CSS so the site's own CSS fully controls the form's appearance: - Skips the inline derived CSS-variable style block for the form - Skips enqueueing the SureForms frontend stylesheets when every form on the requested page has the toggle enabled (third-party library styles for Dropdown/Phone and all scripts are still loaded) - Adds a srfm-styling-none container class so the state is targetable Known limitation: when a page mixes styled and unstyled forms, the stylesheets still load page-wide (they are page-level assets); the unstyled form only skips its inline variable block. Also fixes: the per-form Custom CSS box (_srfm_form_custom_css) was only echoed when the current post type was not sureforms_form, so it never applied on the form's own single/instant form view — only on embedded views. It is now output on all views. --- inc/form-styling.php | 108 ++++++++++++++++++ inc/frontend-assets.php | 23 ++-- inc/generate-form-markup.php | 50 ++++---- inc/post-types.php | 6 + .../tabs/StyleSettings.js | 27 +++++ 5 files changed, 184 insertions(+), 30 deletions(-) diff --git a/inc/form-styling.php b/inc/form-styling.php index 0235db66d..b710a9f9b 100644 --- a/inc/form-styling.php +++ b/inc/form-styling.php @@ -148,6 +148,114 @@ public static function has_custom_styling( $block_attrs ) { return 'inherit' !== ( $block_attrs['formTheme'] ?? 'inherit' ); } + /** + * Check if the form has default SureForms styling disabled. + * + * When enabled from the Style tab, the form is rendered without the + * SureForms frontend stylesheets and inline CSS variables so the site's + * own CSS fully controls the form's appearance. + * + * @param int|string $form_id Form post ID. + * @return bool True when default styling is disabled for the form. + * @since x.x.x + */ + public static function is_default_styling_disabled( $form_id ) { + $form_id = absint( $form_id ); + if ( ! $form_id ) { + return false; + } + + $form_styling = get_post_meta( $form_id, '_srfm_forms_styling', true ); + + return is_array( $form_styling ) && ! empty( $form_styling['disable_default_styles'] ); + } + + /** + * Check whether the SureForms frontend stylesheets can be skipped for a post. + * + * Returns true only when every SureForms form found on the post has default + * styling disabled. When the forms on the post cannot be determined, the + * stylesheets are kept as the safe default. + * + * @param \WP_Post $post Post being rendered. + * @return bool True when the frontend stylesheets can be skipped. + * @since x.x.x + */ + public static function should_skip_frontend_styles( $post ) { + if ( SRFM_FORMS_POST_TYPE === $post->post_type ) { + $form_ids = [ $post->ID ]; + } else { + $form_ids = self::get_form_ids_from_content( $post->post_content ); + } + + if ( empty( $form_ids ) ) { + return false; + } + + foreach ( $form_ids as $form_id ) { + if ( ! self::is_default_styling_disabled( $form_id ) ) { + return false; + } + } + + return true; + } + + /** + * Extract SureForms form IDs from post content. + * + * Looks for srfm/form blocks (including nested ones) and [sureforms] + * shortcodes. + * + * @param string $content Post content. + * @return array Unique form IDs found in the content. + * @since x.x.x + */ + public static function get_form_ids_from_content( $content ) { + $form_ids = []; + + if ( has_block( 'srfm/form', $content ) ) { + $form_ids = self::collect_form_block_ids( parse_blocks( $content ) ); + } + + if ( has_shortcode( $content, 'sureforms' ) && preg_match_all( '/' . get_shortcode_regex( [ 'sureforms' ] ) . '/', $content, $matches ) ) { + foreach ( $matches[3] as $atts_string ) { + $atts = shortcode_parse_atts( $atts_string ); + if ( is_array( $atts ) && ! empty( $atts['id'] ) ) { + $form_ids[] = absint( $atts['id'] ); + } + } + } + + return array_values( array_unique( array_filter( $form_ids ) ) ); + } + + /** + * Recursively collect form IDs from parsed srfm/form blocks. + * + * @param array $blocks Parsed blocks from parse_blocks(). + * @return array Form IDs found in srfm/form blocks. + * @since x.x.x + */ + private static function collect_form_block_ids( $blocks ) { + $form_ids = []; + + foreach ( $blocks as $block ) { + if ( ! is_array( $block ) ) { + continue; + } + $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : []; + if ( isset( $block['blockName'] ) && 'srfm/form' === $block['blockName'] && ! empty( $attrs['id'] ) && is_scalar( $attrs['id'] ) ) { + $form_ids[] = absint( $attrs['id'] ); + } + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { + $form_ids = array_merge( $form_ids, self::collect_form_block_ids( $block['innerBlocks'] ) ); + } + } + + return $form_ids; + } + /** * Map dimension block attributes (Top/Right/Bottom/Left/Unit) to form styling. * diff --git a/inc/frontend-assets.php b/inc/frontend-assets.php index f9cbd003b..2c18e7ae6 100644 --- a/inc/frontend-assets.php +++ b/inc/frontend-assets.php @@ -161,7 +161,8 @@ public function register_scripts() { if ( $load_assets ) { // Load needed styles in head tag if current requested page has SureForms form. - self::enqueue_scripts_and_styles(); + // Skip the SureForms stylesheets when every form on the page has default styling disabled. + self::enqueue_scripts_and_styles( Form_Styling::should_skip_frontend_styles( $current_post ) ); } } } @@ -169,19 +170,25 @@ public function register_scripts() { /** * Enqueue scripts and styles. * + * @param bool $skip_form_styles When true, the SureForms stylesheets are not enqueued + * (default styling disabled for the form). External library + * styles and scripts are always loaded so advanced fields + * like Dropdown and Phone keep working. * @return void * @since 0.0.11 */ - public static function enqueue_scripts_and_styles() { + public static function enqueue_scripts_and_styles( $skip_form_styles = false ) { // Load the styles. - foreach ( self::$css_assets as $handle => $path ) { + if ( ! $skip_form_styles ) { + foreach ( self::$css_assets as $handle => $path ) { - // Skip single form styles if not on single form page. - if ( 'single' === $handle && ! is_singular( SRFM_FORMS_POST_TYPE ) ) { - continue; - } + // Skip single form styles if not on single form page. + if ( 'single' === $handle && ! is_singular( SRFM_FORMS_POST_TYPE ) ) { + continue; + } - wp_enqueue_style( SRFM_SLUG . '-' . $handle ); + wp_enqueue_style( SRFM_SLUG . '-' . $handle ); + } } // Load the external styles. Like Phone and Tom Select. diff --git a/inc/generate-form-markup.php b/inc/generate-form-markup.php index f0ba907a9..e0937b3c5 100644 --- a/inc/generate-form-markup.php +++ b/inc/generate-form-markup.php @@ -136,8 +136,8 @@ public static function get_form_markup( $id, $show_title_current_page = true, $s $block_count = count( $form_blocks ); $current_post_type = get_post_type(); - // load all the frontend assets. - Frontend_Assets::enqueue_scripts_and_styles(); + // load all the frontend assets. Skips the SureForms stylesheets when the form has default styling disabled. + Frontend_Assets::enqueue_scripts_and_styles( Form_Styling::is_default_styling_disabled( $id ) ); ob_start(); if ( '' !== $id && 0 !== $block_count ) { @@ -154,6 +154,10 @@ public static function get_form_markup( $id, $show_title_current_page = true, $s if ( Form_Styling::has_custom_styling( $block_attrs ) ) { $form_styling = Form_Styling::map_block_attrs_to_styling( $form_styling, $block_attrs ); } + + // When enabled, the form renders without the SureForms inline CSS variables so + // the site's own CSS fully controls its appearance. Per-form Custom CSS still applies. + $disable_default_styles = ! empty( $form_styling['disable_default_styles'] ); // Background Settings. $bg_type = $form_styling['bg_type'] ?? 'color'; $bg_color = $form_styling['bg_color'] ?? ''; @@ -241,6 +245,7 @@ public static function get_form_markup( $id, $show_title_current_page = true, $s ! empty( $block_id_suffix ) ? $container_id : '', // Unique class for CSS scoping when blockId exists. $sf_classname, 'Neve' === $theme_name ? $neve_theme_margin_class_name : '', // compatibility with Neve theme for margin between main content and footer. + $disable_default_styles ? 'srfm-styling-none' : '', // Marker class when default styling is disabled, so custom CSS can target the state. $background_classes, ]; @@ -360,6 +365,7 @@ public static function get_form_markup( $id, $show_title_current_page = true, $s diff --git a/inc/post-types.php b/inc/post-types.php index a9a24c4f8..5c1a35303 100644 --- a/inc/post-types.php +++ b/inc/post-types.php @@ -823,6 +823,10 @@ public function register_post_metas() { 'instant_form_border_radius_link' => [ 'type' => 'boolean', ], + // Disable default SureForms styling. + 'disable_default_styles' => [ + 'type' => 'boolean', + ], ], ], ], @@ -903,6 +907,8 @@ public function register_post_metas() { 'instant_form_border_radius_left' => 12, 'instant_form_border_radius_unit' => 'px', 'instant_form_border_radius_link' => true, + // Disable default SureForms styling. + 'disable_default_styles' => false, ], ] ); diff --git a/src/admin/single-form-settings/tabs/StyleSettings.js b/src/admin/single-form-settings/tabs/StyleSettings.js index c3ecc1db9..7a235b76f 100644 --- a/src/admin/single-form-settings/tabs/StyleSettings.js +++ b/src/admin/single-form-settings/tabs/StyleSettings.js @@ -3,6 +3,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; import { store as editorStore } from '@wordpress/editor'; import { applyFilters } from '@wordpress/hooks'; +import { ToggleControl } from '@wordpress/components'; import AdvancedPopColorControl from '@Components/color-control/advanced-pop-color-control.js'; import SRFMAdvancedPanelBody from '@Components/advanced-panel-body'; import SRFMTextControl from '@Components/text-control'; @@ -1173,6 +1174,32 @@ function StyleSettings( props ) { ]; const advanced = [ + { + id: 'disable_default_styles', + component: ( + <> + { + updateFormStyling( + 'disable_default_styles', + !! value + ); + } } + /> +

+ { __( + 'Render this form without the SureForms default styles, so your own CSS (theme styles or the Custom CSS panel) fully controls its appearance. Essential styles for advanced fields like Dropdown and Phone are still loaded. This setting takes effect only on the live page, not in the editor preview.', + 'sureforms' + ) } +

+ + ), + }, { id: 'additional_css_classes', component: ( From 372318b7b0e96755ea33ca089fef467e0ca99b53 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Tue, 7 Jul 2026 23:29:34 +0530 Subject: [PATCH 02/19] test: cover disable-default-styling helpers and conditional style enqueue Adds unit tests for Form_Styling::is_default_styling_disabled(), Form_Styling::get_form_ids_from_content(), Form_Styling::should_skip_frontend_styles() and Frontend_Assets::enqueue_scripts_and_styles() with the skip_form_styles flag. --- tests/unit/inc/test-form-styling.php | 116 ++++++++++++++++++++++++ tests/unit/inc/test-frontend-assets.php | 34 +++++++ 2 files changed, 150 insertions(+) diff --git a/tests/unit/inc/test-form-styling.php b/tests/unit/inc/test-form-styling.php index b7450f751..55670d0f8 100644 --- a/tests/unit/inc/test-form-styling.php +++ b/tests/unit/inc/test-form-styling.php @@ -458,4 +458,120 @@ public function test_has_custom_styling_returns_false_when_form_theme_inherit() public function test_has_custom_styling_returns_false_when_not_set() { $this->assertFalse( Form_Styling::has_custom_styling( [] ) ); } + + // ─── disable default styling ────────────────────────────────── + + /** + * Test is_default_styling_disabled() reads the form styling meta correctly. + */ + public function test_is_default_styling_disabled() { + // Invalid form IDs are never treated as disabled. + $this->assertFalse( Form_Styling::is_default_styling_disabled( 0 ) ); + $this->assertFalse( Form_Styling::is_default_styling_disabled( '' ) ); + + $form_id = wp_insert_post( + [ + 'post_type' => SRFM_FORMS_POST_TYPE, + 'post_status' => 'publish', + 'post_title' => 'Styling Test Form', + ] + ); + + // Explicitly enabled default styling. + update_post_meta( $form_id, '_srfm_forms_styling', [ 'disable_default_styles' => false ] ); + $this->assertFalse( Form_Styling::is_default_styling_disabled( $form_id ) ); + + // Styling meta without the key keeps default styling. + update_post_meta( $form_id, '_srfm_forms_styling', [ 'primary_color' => '#000000' ] ); + $this->assertFalse( Form_Styling::is_default_styling_disabled( $form_id ) ); + + update_post_meta( $form_id, '_srfm_forms_styling', [ 'disable_default_styles' => true ] ); + $this->assertTrue( Form_Styling::is_default_styling_disabled( $form_id ) ); + + wp_delete_post( $form_id, true ); + } + + /** + * Test get_form_ids_from_content() finds top-level blocks, nested blocks and shortcodes. + */ + public function test_get_form_ids_from_content() { + if ( ! shortcode_exists( 'sureforms' ) ) { + add_shortcode( 'sureforms', '__return_empty_string' ); + } + + $this->assertSame( [], Form_Styling::get_form_ids_from_content( '' ) ); + $this->assertSame( [], Form_Styling::get_form_ids_from_content( '

No forms here.

' ) ); + + // Top-level block. + $this->assertSame( [ 11 ], Form_Styling::get_form_ids_from_content( '' ) ); + + // Nested block, another top-level block, shortcodes and a duplicate ID. + $content = '
'; + $content .= ''; + $content .= '[sureforms id="33"][sureforms id="22"]'; + + $form_ids = Form_Styling::get_form_ids_from_content( $content ); + sort( $form_ids ); + + $this->assertSame( [ 11, 22, 33 ], $form_ids ); + } + + /** + * Test should_skip_frontend_styles() only skips when every form on the post disables styling. + */ + public function test_should_skip_frontend_styles() { + $disabled_form_id = wp_insert_post( + [ + 'post_type' => SRFM_FORMS_POST_TYPE, + 'post_status' => 'publish', + 'post_title' => 'Unstyled Form', + ] + ); + update_post_meta( $disabled_form_id, '_srfm_forms_styling', [ 'disable_default_styles' => true ] ); + + $styled_form_id = wp_insert_post( + [ + 'post_type' => SRFM_FORMS_POST_TYPE, + 'post_status' => 'publish', + 'post_title' => 'Styled Form', + ] + ); + + // Single form views. + $this->assertTrue( Form_Styling::should_skip_frontend_styles( get_post( $disabled_form_id ) ), 'Single view of a form with disabled styling should skip the stylesheets.' ); + $this->assertFalse( Form_Styling::should_skip_frontend_styles( get_post( $styled_form_id ) ), 'Single view of a styled form should keep the stylesheets.' ); + + // Page embedding only the unstyled form. + $page_id = wp_insert_post( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Embeds', + 'post_content' => '', + ] + ); + $this->assertTrue( Form_Styling::should_skip_frontend_styles( get_post( $page_id ) ), 'A page whose only form has disabled styling should skip the stylesheets.' ); + + // Page mixing an unstyled and a styled form keeps the stylesheets. + wp_update_post( + [ + 'ID' => $page_id, + 'post_content' => '', + ] + ); + $this->assertFalse( Form_Styling::should_skip_frontend_styles( get_post( $page_id ) ), 'A page mixing styled and unstyled forms should keep the stylesheets.' ); + + // When no forms can be determined, keep the stylesheets as the safe default. + wp_update_post( + [ + 'ID' => $page_id, + 'post_content' => '

Nothing detectable.

', + ] + ); + $this->assertFalse( Form_Styling::should_skip_frontend_styles( get_post( $page_id ) ), 'A post without detectable forms should keep the stylesheets.' ); + + wp_delete_post( $disabled_form_id, true ); + wp_delete_post( $styled_form_id, true ); + wp_delete_post( $page_id, true ); + } } diff --git a/tests/unit/inc/test-frontend-assets.php b/tests/unit/inc/test-frontend-assets.php index d8d1d1122..fde536fa3 100644 --- a/tests/unit/inc/test-frontend-assets.php +++ b/tests/unit/inc/test-frontend-assets.php @@ -129,4 +129,38 @@ public function test_register_scripts() { $registered = wp_scripts()->query( SRFM_SLUG . '-form-submit', 'registered' ); $this->assertNotFalse( $registered, 'srfm-form-submit script should be registered after register_scripts().' ); } + + /** + * Test enqueue_scripts_and_styles() honors the $skip_form_styles flag. + * + * When a form has default styling disabled, the SureForms stylesheets must + * be skipped while external library styles (Tom Select, intl-tel-input) + * and all scripts keep loading. + */ + public function test_enqueue_scripts_and_styles() { + // Register all handles first, then start from an empty queue. + $this->frontend_assets->register_scripts(); + wp_styles()->queue = []; + wp_scripts()->queue = []; + + Frontend_Assets::enqueue_scripts_and_styles( true ); + + $this->assertFalse( wp_style_is( SRFM_SLUG . '-common', 'enqueued' ), 'SureForms stylesheets should be skipped when default styling is disabled.' ); + $this->assertFalse( wp_style_is( SRFM_SLUG . '-frontend-default', 'enqueued' ), 'SureForms stylesheets should be skipped when default styling is disabled.' ); + $this->assertFalse( wp_style_is( SRFM_SLUG . '-form', 'enqueued' ), 'SureForms stylesheets should be skipped when default styling is disabled.' ); + $this->assertTrue( wp_style_is( SRFM_SLUG . '-tom-select', 'enqueued' ), 'External library styles should always be enqueued.' ); + $this->assertTrue( wp_style_is( SRFM_SLUG . '-intl-tel-input', 'enqueued' ), 'External library styles should always be enqueued.' ); + $this->assertTrue( wp_script_is( SRFM_SLUG . '-frontend', 'enqueued' ), 'Scripts should always be enqueued.' ); + $this->assertTrue( wp_script_is( SRFM_SLUG . '-form-submit', 'enqueued' ), 'Scripts should always be enqueued.' ); + + // Reset the queues and enqueue with default styling enabled. + wp_styles()->queue = []; + wp_scripts()->queue = []; + + Frontend_Assets::enqueue_scripts_and_styles(); + + $this->assertTrue( wp_style_is( SRFM_SLUG . '-common', 'enqueued' ), 'SureForms stylesheets should be enqueued by default.' ); + $this->assertTrue( wp_style_is( SRFM_SLUG . '-frontend-default', 'enqueued' ), 'SureForms stylesheets should be enqueued by default.' ); + $this->assertTrue( wp_style_is( SRFM_SLUG . '-form', 'enqueued' ), 'SureForms stylesheets should be enqueued by default.' ); + } } From 74d11c7100e823f34db0121882f636308615bf49 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Wed, 8 Jul 2026 16:27:06 +0530 Subject: [PATCH 03/19] feat: add Payment History widget to Elementor and Bricks (#2930) Adds a Payment History widget/element to both page builders at parity with the srfm/payment-history Gutenberg block, rendering via Payment_History_Shortcode. Closes #2930 --- .../elements/payment-history-widget.php | 115 ++++++++++++++ inc/page-builders/bricks/service-provider.php | 1 + .../elementor/payment-history-widget.php | 148 ++++++++++++++++++ .../elementor/service-provider.php | 1 + 4 files changed, 265 insertions(+) create mode 100644 inc/page-builders/bricks/elements/payment-history-widget.php create mode 100644 inc/page-builders/elementor/payment-history-widget.php diff --git a/inc/page-builders/bricks/elements/payment-history-widget.php b/inc/page-builders/bricks/elements/payment-history-widget.php new file mode 100644 index 000000000..7f5de111b --- /dev/null +++ b/inc/page-builders/bricks/elements/payment-history-widget.php @@ -0,0 +1,115 @@ + element keywords. + */ + public function get_keywords() { + return [ + 'sureforms', + 'payment', + 'payment history', + 'subscription', + ]; + } + + /** + * Set element controls. + * + * @since x.x.x + * @return void + */ + public function set_controls() { + $this->controls['per_page'] = [ + 'tab' => 'content', + 'label' => __( 'Items per page', 'sureforms' ), + 'type' => 'number', + 'min' => 1, + 'default' => 10, + ]; + + $this->controls['show_subscription'] = [ + 'tab' => 'content', + 'label' => __( 'Show Subscriptions', 'sureforms' ), + 'type' => 'checkbox', + 'default' => true, + ]; + } + + /** + * Render the element output. + * + * Delegates to the Payment_History_Shortcode, mirroring the Gutenberg block. + * + * @since x.x.x + * @return void + */ + public function render() { + $per_page = absint( $this->settings['per_page'] ?? 10 ); + if ( $per_page <= 0 ) { + $per_page = 10; + } + + $atts = [ + 'per_page' => strval( $per_page ), + 'show_subscription' => ! empty( $this->settings['show_subscription'] ) ? 'true' : 'false', + ]; + + echo '
render_attributes( '_root' ) . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_attributes() returns escaped attributes. + echo Payment_History_Shortcode::get_instance()->render( $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped within the shortcode renderer. + echo '
'; + } +} diff --git a/inc/page-builders/bricks/service-provider.php b/inc/page-builders/bricks/service-provider.php index 95e2819bc..2671dffd2 100644 --- a/inc/page-builders/bricks/service-provider.php +++ b/inc/page-builders/bricks/service-provider.php @@ -42,6 +42,7 @@ public function widget() { [ $this, 'bricks_translatable_strings' ] ); \Bricks\Elements::register_element( __DIR__ . '/elements/form-widget.php' ); + \Bricks\Elements::register_element( __DIR__ . '/elements/payment-history-widget.php' ); } /** diff --git a/inc/page-builders/elementor/payment-history-widget.php b/inc/page-builders/elementor/payment-history-widget.php new file mode 100644 index 000000000..509db68d9 --- /dev/null +++ b/inc/page-builders/elementor/payment-history-widget.php @@ -0,0 +1,148 @@ + Widget categories. + */ + public function get_categories() { + return [ 'sureforms-elementor' ]; + } + + /** + * Get widget keywords. + * + * @since x.x.x + * @return array Widget keywords. + */ + public function get_keywords() { + return [ + 'sureforms', + 'payment', + 'payment history', + 'subscription', + 'transactions', + ]; + } + + /** + * Register widget controls. + * + * @since x.x.x + * @return void + */ + protected function register_controls() { + $this->start_controls_section( + 'srfm_payment_history_section', + [ + 'label' => __( 'Payment History', 'sureforms' ), + ] + ); + + $this->add_control( + 'per_page', + [ + 'label' => __( 'Items per page', 'sureforms' ), + 'type' => Controls_Manager::NUMBER, + 'min' => 1, + 'default' => 10, + ] + ); + + $this->add_control( + 'show_subscription', + [ + 'label' => __( 'Show Subscriptions', 'sureforms' ), + 'type' => Controls_Manager::SWITCHER, + 'label_on' => __( 'Show', 'sureforms' ), + 'label_off' => __( 'Hide', 'sureforms' ), + 'return_value' => 'true', + 'default' => 'true', + ] + ); + + $this->end_controls_section(); + } + + /** + * Render the widget output on the frontend. + * + * Delegates to the Payment_History_Shortcode, mirroring the Gutenberg block. + * + * @since x.x.x + * @return void + */ + protected function render() { + $settings = $this->get_settings_for_display(); + + if ( ! is_array( $settings ) ) { + return; + } + + $per_page = absint( $settings['per_page'] ?? 10 ); + if ( $per_page <= 0 ) { + $per_page = 10; + } + + $atts = [ + 'per_page' => strval( $per_page ), + 'show_subscription' => ! empty( $settings['show_subscription'] ) && 'true' === $settings['show_subscription'] ? 'true' : 'false', + ]; + + echo Payment_History_Shortcode::get_instance()->render( $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped within the shortcode renderer. + } +} diff --git a/inc/page-builders/elementor/service-provider.php b/inc/page-builders/elementor/service-provider.php index 43b25e9de..9b3d17e33 100644 --- a/inc/page-builders/elementor/service-provider.php +++ b/inc/page-builders/elementor/service-provider.php @@ -89,6 +89,7 @@ public function widget( $widgets_manager ) { return; } $widgets_manager->register( new Form_Widget() ); + $widgets_manager->register( new Payment_History_Widget() ); } } From 4042d93dfdbc26a0ef7fc88e1a4c6059244476d6 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Wed, 8 Jul 2026 17:58:29 +0530 Subject: [PATCH 04/19] test: add coverage for Payment History widgets; fix PHP Insights blank line --- .../elementor/payment-history-widget.php | 1 - .../elements/test-payment-history-widget.php | 72 +++++++++++++++++ .../bricks/test-service-provider.php | 39 +++++++++ .../elementor/test-payment-history-widget.php | 81 +++++++++++++++++++ .../elementor/test-service-provider.php | 28 +++++++ 5 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 tests/unit/inc/page-builders/bricks/elements/test-payment-history-widget.php create mode 100644 tests/unit/inc/page-builders/bricks/test-service-provider.php create mode 100644 tests/unit/inc/page-builders/elementor/test-payment-history-widget.php diff --git a/inc/page-builders/elementor/payment-history-widget.php b/inc/page-builders/elementor/payment-history-widget.php index 509db68d9..4c082d813 100644 --- a/inc/page-builders/elementor/payment-history-widget.php +++ b/inc/page-builders/elementor/payment-history-widget.php @@ -22,7 +22,6 @@ * @since x.x.x */ class Payment_History_Widget extends Widget_Base { - /** * Get widget name. * diff --git a/tests/unit/inc/page-builders/bricks/elements/test-payment-history-widget.php b/tests/unit/inc/page-builders/bricks/elements/test-payment-history-widget.php new file mode 100644 index 000000000..bb02658d1 --- /dev/null +++ b/tests/unit/inc/page-builders/bricks/elements/test-payment-history-widget.php @@ -0,0 +1,72 @@ +newInstanceWithoutConstructor(); + } + + /** + * Element label. + */ + public function test_get_label() { + $this->assertSame( 'Payment History', $this->element()->get_label() ); + } + + /** + * Keywords include payment-related terms. + */ + public function test_get_keywords() { + $this->assertContains( 'payment', $this->element()->get_keywords() ); + } + + /** + * set_controls() is the public Bricks control hook. + */ + public function test_set_controls() { + $method = new \ReflectionMethod( Payment_History_Widget::class, 'set_controls' ); + $this->assertTrue( $method->isPublic() ); + } + + /** + * render() is the public Bricks render hook. + */ + public function test_render() { + $method = new \ReflectionMethod( Payment_History_Widget::class, 'render' ); + $this->assertTrue( $method->isPublic() ); + } + } +} diff --git a/tests/unit/inc/page-builders/bricks/test-service-provider.php b/tests/unit/inc/page-builders/bricks/test-service-provider.php new file mode 100644 index 000000000..b567f99ab --- /dev/null +++ b/tests/unit/inc/page-builders/bricks/test-service-provider.php @@ -0,0 +1,39 @@ +markTestSkipped( 'Bricks is not available.' ); + } + } + + /** + * Test widget() registers the SureForms Bricks elements without error. + */ + public function test_widget() { + $provider = new \SRFM\Inc\Page_Builders\Bricks\Service_Provider(); + $provider->widget(); + $this->assertInstanceOf( \SRFM\Inc\Page_Builders\Bricks\Service_Provider::class, $provider ); + } +} diff --git a/tests/unit/inc/page-builders/elementor/test-payment-history-widget.php b/tests/unit/inc/page-builders/elementor/test-payment-history-widget.php new file mode 100644 index 000000000..0750a7000 --- /dev/null +++ b/tests/unit/inc/page-builders/elementor/test-payment-history-widget.php @@ -0,0 +1,81 @@ +newInstanceWithoutConstructor(); + } + + /** + * Widget name matches the block/widget identifier. + */ + public function test_get_name() { + $this->assertSame( 'srfm-payment-history', $this->widget()->get_name() ); + } + + /** + * Widget title. + */ + public function test_get_title() { + $this->assertSame( 'Payment History', $this->widget()->get_title() ); + } + + /** + * Widget icon is an Elementor icon. + */ + public function test_get_icon() { + $this->assertStringContainsString( 'eicon', $this->widget()->get_icon() ); + } + + /** + * Widget lives in the SureForms Elementor category. + */ + public function test_get_categories() { + $this->assertContains( 'sureforms-elementor', $this->widget()->get_categories() ); + } + + /** + * Keywords include payment-related terms. + */ + public function test_get_keywords() { + $this->assertContains( 'payment', $this->widget()->get_keywords() ); + } + + /** + * register_controls() is the protected Elementor control hook. + */ + public function test_register_controls() { + $method = new \ReflectionMethod( Payment_History_Widget::class, 'register_controls' ); + $this->assertTrue( $method->isProtected() ); + } + + /** + * render() is the protected Elementor render hook. + */ + public function test_render() { + $method = new \ReflectionMethod( Payment_History_Widget::class, 'render' ); + $this->assertTrue( $method->isProtected() ); + } +} diff --git a/tests/unit/inc/page-builders/elementor/test-service-provider.php b/tests/unit/inc/page-builders/elementor/test-service-provider.php index a6d9f890e..62494801b 100644 --- a/tests/unit/inc/page-builders/elementor/test-service-provider.php +++ b/tests/unit/inc/page-builders/elementor/test-service-provider.php @@ -37,4 +37,32 @@ public function test_load_scripts() { $this->assertTrue( wp_script_is( 'sureforms-elementor-editor', 'enqueued' ) ); wp_dequeue_script( 'sureforms-elementor-editor' ); } + + /** + * Test widget() registers the SureForms widgets, including Payment History. + */ + public function test_widget() { + $manager = new class() { + /** + * Registered widget class names. + * + * @var array + */ + public $registered = []; + + /** + * Mock Elementor widget registration. + * + * @param object $widget Widget instance. + * @return void + */ + public function register( $widget ) { + $this->registered[] = get_class( $widget ); + } + }; + + ( new \SRFM\Inc\Page_Builders\Elementor\Service_Provider() )->widget( $manager ); + + $this->assertContains( \SRFM\Inc\Page_Builders\Elementor\Payment_History_Widget::class, $manager->registered ); + } } From 207055c53ac7fe00c2986dbec6410a6016a1d7fd Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 9 Jul 2026 00:32:20 +0530 Subject: [PATCH 05/19] feat: expose disable default styling toggle via Abilities API Add disableDefaultStyles to the formStyling object in the create-form and update-form ability input schemas, and map it to the _srfm_forms_styling.disable_default_styles meta so MCP clients can enable the unstyled mode when creating/updating a form. --- inc/abilities/ABILITIES.md | 3 ++- inc/abilities/forms/create-form.php | 12 ++++++--- inc/abilities/forms/form-metadata.php | 3 +++ inc/abilities/forms/update-form.php | 12 ++++++--- .../abilities/forms/test-form-metadata.php | 25 +++++++++++++++++++ 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/inc/abilities/ABILITIES.md b/inc/abilities/ABILITIES.md index e4f2bd45e..7beddaa75 100644 --- a/inc/abilities/ABILITIES.md +++ b/inc/abilities/ABILITIES.md @@ -95,7 +95,8 @@ Complete reference for all 15 abilities registered with the WordPress Abilities - `emailConfirmation` — `{name, subject, emailBody}` - `compliance` — `{enableCompliance, neverStoreEntries, autoDeleteEntries, autoDeleteEntriesDays}` - `instantForm` — `{instantForm, showTitle, bannerColor, useBannerColorAsBackground, formBackgroundColor, formWidth, formSlug}` -- `styling` — `{primaryColor, textColor, textColorOnPrimary, fieldSpacing, submitAlignment}` +- `styling` — `{submitAlignment}` +- `formStyling` — `{primaryColor, textColor, textColorOnPrimary, fieldSpacing, disableDefaultStyles}` (`disableDefaultStyles: true` disables SureForms' default frontend styles so the theme styling applies) **Output:** ```json diff --git a/inc/abilities/forms/create-form.php b/inc/abilities/forms/create-form.php index b0944f2a7..7ef537212 100644 --- a/inc/abilities/forms/create-form.php +++ b/inc/abilities/forms/create-form.php @@ -137,23 +137,27 @@ public function get_input_schema() { 'type' => 'object', 'description' => __( 'Form styling settings including colors and spacing.', 'sureforms' ), 'properties' => [ - 'primaryColor' => [ + 'primaryColor' => [ 'type' => 'string', 'description' => __( 'Primary/accent color as hex (e.g. #111C44).', 'sureforms' ), ], - 'textColor' => [ + 'textColor' => [ 'type' => 'string', 'description' => __( 'Text color as hex (e.g. #1E1E1E).', 'sureforms' ), ], - 'textColorOnPrimary' => [ + 'textColorOnPrimary' => [ 'type' => 'string', 'description' => __( 'Text color on primary backgrounds as hex (e.g. #FFFFFF).', 'sureforms' ), ], - 'fieldSpacing' => [ + 'fieldSpacing' => [ 'type' => 'string', 'description' => __( 'Spacing between fields.', 'sureforms' ), 'enum' => [ 'small', 'medium', 'large' ], ], + 'disableDefaultStyles' => [ + 'type' => 'boolean', + 'description' => __( 'When true, SureForms does not enqueue its default frontend styles for this form, so the active theme styling applies instead.', 'sureforms' ), + ], ], ], 'emailNotification' => [ diff --git a/inc/abilities/forms/form-metadata.php b/inc/abilities/forms/form-metadata.php index 273cfc3b9..260001e6c 100644 --- a/inc/abilities/forms/form-metadata.php +++ b/inc/abilities/forms/form-metadata.php @@ -162,6 +162,9 @@ protected function apply_metadata_overrides( $post_metas, $meta_data ) { $styling_data['field_spacing'] = $form_styling['fieldSpacing']; } } + if ( isset( $form_styling['disableDefaultStyles'] ) ) { + $styling_data['disable_default_styles'] = (bool) $form_styling['disableDefaultStyles']; + } $post_metas['_srfm_forms_styling'] = $styling_data; } diff --git a/inc/abilities/forms/update-form.php b/inc/abilities/forms/update-form.php index 997bd958c..6b6a657f4 100644 --- a/inc/abilities/forms/update-form.php +++ b/inc/abilities/forms/update-form.php @@ -140,23 +140,27 @@ public function get_input_schema() { 'type' => 'object', 'description' => __( 'Form styling settings including colors and spacing.', 'sureforms' ), 'properties' => [ - 'primaryColor' => [ + 'primaryColor' => [ 'type' => 'string', 'description' => __( 'Primary/accent color as hex (e.g. #111C44).', 'sureforms' ), ], - 'textColor' => [ + 'textColor' => [ 'type' => 'string', 'description' => __( 'Text color as hex (e.g. #1E1E1E).', 'sureforms' ), ], - 'textColorOnPrimary' => [ + 'textColorOnPrimary' => [ 'type' => 'string', 'description' => __( 'Text color on primary backgrounds as hex (e.g. #FFFFFF).', 'sureforms' ), ], - 'fieldSpacing' => [ + 'fieldSpacing' => [ 'type' => 'string', 'description' => __( 'Spacing between fields.', 'sureforms' ), 'enum' => [ 'small', 'medium', 'large' ], ], + 'disableDefaultStyles' => [ + 'type' => 'boolean', + 'description' => __( 'When true, SureForms does not enqueue its default frontend styles for this form, so the active theme styling applies instead.', 'sureforms' ), + ], ], ], 'emailNotification' => [ diff --git a/tests/unit/inc/abilities/forms/test-form-metadata.php b/tests/unit/inc/abilities/forms/test-form-metadata.php index 7d0032592..e7670b669 100644 --- a/tests/unit/inc/abilities/forms/test-form-metadata.php +++ b/tests/unit/inc/abilities/forms/test-form-metadata.php @@ -38,4 +38,29 @@ public function test_apply_metadata_overrides() { $reflection->setAccessible( true ); $this->assertTrue( $reflection->isProtected() ); } + + /** + * Test formStyling.disableDefaultStyles maps to the _srfm_forms_styling meta flag. + */ + public function test_form_styling_disable_default_styles_maps_to_meta() { + $reflection = new \ReflectionMethod( $this->ability, 'apply_metadata_overrides' ); + $reflection->setAccessible( true ); + + // Enable it, and confirm it merges into existing styling data. + $result = $reflection->invoke( + $this->ability, + [ '_srfm_forms_styling' => [ 'primary_color' => '#111C44' ] ], + [ 'formStyling' => [ 'disableDefaultStyles' => true ] ] + ); + $this->assertTrue( $result['_srfm_forms_styling']['disable_default_styles'] ); + $this->assertSame( '#111C44', $result['_srfm_forms_styling']['primary_color'] ); + + // Explicit false is respected (not dropped like empty values). + $result = $reflection->invoke( + $this->ability, + [], + [ 'formStyling' => [ 'disableDefaultStyles' => false ] ] + ); + $this->assertFalse( $result['_srfm_forms_styling']['disable_default_styles'] ); + } } From acef36caf99d598e84d61c216d06ed774566248a Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 9 Jul 2026 10:31:45 +0530 Subject: [PATCH 06/19] refactor: make disable-default-styling programmatic-only Remove the block-editor sidebar toggle for disabling default styling and keep the feature controllable only programmatically: - Add srfm_disable_default_styles filter in Form_Styling::is_default_styling_disabled() so themes/plugins can override the stored meta per form. - Route the marker class and inline-CSS guard in generate-form-markup through the canonical reader so the filter governs them too. - The _srfm_forms_styling.disable_default_styles meta stays registered and remains settable via REST and the MCP/Abilities API. - Drop the now-unused ToggleControl import and the Style tab UI block. --- inc/form-styling.php | 21 ++++++++-- inc/generate-form-markup.php | 4 +- .../tabs/StyleSettings.js | 27 ------------ tests/unit/inc/test-form-styling.php | 42 +++++++++++++++++++ 4 files changed, 62 insertions(+), 32 deletions(-) diff --git a/inc/form-styling.php b/inc/form-styling.php index b710a9f9b..f946bd491 100644 --- a/inc/form-styling.php +++ b/inc/form-styling.php @@ -151,9 +151,10 @@ public static function has_custom_styling( $block_attrs ) { /** * Check if the form has default SureForms styling disabled. * - * When enabled from the Style tab, the form is rendered without the - * SureForms frontend stylesheets and inline CSS variables so the site's - * own CSS fully controls the form's appearance. + * When enabled (via the `_srfm_forms_styling` meta set through REST/MCP, or + * the `srfm_disable_default_styles` filter), the form is rendered without the + * SureForms frontend stylesheets and inline CSS variables so the site's own + * CSS fully controls the form's appearance. * * @param int|string $form_id Form post ID. * @return bool True when default styling is disabled for the form. @@ -166,8 +167,20 @@ public static function is_default_styling_disabled( $form_id ) { } $form_styling = get_post_meta( $form_id, '_srfm_forms_styling', true ); + $disabled = is_array( $form_styling ) && ! empty( $form_styling['disable_default_styles'] ); - return is_array( $form_styling ) && ! empty( $form_styling['disable_default_styles'] ); + /** + * Filters whether SureForms' default frontend styling is disabled for a form. + * + * Lets themes/plugins toggle the unstyled mode programmatically, overriding the + * stored per-form meta. Return true to render the form without SureForms' default + * stylesheets and inline CSS variables. + * + * @param bool $disabled Whether default styling is disabled (from meta). + * @param int $form_id Form post ID. + * @since x.x.x + */ + return (bool) apply_filters( 'srfm_disable_default_styles', $disabled, $form_id ); } /** diff --git a/inc/generate-form-markup.php b/inc/generate-form-markup.php index e0937b3c5..0048600cd 100644 --- a/inc/generate-form-markup.php +++ b/inc/generate-form-markup.php @@ -157,7 +157,9 @@ public static function get_form_markup( $id, $show_title_current_page = true, $s // When enabled, the form renders without the SureForms inline CSS variables so // the site's own CSS fully controls its appearance. Per-form Custom CSS still applies. - $disable_default_styles = ! empty( $form_styling['disable_default_styles'] ); + // Read through the canonical checker so the `srfm_disable_default_styles` filter + // governs the marker class and inline CSS guard as well as the enqueue path. + $disable_default_styles = Form_Styling::is_default_styling_disabled( $id ); // Background Settings. $bg_type = $form_styling['bg_type'] ?? 'color'; $bg_color = $form_styling['bg_color'] ?? ''; diff --git a/src/admin/single-form-settings/tabs/StyleSettings.js b/src/admin/single-form-settings/tabs/StyleSettings.js index 7a235b76f..c3ecc1db9 100644 --- a/src/admin/single-form-settings/tabs/StyleSettings.js +++ b/src/admin/single-form-settings/tabs/StyleSettings.js @@ -3,7 +3,6 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; import { store as editorStore } from '@wordpress/editor'; import { applyFilters } from '@wordpress/hooks'; -import { ToggleControl } from '@wordpress/components'; import AdvancedPopColorControl from '@Components/color-control/advanced-pop-color-control.js'; import SRFMAdvancedPanelBody from '@Components/advanced-panel-body'; import SRFMTextControl from '@Components/text-control'; @@ -1174,32 +1173,6 @@ function StyleSettings( props ) { ]; const advanced = [ - { - id: 'disable_default_styles', - component: ( - <> - { - updateFormStyling( - 'disable_default_styles', - !! value - ); - } } - /> -

- { __( - 'Render this form without the SureForms default styles, so your own CSS (theme styles or the Custom CSS panel) fully controls its appearance. Essential styles for advanced fields like Dropdown and Phone are still loaded. This setting takes effect only on the live page, not in the editor preview.', - 'sureforms' - ) } -

- - ), - }, { id: 'additional_css_classes', component: ( diff --git a/tests/unit/inc/test-form-styling.php b/tests/unit/inc/test-form-styling.php index 55670d0f8..f7bfc2a52 100644 --- a/tests/unit/inc/test-form-styling.php +++ b/tests/unit/inc/test-form-styling.php @@ -491,6 +491,48 @@ public function test_is_default_styling_disabled() { wp_delete_post( $form_id, true ); } + /** + * Test the srfm_disable_default_styles filter overrides the stored meta both ways. + */ + public function test_is_default_styling_disabled_filter_override() { + $form_id = wp_insert_post( + [ + 'post_type' => SRFM_FORMS_POST_TYPE, + 'post_status' => 'publish', + 'post_title' => 'Styling Filter Form', + ] + ); + + // Meta says disabled; a filter returning false re-enables default styling. + update_post_meta( $form_id, '_srfm_forms_styling', [ 'disable_default_styles' => true ] ); + add_filter( 'srfm_disable_default_styles', '__return_false' ); + $this->assertFalse( Form_Styling::is_default_styling_disabled( $form_id ) ); + remove_all_filters( 'srfm_disable_default_styles' ); + + // Meta says enabled (default); a filter returning true disables default styling. + update_post_meta( $form_id, '_srfm_forms_styling', [ 'disable_default_styles' => false ] ); + add_filter( 'srfm_disable_default_styles', '__return_true' ); + $this->assertTrue( Form_Styling::is_default_styling_disabled( $form_id ) ); + remove_all_filters( 'srfm_disable_default_styles' ); + + // The filter receives the form ID as the second argument. + $received_form_id = 0; + add_filter( + 'srfm_disable_default_styles', + static function ( $disabled, $id ) use ( &$received_form_id ) { + $received_form_id = $id; + return $disabled; + }, + 10, + 2 + ); + Form_Styling::is_default_styling_disabled( $form_id ); + $this->assertSame( $form_id, $received_form_id ); + remove_all_filters( 'srfm_disable_default_styles' ); + + wp_delete_post( $form_id, true ); + } + /** * Test get_form_ids_from_content() finds top-level blocks, nested blocks and shortcodes. */ From 62e65943109c60c7c015087639695019ccd59e5b Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 9 Jul 2026 19:03:11 +0530 Subject: [PATCH 07/19] fix: open SureForms Description panel by default in the form editor Gutenberg persists PluginDocumentSettingPanel state as closed until the user toggles it, so on a fresh install the SureForms Description panel rendered in single-form-settings/Editor.js stayed collapsed. forcePanel() already enables and opens the sibling Form Options panel on editor load; apply the same enable + open pattern to srfm-form-specific-settings/srfm-description so both panels are expanded by default. Fixes #2288 (SF-1485) --- src/admin/single-form-settings/utils.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/admin/single-form-settings/utils.js b/src/admin/single-form-settings/utils.js index 8ea82e51d..9e9105594 100644 --- a/src/admin/single-form-settings/utils.js +++ b/src/admin/single-form-settings/utils.js @@ -73,6 +73,24 @@ export const forcePanel = () => { 'srfm-form-specific-settings/srfm-sidebar' ); } + if ( + ! select( 'core/editor' ).isEditorPanelEnabled( + 'srfm-form-specific-settings/srfm-description' + ) + ) { + dispatch( 'core/edit-post' ).toggleEditorPanelEnabled( + 'srfm-form-specific-settings/srfm-description' + ); + } + if ( + ! select( 'core/editor' ).isEditorPanelOpened( + 'srfm-form-specific-settings/srfm-description' + ) + ) { + dispatch( 'core/edit-post' ).toggleEditorPanelOpened( + 'srfm-form-specific-settings/srfm-description' + ); + } }; export const validateClassName = ( className ) => { From 31a5678416f51676b53a0e339c60d8860be3963b Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 13 Jul 2026 18:02:53 +0530 Subject: [PATCH 08/19] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20sin?= =?UTF-8?q?gle=20Custom=20CSS=20output=20path=20+=20reusable-block=20detec?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert the Change-2 guard removal: on the single/instant form view templates/single-form.php already outputs the per-form Custom CSS in , so get_form_markup() emitting it too duplicated it. The scoped output is now embedded-views-only again (single output path per view, zero net behavior change vs the released plugin). - Skip the whole scoped + assertFalse( $result['_srfm_forms_styling']['disable_default_styles'] ); } + + /** + * styling.submitAlignment and the formStyling object map independently in + * one apply_metadata_overrides() call — guards the documented split. + */ + public function test_styling_and_form_styling_map_independently() { + $reflection = new \ReflectionMethod( $this->ability, 'apply_metadata_overrides' ); + $reflection->setAccessible( true ); + + $result = $reflection->invoke( + $this->ability, + [], + [ + 'styling' => [ 'submitAlignment' => 'center' ], + 'formStyling' => [ + 'primaryColor' => '#123456', + 'disableDefaultStyles' => true, + ], + ] + ); + + $this->assertSame( 'center', $result['_srfm_submit_alignment'] ); + $this->assertSame( '#123456', $result['_srfm_forms_styling']['primary_color'] ); + $this->assertTrue( $result['_srfm_forms_styling']['disable_default_styles'] ); + } } diff --git a/tests/unit/inc/test-form-styling.php b/tests/unit/inc/test-form-styling.php index f7bfc2a52..4a24e300e 100644 --- a/tests/unit/inc/test-form-styling.php +++ b/tests/unit/inc/test-form-styling.php @@ -616,4 +616,51 @@ public function test_should_skip_frontend_styles() { wp_delete_post( $styled_form_id, true ); wp_delete_post( $page_id, true ); } + + /** + * A form embedded only through a reusable/synced pattern (core/block ref) + * is detected by get_form_ids_from_content(). + */ + public function test_get_form_ids_resolves_reusable_block_refs() { + $ref_id = wp_insert_post( + [ + 'post_title' => 'Pattern with form', + 'post_type' => 'wp_block', + 'post_status' => 'publish', + 'post_content' => '', + ] + ); + + $content = ''; + $this->assertSame( [ 111, 4242 ], Form_Styling::get_form_ids_from_content( $content ) ); + + // A page with ONLY the pattern (no inline srfm/form) is detected too — + // the has_block( 'srfm/form' ) gate alone would have missed it. + $this->assertSame( [ 4242 ], Form_Styling::get_form_ids_from_content( '' ) ); + + wp_delete_post( $ref_id, true ); + } + + /** + * Self-referencing reusable blocks terminate (cycle guard) instead of recursing forever. + */ + public function test_get_form_ids_reusable_block_cycle_terminates() { + $ref_id = wp_insert_post( + [ + 'post_title' => 'Self-referencing pattern', + 'post_type' => 'wp_block', + 'post_status' => 'publish', + ] + ); + wp_update_post( + [ + 'ID' => $ref_id, + 'post_content' => '', + ] + ); + + $this->assertSame( [], Form_Styling::get_form_ids_from_content( '' ) ); + + wp_delete_post( $ref_id, true ); + } } diff --git a/tests/unit/inc/test-generate-form-markup.php b/tests/unit/inc/test-generate-form-markup.php index deb14b07b..97c647aed 100644 --- a/tests/unit/inc/test-generate-form-markup.php +++ b/tests/unit/inc/test-generate-form-markup.php @@ -356,4 +356,107 @@ public function test_get_cf_turnstile_script() { remove_filter( 'doing_it_wrong_trigger_error', '__return_false' ); } + + /** + * Create a published form with optional Custom CSS / disable-styling meta. + * + * @param string $custom_css Custom CSS meta value. + * @param bool $disable Whether default styling is disabled. + * @return int Form ID. + */ + private function make_form_with_styling( $custom_css = '', $disable = false ) { + remove_all_actions( 'wp_insert_post_data' ); + + $form_id = wp_insert_post( + [ + 'post_title' => 'Styling test form', + 'post_type' => SRFM_FORMS_POST_TYPE, + 'post_status' => 'publish', + 'post_content' => 'simple content', + ] + ); + + if ( '' !== $custom_css ) { + update_post_meta( $form_id, '_srfm_form_custom_css', $custom_css ); + } + if ( $disable ) { + update_post_meta( $form_id, '_srfm_forms_styling', [ 'disable_default_styles' => true ] ); + } + + return $form_id; + } + + /** + * Custom CSS appears exactly once on embedded views and is NOT emitted by + * get_form_markup() on the form's own single/instant view — there + * templates/single-form.php owns the (head, unscoped) output, so a second + * copy here would duplicate it. + */ + public function test_custom_css_once_embedded_and_absent_on_single_view() { + $css = '.srfm-test-marker { color: red; }'; + $form_id = $this->make_form_with_styling( $css ); + + // Embedded context: global post is not the form. + $GLOBALS['post'] = null; + $markup = Generate_Form_Markup::get_form_markup( $form_id ); + $this->assertSame( 1, substr_count( $markup, '.srfm-test-marker' ), 'Embedded markup must contain the Custom CSS exactly once.' ); + + // Single/instant view context: the form itself is the queried post. + $GLOBALS['post'] = get_post( $form_id ); + $markup = Generate_Form_Markup::get_form_markup( $form_id ); + $this->assertStringNotContainsString( '.srfm-test-marker', $markup, 'On the single view the template outputs the Custom CSS; the markup must not duplicate it.' ); + + $GLOBALS['post'] = null; + wp_delete_post( $form_id, true ); + } + + /** + * Custom CSS still applies when default styling is disabled, the marker + * class renders, and the inline --srfm-* variable block is skipped. + */ + public function test_custom_css_and_marker_class_when_styling_disabled() { + $css = '.srfm-test-marker { color: red; }'; + $form_id = $this->make_form_with_styling( $css, true ); + + $GLOBALS['post'] = null; + $markup = Generate_Form_Markup::get_form_markup( $form_id ); + + $this->assertStringContainsString( '.srfm-test-marker', $markup ); + $this->assertStringContainsString( 'srfm-styling-none', $markup ); + $this->assertStringNotContainsString( '--srfm-color-scheme-primary', $markup ); + + wp_delete_post( $form_id, true ); + } + + /** + * With default styling enabled the inline variable block renders and the + * marker class is absent. + */ + public function test_inline_variables_present_when_styling_enabled() { + $form_id = $this->make_form_with_styling(); + + $GLOBALS['post'] = null; + $markup = Generate_Form_Markup::get_form_markup( $form_id ); + + $this->assertStringContainsString( '--srfm-color-scheme-primary', $markup ); + $this->assertStringNotContainsString( 'srfm-styling-none', $markup ); + + wp_delete_post( $form_id, true ); + } + + /** + * When styling is disabled and there is no Custom CSS, no style tag is + * emitted at all (no empty scoped ruleset). + */ + public function test_no_style_tag_when_disabled_without_custom_css() { + $form_id = $this->make_form_with_styling( '', true ); + + $GLOBALS['post'] = null; + $markup = Generate_Form_Markup::get_form_markup( $form_id ); + + $this->assertStringNotContainsString( 'assertStringContainsString( 'srfm-styling-none', $markup ); + + wp_delete_post( $form_id, true ); + } } From c1e4a96ccc5e6464002e874feb3e416b30de43be Mon Sep 17 00:00:00 2001 From: Avinash Kumar Sharma Date: Thu, 16 Jul 2026 12:11:10 +0530 Subject: [PATCH 09/19] fix: reset notice actions margin to fix editor nudge button alignment WordPress core's `.components-notice__actions` wrapper has a default `margin-top: 8px` which pushed the "Create Form" CTA below the vertical centre of the banner. Reset `margin: 0` and add `flex-shrink: 0` on `.components-notice__actions` inside the nudge scope so the button stays centred alongside the message text. Fixes #2943 Co-Authored-By: Claude Sonnet 4.6 --- sass/editor-nudge.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sass/editor-nudge.scss b/sass/editor-nudge.scss index 3bbf2549c..cbaef4771 100644 --- a/sass/editor-nudge.scss +++ b/sass/editor-nudge.scss @@ -39,6 +39,11 @@ $srfm-nudge-cta-color: #fff; margin: 0 25px 0 0; } + .components-notice__actions { + margin: 0; + flex-shrink: 0; + } + .components-notice__dismiss { color: #757575; From 6dbf3ed2bba957e19215e1273b37f779b89ec4d0 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 16 Jul 2026 12:26:47 +0530 Subject: [PATCH 10/19] Version Bump 2.12.2 --- README.md | 13 +- inc/form-styling.php | 10 +- .../elements/payment-history-widget.php | 12 +- .../elementor/payment-history-widget.php | 18 +-- languages/sureforms.pot | 142 ++++++++++-------- package-lock.json | 4 +- package.json | 2 +- readme.txt | 13 +- sureforms.php | 6 +- 9 files changed, 121 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 1b92b8049..5f143407a 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ **Contributors:** [brainstormforce](https://profiles.wordpress.org/brainstormforce/) **Tags:** forms, contact form, form builder, survey, payment form **Requires at least:** 6.4 -**Tested up to:** 7.0 +**Tested up to:** 7.0.1 **Requires PHP:** 7.4 -**Stable tag:** 2.12.1 +**Stable tag:** 2.12.2 **License:** GPLv2 or later **License URI:** http://www.gnu.org/licenses/gpl-2.0.html @@ -458,6 +458,11 @@ Yes. SureForms Business includes fully functional user registration forms and lo You can report security issues through our [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/). We collaborate with Patchstack to provide opportunities for researchers to report vulnerabilities. The Patchstack team will help validate, triage, and handle any reported security issues. ## Changelog ## +### 2.12.2 - 16th July 2026 ### +* New: Added a Payment History widget for Elementor and Bricks, bringing the same subscriptions and transaction view already available in the block editor to both page builders. +* New: Added a per-form "Disable SureForms Styling" option so you can turn off the plugin's default field styling and match your page design without fighting CSS. +* Fix: Custom CSS now applies correctly on the single form view. +* Fix: The SureForms Description panel now opens by default in the form editor so field descriptions are easier to find. ### 2.12.1 - 8th July 2026 ### * New: Added an AI-powered quick draft flow on the dashboard to generate forms faster, with built-in usage tracking. * Improvement: Email fields now enforce standard RFC 5321 length limits (a 64-character local part and 255-character domain) to prevent oversized submissions, with a filter to customize the limits. @@ -470,10 +475,6 @@ You can report security issues through our [Bug Bounty Program](https://brainsto * Fix: Forms with multiple Cloudflare Turnstile widgets now submit correctly instead of silently failing with a generic error. * Fix: Resolved an issue where the Stripe Payment Element failed to render on live accounts that have Bacs Direct Debit, Link, Cash App, or BNPL enabled. * Fix: Restored the form editor on WordPress 6.x sites running plugins that register older-style blocks such as ThirstyAffiliates, Ninja Forms, and Gravity Forms. -### 2.11.1 - 16th June 2026 ### -* Fix: Phone field auto country detection always resolved to the United States. -* Fix: Corrected the Cloudflare Turnstile "Get Keys" link. -* Fix: This update addressed a security bug. Props to Yaswanth Reddy Sunkara for reporting it responsibly to our team. The full changelog is available [here](https://sureforms.com/whats-new/?utm_source=wordpress.org&utm_medium=whats_new). ## Upgrade Notice ## diff --git a/inc/form-styling.php b/inc/form-styling.php index b9ebf7f5d..627e4c672 100644 --- a/inc/form-styling.php +++ b/inc/form-styling.php @@ -158,7 +158,7 @@ public static function has_custom_styling( $block_attrs ) { * * @param int|string $form_id Form post ID. * @return bool True when default styling is disabled for the form. - * @since x.x.x + * @since 2.12.2 */ public static function is_default_styling_disabled( $form_id ) { $form_id = absint( $form_id ); @@ -178,7 +178,7 @@ public static function is_default_styling_disabled( $form_id ) { * * @param bool $disabled Whether default styling is disabled (from meta). * @param int $form_id Form post ID. - * @since x.x.x + * @since 2.12.2 */ return (bool) apply_filters( 'srfm_disable_default_styles', $disabled, $form_id ); } @@ -192,7 +192,7 @@ public static function is_default_styling_disabled( $form_id ) { * * @param \WP_Post $post Post being rendered. * @return bool True when the frontend stylesheets can be skipped. - * @since x.x.x + * @since 2.12.2 */ public static function should_skip_frontend_styles( $post ) { if ( SRFM_FORMS_POST_TYPE === $post->post_type ) { @@ -222,7 +222,7 @@ public static function should_skip_frontend_styles( $post ) { * * @param string $content Post content. * @return array Unique form IDs found in the content. - * @since x.x.x + * @since 2.12.2 */ public static function get_form_ids_from_content( $content ) { $form_ids = []; @@ -255,7 +255,7 @@ public static function get_form_ids_from_content( $content ) { * @param array $visited_refs Reusable-block post IDs already expanded, * keyed by ID — guards against reference cycles. * @return array Form IDs found in srfm/form blocks. - * @since x.x.x + * @since 2.12.2 */ private static function collect_form_block_ids( $blocks, &$visited_refs = [] ) { $form_ids = []; diff --git a/inc/page-builders/bricks/elements/payment-history-widget.php b/inc/page-builders/bricks/elements/payment-history-widget.php index 7f5de111b..21d571d4a 100644 --- a/inc/page-builders/bricks/elements/payment-history-widget.php +++ b/inc/page-builders/bricks/elements/payment-history-widget.php @@ -3,7 +3,7 @@ * Bricks SureForms Payment History element. * * @package sureforms. - * @since x.x.x + * @since 2.12.2 */ namespace SRFM\Inc\Page_Builders\Bricks\Elements; @@ -17,7 +17,7 @@ /** * SureForms Bricks element that displays the Payment History (parity with the srfm/payment-history block). * - * @since x.x.x + * @since 2.12.2 */ class Payment_History_Widget extends \Bricks\Element { /** @@ -44,7 +44,7 @@ class Payment_History_Widget extends \Bricks\Element { /** * Get element label. * - * @since x.x.x + * @since 2.12.2 * @return string element label. */ public function get_label() { @@ -54,7 +54,7 @@ public function get_label() { /** * Get element keywords. * - * @since x.x.x + * @since 2.12.2 * @return array element keywords. */ public function get_keywords() { @@ -69,7 +69,7 @@ public function get_keywords() { /** * Set element controls. * - * @since x.x.x + * @since 2.12.2 * @return void */ public function set_controls() { @@ -94,7 +94,7 @@ public function set_controls() { * * Delegates to the Payment_History_Shortcode, mirroring the Gutenberg block. * - * @since x.x.x + * @since 2.12.2 * @return void */ public function render() { diff --git a/inc/page-builders/elementor/payment-history-widget.php b/inc/page-builders/elementor/payment-history-widget.php index 4c082d813..4d4e872ae 100644 --- a/inc/page-builders/elementor/payment-history-widget.php +++ b/inc/page-builders/elementor/payment-history-widget.php @@ -3,7 +3,7 @@ * Elementor SureForms Payment History widget. * * @package sureforms. - * @since x.x.x + * @since 2.12.2 */ namespace SRFM\Inc\Page_Builders\Elementor; @@ -19,13 +19,13 @@ /** * SureForms Elementor widget that displays the Payment History (parity with the srfm/payment-history block). * - * @since x.x.x + * @since 2.12.2 */ class Payment_History_Widget extends Widget_Base { /** * Get widget name. * - * @since x.x.x + * @since 2.12.2 * @return string Widget name. */ public function get_name() { @@ -35,7 +35,7 @@ public function get_name() { /** * Get widget title. * - * @since x.x.x + * @since 2.12.2 * @return string Widget title. */ public function get_title() { @@ -45,7 +45,7 @@ public function get_title() { /** * Get widget icon. * - * @since x.x.x + * @since 2.12.2 * @return string Widget icon. */ public function get_icon() { @@ -55,7 +55,7 @@ public function get_icon() { /** * Get widget categories. * - * @since x.x.x + * @since 2.12.2 * @return array Widget categories. */ public function get_categories() { @@ -65,7 +65,7 @@ public function get_categories() { /** * Get widget keywords. * - * @since x.x.x + * @since 2.12.2 * @return array Widget keywords. */ public function get_keywords() { @@ -81,7 +81,7 @@ public function get_keywords() { /** * Register widget controls. * - * @since x.x.x + * @since 2.12.2 * @return void */ protected function register_controls() { @@ -122,7 +122,7 @@ protected function register_controls() { * * Delegates to the Payment_History_Shortcode, mirroring the Gutenberg block. * - * @since x.x.x + * @since 2.12.2 * @return void */ protected function render() { diff --git a/languages/sureforms.pot b/languages/sureforms.pot index 95c23fd6d..d126c2ed0 100644 --- a/languages/sureforms.pot +++ b/languages/sureforms.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPLv2 or later. msgid "" msgstr "" -"Project-Id-Version: SureForms 2.12.1\n" +"Project-Id-Version: SureForms 2.12.2\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/sureforms\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-07-06T02:38:45+00:00\n" +"POT-Creation-Date: 2026-07-16T06:55:18+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: sureforms\n" @@ -23,7 +23,7 @@ msgstr "" #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 -#: inc/page-builders/bricks/service-provider.php:55 +#: inc/page-builders/bricks/service-provider.php:56 #: inc/page-builders/elementor/form-widget.php:144 #: inc/page-builders/elementor/form-widget.php:301 #: inc/page-builders/elementor/service-provider.php:74 @@ -646,98 +646,103 @@ msgstr "" msgid "Spacing between fields." msgstr "" -#: inc/abilities/forms/create-form.php:161 -#: inc/abilities/forms/update-form.php:164 -msgid "Email notification settings for the first notification. Merges with existing." +#: inc/abilities/forms/create-form.php:159 +#: inc/abilities/forms/update-form.php:162 +msgid "When true, SureForms does not enqueue its default frontend styles for this form, so the active theme styling applies instead." msgstr "" #: inc/abilities/forms/create-form.php:165 #: inc/abilities/forms/update-form.php:168 -msgid "Enable or disable the notification." +msgid "Email notification settings for the first notification. Merges with existing." msgstr "" #: inc/abilities/forms/create-form.php:169 #: inc/abilities/forms/update-form.php:172 -msgid "Notification name." +msgid "Enable or disable the notification." msgstr "" #: inc/abilities/forms/create-form.php:173 #: inc/abilities/forms/update-form.php:176 -msgid "Recipient email. Supports smart tags like {admin_email}." +msgid "Notification name." msgstr "" -#: inc/abilities/forms/create-form.php:178 -#: inc/abilities/forms/update-form.php:181 -msgid "Sender name. Supports smart tags like {site_title}." +#: inc/abilities/forms/create-form.php:177 +#: inc/abilities/forms/update-form.php:180 +msgid "Recipient email. Supports smart tags like {admin_email}." msgstr "" -#: inc/abilities/forms/create-form.php:185 -#: inc/abilities/forms/update-form.php:188 -msgid "Email subject. Supports smart tags like {form_title}." +#: inc/abilities/forms/create-form.php:182 +#: inc/abilities/forms/update-form.php:185 +msgid "Sender name. Supports smart tags like {site_title}." msgstr "" #: inc/abilities/forms/create-form.php:189 #: inc/abilities/forms/update-form.php:192 -msgid "Email body. Supports smart tags like {all_data}." +msgid "Email subject. Supports smart tags like {form_title}." msgstr "" -#: inc/abilities/forms/create-form.php:195 -#: inc/abilities/forms/update-form.php:198 -msgid "Form submission restrictions and scheduling." +#: inc/abilities/forms/create-form.php:193 +#: inc/abilities/forms/update-form.php:196 +msgid "Email body. Supports smart tags like {all_data}." msgstr "" #: inc/abilities/forms/create-form.php:199 #: inc/abilities/forms/update-form.php:202 -msgid "Enable entry limit restriction." +msgid "Form submission restrictions and scheduling." msgstr "" #: inc/abilities/forms/create-form.php:203 #: inc/abilities/forms/update-form.php:206 -msgid "Maximum number of entries allowed (0 = unlimited)." +msgid "Enable entry limit restriction." msgstr "" #: inc/abilities/forms/create-form.php:207 #: inc/abilities/forms/update-form.php:210 -msgid "Expiry date (YYYY-MM-DD)." +msgid "Maximum number of entries allowed (0 = unlimited)." msgstr "" -#: inc/abilities/forms/create-form.php:217 -#: inc/abilities/forms/update-form.php:220 -msgid "Message shown when form is closed." +#: inc/abilities/forms/create-form.php:211 +#: inc/abilities/forms/update-form.php:214 +msgid "Expiry date (YYYY-MM-DD)." msgstr "" #: inc/abilities/forms/create-form.php:221 #: inc/abilities/forms/update-form.php:224 -msgid "Enable form scheduling." +msgid "Message shown when form is closed." msgstr "" #: inc/abilities/forms/create-form.php:225 #: inc/abilities/forms/update-form.php:228 +msgid "Enable form scheduling." +msgstr "" + +#: inc/abilities/forms/create-form.php:229 +#: inc/abilities/forms/update-form.php:232 msgid "Schedule start date (YYYY-MM-DD)." msgstr "" -#: inc/abilities/forms/create-form.php:239 -#: inc/abilities/forms/update-form.php:242 +#: inc/abilities/forms/create-form.php:243 +#: inc/abilities/forms/update-form.php:246 msgid "Custom CSS for the form." msgstr "" -#: inc/abilities/forms/create-form.php:245 +#: inc/abilities/forms/create-form.php:249 msgid "Form publish status." msgstr "" -#: inc/abilities/forms/create-form.php:290 +#: inc/abilities/forms/create-form.php:294 msgid "Form title is required." msgstr "" -#: inc/abilities/forms/create-form.php:298 +#: inc/abilities/forms/create-form.php:302 msgid "At least one form field is required." msgstr "" -#: inc/abilities/forms/create-form.php:326 +#: inc/abilities/forms/create-form.php:330 msgid "Failed to generate form fields from the provided data." msgstr "" -#: inc/abilities/forms/create-form.php:383 +#: inc/abilities/forms/create-form.php:387 msgid "Failed to create the form." msgstr "" @@ -761,7 +766,7 @@ msgstr "" #: inc/abilities/forms/get-form-stats.php:113 #: inc/abilities/forms/get-form.php:110 #: inc/abilities/forms/get-shortcode.php:95 -#: inc/abilities/forms/update-form.php:287 +#: inc/abilities/forms/update-form.php:291 msgid "Form not found." msgstr "" @@ -1341,7 +1346,7 @@ msgstr "" #: inc/blocks/sform/block.php:42 #: inc/page-builders/bricks/elements/form-widget.php:508 #: inc/page-builders/elementor/form-widget.php:827 -#: inc/post-types.php:1318 +#: inc/post-types.php:1324 #: assets/build/blocks.js:172 msgid "This form has been deleted or is unavailable." msgstr "" @@ -1725,7 +1730,7 @@ msgid "I consent to have this website store my information so they can respond t msgstr "" #: inc/fields/inlinebutton-markup.php:184 -#: inc/generate-form-markup.php:640 +#: inc/generate-form-markup.php:654 msgid "Please verify that you are not a robot." msgstr "" @@ -1844,8 +1849,8 @@ msgstr "" #: inc/form-restriction.php:183 #: inc/form-restriction.php:184 -#: inc/post-types.php:1224 -#: inc/post-types.php:1264 +#: inc/post-types.php:1230 +#: inc/post-types.php:1270 msgid "This form is not yet available. Check back after the scheduled start time." msgstr "" @@ -2001,75 +2006,75 @@ msgstr "" msgid "HCaptcha sitekey verification failed. Please contact your site administrator." msgstr "" -#: inc/frontend-assets.php:303 +#: inc/frontend-assets.php:310 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgctxt "Quill heading picker: default paragraph style" msgid "Normal" msgstr "" -#: inc/frontend-assets.php:304 +#: inc/frontend-assets.php:311 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 1" msgstr "" -#: inc/frontend-assets.php:305 +#: inc/frontend-assets.php:312 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 2" msgstr "" -#: inc/frontend-assets.php:306 +#: inc/frontend-assets.php:313 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 3" msgstr "" -#: inc/frontend-assets.php:307 +#: inc/frontend-assets.php:314 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 4" msgstr "" -#: inc/frontend-assets.php:308 +#: inc/frontend-assets.php:315 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 5" msgstr "" -#: inc/frontend-assets.php:309 +#: inc/frontend-assets.php:316 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 6" msgstr "" -#: inc/frontend-assets.php:310 +#: inc/frontend-assets.php:317 msgctxt "Quill link tooltip label" msgid "Visit URL:" msgstr "" -#: inc/frontend-assets.php:311 +#: inc/frontend-assets.php:318 msgctxt "Quill link tooltip label" msgid "Enter link:" msgstr "" -#: inc/frontend-assets.php:312 +#: inc/frontend-assets.php:319 msgctxt "Quill link tooltip action" msgid "Edit" msgstr "" -#: inc/frontend-assets.php:313 +#: inc/frontend-assets.php:320 msgctxt "Quill link tooltip action" msgid "Save" msgstr "" -#: inc/frontend-assets.php:314 +#: inc/frontend-assets.php:321 msgctxt "Quill link tooltip action" msgid "Remove" msgstr "" -#: inc/generate-form-markup.php:820 +#: inc/generate-form-markup.php:834 msgid "There was an error trying to submit your form. Please try again." msgstr "" @@ -2183,7 +2188,7 @@ msgstr "" #: inc/global-settings/global-settings-defaults.php:166 #: inc/migrator/importers/cf7-importer.php:168 #: inc/migrator/importers/ninja-importer.php:1152 -#: inc/post-types.php:998 +#: inc/post-types.php:1004 msgid "Admin Notification Email" msgstr "" @@ -2191,7 +2196,7 @@ msgstr "" #. translators: %s: {form_title} smart tag placeholder. #: inc/global-settings/global-settings-defaults.php:173 #: inc/global-settings/global-settings.php:588 -#: inc/post-types.php:1005 +#: inc/post-types.php:1011 #: assets/build/settings.js:172 #, php-format,js-format msgid "New Form Submission - %s" @@ -2224,14 +2229,14 @@ msgstr "" #: inc/global-settings/global-settings.php:530 #: inc/migrator/base-migrator.php:548 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 #: assets/build/formEditor.js:172 msgid "Thank you" msgstr "" #: inc/global-settings/global-settings.php:531 #: inc/migrator/base-migrator.php:549 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 msgid "Your form has been submitted successfully. We'll review your details and get back to you soon." msgstr "" @@ -2998,6 +3003,23 @@ msgstr "" msgid "Select the form that you wish to add here." msgstr "" +#: inc/page-builders/bricks/elements/payment-history-widget.php:51 +#: inc/page-builders/elementor/payment-history-widget.php:42 +#: inc/page-builders/elementor/payment-history-widget.php:91 +#: inc/payments/payment-history-shortcode.php:499 +msgid "Payment History" +msgstr "" + +#: inc/page-builders/bricks/elements/payment-history-widget.php:78 +#: inc/page-builders/elementor/payment-history-widget.php:98 +msgid "Items per page" +msgstr "" + +#: inc/page-builders/bricks/elements/payment-history-widget.php:86 +#: inc/page-builders/elementor/payment-history-widget.php:108 +msgid "Show Subscriptions" +msgstr "" + #: inc/page-builders/elementor/form-widget.php:318 #: inc/smart-tags.php:113 #: assets/build/blocks.js:172 @@ -3006,11 +3028,13 @@ msgid "Form Title" msgstr "" #: inc/page-builders/elementor/form-widget.php:320 +#: inc/page-builders/elementor/payment-history-widget.php:110 #: assets/build/blocks.js:172 msgid "Show" msgstr "" #: inc/page-builders/elementor/form-widget.php:321 +#: inc/page-builders/elementor/payment-history-widget.php:111 #: assets/build/blocks.js:172 msgid "Hide" msgstr "" @@ -4260,10 +4284,6 @@ msgstr "" msgid "Cancelled %s" msgstr "" -#: inc/payments/payment-history-shortcode.php:499 -msgid "Payment History" -msgstr "" - #. translators: %d: total number of transactions #: inc/payments/payment-history-shortcode.php:503 #, php-format @@ -4942,8 +4962,8 @@ msgstr "" msgid "Form updated." msgstr "" -#: inc/post-types.php:1225 -#: inc/post-types.php:1265 +#: inc/post-types.php:1231 +#: inc/post-types.php:1271 msgid "This form is closed. The submission period has ended." msgstr "" diff --git a/package-lock.json b/package-lock.json index 484dbe1ff..e345b36eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sureforms", - "version": "2.12.1", + "version": "2.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sureforms", - "version": "2.12.1", + "version": "2.12.2", "license": "GPL", "dependencies": { "@bsf/force-ui": "git+https://github.com/brainstormforce/bsf-admin-ui#v1.7.7", diff --git a/package.json b/package.json index f731b4add..e1526b866 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sureforms", - "version": "2.12.1", + "version": "2.12.2", "description": "A simple yet powerful way to add forms to your website.", "main": "index.js", "scripts": { diff --git a/readme.txt b/readme.txt index 371a5aafe..223d55e4e 100644 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: brainstormforce Tags: forms, contact form, form builder, survey, payment form Requires at least: 6.4 -Tested up to: 7.0 +Tested up to: 7.0.1 Requires PHP: 7.4 -Stable tag: 2.12.1 +Stable tag: 2.12.2 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -458,6 +458,11 @@ Yes. SureForms Business includes fully functional user registration forms and lo You can report security issues through our [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/). We collaborate with Patchstack to provide opportunities for researchers to report vulnerabilities. The Patchstack team will help validate, triage, and handle any reported security issues. == Changelog == += 2.12.2 - 16th July 2026 = +* New: Added a Payment History widget for Elementor and Bricks, bringing the same subscriptions and transaction view already available in the block editor to both page builders. +* New: Added a per-form "Disable SureForms Styling" option so you can turn off the plugin's default field styling and match your page design without fighting CSS. +* Fix: Custom CSS now applies correctly on the single form view. +* Fix: The SureForms Description panel now opens by default in the form editor so field descriptions are easier to find. = 2.12.1 - 8th July 2026 = * New: Added an AI-powered quick draft flow on the dashboard to generate forms faster, with built-in usage tracking. * Improvement: Email fields now enforce standard RFC 5321 length limits (a 64-character local part and 255-character domain) to prevent oversized submissions, with a filter to customize the limits. @@ -470,10 +475,6 @@ You can report security issues through our [Bug Bounty Program](https://brainsto * Fix: Forms with multiple Cloudflare Turnstile widgets now submit correctly instead of silently failing with a generic error. * Fix: Resolved an issue where the Stripe Payment Element failed to render on live accounts that have Bacs Direct Debit, Link, Cash App, or BNPL enabled. * Fix: Restored the form editor on WordPress 6.x sites running plugins that register older-style blocks such as ThirstyAffiliates, Ninja Forms, and Gravity Forms. -= 2.11.1 - 16th June 2026 = -* Fix: Phone field auto country detection always resolved to the United States. -* Fix: Corrected the Cloudflare Turnstile "Get Keys" link. -* Fix: This update addressed a security bug. Props to Yaswanth Reddy Sunkara for reporting it responsibly to our team. The full changelog is available [here](https://sureforms.com/whats-new/?utm_source=wordpress.org&utm_medium=whats_new). == Upgrade Notice == diff --git a/sureforms.php b/sureforms.php index f161d9429..af1f180be 100644 --- a/sureforms.php +++ b/sureforms.php @@ -7,7 +7,7 @@ * Requires PHP: 7.4 * Author: SureForms * Author URI: https://sureforms.com/ - * Version: 2.12.1 + * Version: 2.12.2 * License: GPLv2 or later * Text Domain: sureforms * @@ -25,7 +25,7 @@ define( 'SRFM_BASENAME', plugin_basename( SRFM_FILE ) ); define( 'SRFM_DIR', plugin_dir_path( SRFM_FILE ) ); define( 'SRFM_URL', plugins_url( '/', SRFM_FILE ) ); -define( 'SRFM_VER', '2.12.1' ); +define( 'SRFM_VER', '2.12.2' ); define( 'SRFM_SLUG', 'srfm' ); // ------ ADDITIONAL CONSTANTS ------- // define( 'SRFM_FORMS_POST_TYPE', 'sureforms_form' ); @@ -35,7 +35,7 @@ define( 'SRFM_AI_MIDDLEWARE', 'https://credits.startertemplates.com/sureforms/' ); define( 'SRFM_MIDDLEWARE_BASE_URL', 'https://api.sureforms.com/' ); define( 'SRFM_BILLING_PORTAL', 'https://billing.sureforms.com/' ); -define( 'SRFM_PRO_RECOMMENDED_VER', '2.12.1' ); +define( 'SRFM_PRO_RECOMMENDED_VER', '2.12.2' ); define( 'SRFM_SURETRIGGERS_INTEGRATION_BASE_URL', 'https://app.ottokit.com/' ); From 04a0520d52cebef97bd850287bb1335d65f01b42 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 16 Jul 2026 12:38:29 +0530 Subject: [PATCH 11/19] Update 2.12.2 changelog entries --- README.md | 3 +-- readme.txt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5f143407a..8d9f40d44 100644 --- a/README.md +++ b/README.md @@ -459,8 +459,7 @@ You can report security issues through our [Bug Bounty Program](https://brainsto ## Changelog ## ### 2.12.2 - 16th July 2026 ### -* New: Added a Payment History widget for Elementor and Bricks, bringing the same subscriptions and transaction view already available in the block editor to both page builders. -* New: Added a per-form "Disable SureForms Styling" option so you can turn off the plugin's default field styling and match your page design without fighting CSS. +* New: Added a Payment History widget for Elementor and Bricks. * Fix: Custom CSS now applies correctly on the single form view. * Fix: The SureForms Description panel now opens by default in the form editor so field descriptions are easier to find. ### 2.12.1 - 8th July 2026 ### diff --git a/readme.txt b/readme.txt index 223d55e4e..cef13c39e 100644 --- a/readme.txt +++ b/readme.txt @@ -459,8 +459,7 @@ You can report security issues through our [Bug Bounty Program](https://brainsto == Changelog == = 2.12.2 - 16th July 2026 = -* New: Added a Payment History widget for Elementor and Bricks, bringing the same subscriptions and transaction view already available in the block editor to both page builders. -* New: Added a per-form "Disable SureForms Styling" option so you can turn off the plugin's default field styling and match your page design without fighting CSS. +* New: Added a Payment History widget for Elementor and Bricks. * Fix: Custom CSS now applies correctly on the single form view. * Fix: The SureForms Description panel now opens by default in the form editor so field descriptions are easier to find. = 2.12.1 - 8th July 2026 = From 741dee82954f6e37a7ae9b330ca4ce09c79663fe Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Fri, 17 Jul 2026 10:18:27 +0530 Subject: [PATCH 12/19] fix(settings): render global settings after all filters register MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The global settings React app renders its sidebar navigation exactly once, via setTimeout( renderApp, 0 ). Pro (and third-party) bundles register their srfm.settings.navigation / page-content filters at module-eval time, but they are classic, parser-blocking footer scripts that load after the core settings bundle. setTimeout( 0 ) is not a safe barrier: it fires as soon as the HTML parser yields (e.g. while a later footer script is still downloading), which can be before those pro bundles execute. An early bundle (License tab) therefore always wins the race and renders, while a later one (Login/Registration) is intermittently dropped from the first — and only — paint. Render on DOMContentLoaded instead, which fires only after the document is fully parsed and every parser-blocking script has executed, guaranteeing all dependent filters are registered before the first render. Falls back to setTimeout( 0 ) if the document has already finished parsing. --- src/admin/settings/settings.js | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/admin/settings/settings.js b/src/admin/settings/settings.js index 1469c035d..bca0765a1 100644 --- a/src/admin/settings/settings.js +++ b/src/admin/settings/settings.js @@ -136,14 +136,27 @@ export default Settings; } } - // Footer scripts run synchronously and in dependency order, so pro's - // settings bundle runs AFTER this file. If we render immediately, pro's - // `addFilter( 'srfm.settings.navigation', ... )` (and other settings-page - // filters) hasn't run yet and the License tab — plus any other pro-injected - // tab/page-content — is missing from the first paint. + // Pro (and third-party) bundles register their settings-page filters + // (`addFilter( 'srfm.settings.navigation', ... )`, page content, etc.) at + // module-eval time. Those bundles are classic, parser-blocking footer + // scripts that load AFTER this one, so we must not render until every one + // of them has executed — otherwise pro-injected tabs are missing from the + // first (and only) paint. // - // Defer one event-loop tick so every dependent script (pro + any third - // party) has finished registering its filters before React calls - // applyFilters during the first render. - setTimeout( renderApp, 0 ); + // `setTimeout( 0 )` is NOT a safe barrier here: it fires as soon as the + // parser yields (e.g. while a later footer script is still downloading), + // which can be before those scripts run. That race is why an early bundle + // like the License tab always appears while a later one — Login/Registration + // — intermittently does not. + // + // `DOMContentLoaded` fires only after the document is fully parsed and all + // parser-blocking scripts have executed, so every dependent filter is + // guaranteed to be registered by then. Fall back to `setTimeout( 0 )` if the + // document has already finished parsing (defensive; not expected for a + // footer script). + if ( 'loading' === document.readyState ) { + document.addEventListener( 'DOMContentLoaded', renderApp ); + } else { + setTimeout( renderApp, 0 ); + } }() ); From 5b455ed3cc13d20111e1791610481d5d4ad74d9e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:33:11 +0000 Subject: [PATCH 13/19] chore: update i18n translations Auto-generated by /i18n command on PR #2946 --- languages/sureforms-de_DE.mo | Bin 323274 -> 323690 bytes languages/sureforms-de_DE.po | 140 ++++++++++++++++++++--------------- languages/sureforms-es_ES.mo | Bin 325080 -> 325489 bytes languages/sureforms-es_ES.po | 140 ++++++++++++++++++++--------------- languages/sureforms-fr_FR.mo | Bin 329336 -> 329759 bytes languages/sureforms-fr_FR.po | 140 ++++++++++++++++++++--------------- languages/sureforms-it_IT.mo | Bin 318658 -> 319046 bytes languages/sureforms-it_IT.po | 140 ++++++++++++++++++++--------------- languages/sureforms-nl_NL.mo | Bin 311920 -> 312339 bytes languages/sureforms-nl_NL.po | 140 ++++++++++++++++++++--------------- languages/sureforms-pl_PL.mo | Bin 319835 -> 320259 bytes languages/sureforms-pl_PL.po | 140 ++++++++++++++++++++--------------- languages/sureforms-pt_PT.mo | Bin 322057 -> 322461 bytes languages/sureforms-pt_PT.po | 140 ++++++++++++++++++++--------------- 14 files changed, 560 insertions(+), 420 deletions(-) diff --git a/languages/sureforms-de_DE.mo b/languages/sureforms-de_DE.mo index d6a1f53724c464a13dc51f092ffbb9ef9c139e30..1fdc9505efd59efd99d01b62db30320b37d61bd7 100644 GIT binary patch delta 70939 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 aPxXN(M$rAtVd*3K4C%4?$`2B+7y3UVp4EE* delta 70582 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 Ga{oU}w=stR diff --git a/languages/sureforms-de_DE.po b/languages/sureforms-de_DE.po index f120aef4b..1c7a90e8c 100644 --- a/languages/sureforms-de_DE.po +++ b/languages/sureforms-de_DE.po @@ -20,7 +20,7 @@ msgstr "" #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 -#: inc/page-builders/bricks/service-provider.php:55 +#: inc/page-builders/bricks/service-provider.php:56 #: inc/page-builders/elementor/form-widget.php:144 #: inc/page-builders/elementor/form-widget.php:301 #: inc/page-builders/elementor/service-provider.php:74 @@ -562,7 +562,7 @@ msgid "I consent to have this website store my submitted information so they can msgstr "Ich stimme zu, dass diese Website meine übermittelten Informationen speichert, damit sie auf meine Anfrage antworten können." #: inc/fields/inlinebutton-markup.php:184 -#: inc/generate-form-markup.php:640 +#: inc/generate-form-markup.php:654 msgid "Please verify that you are not a robot." msgstr "Bitte bestätigen Sie, dass Sie kein Roboter sind." @@ -614,7 +614,7 @@ msgstr "Formulardaten wurden nicht gefunden." msgid "Sorry, you are not allowed to view the form." msgstr "Entschuldigung, Sie dürfen das Formular nicht ansehen." -#: inc/generate-form-markup.php:820 +#: inc/generate-form-markup.php:834 msgid "There was an error trying to submit your form. Please try again." msgstr "Beim Absenden Ihres Formulars ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut." @@ -695,11 +695,13 @@ msgid "Form Title" msgstr "Formulartitel" #: inc/page-builders/elementor/form-widget.php:320 +#: inc/page-builders/elementor/payment-history-widget.php:110 #: assets/build/blocks.js:172 msgid "Show" msgstr "Zeigen" #: inc/page-builders/elementor/form-widget.php:321 +#: inc/page-builders/elementor/payment-history-widget.php:111 #: assets/build/blocks.js:172 msgid "Hide" msgstr "Verbergen" @@ -793,7 +795,7 @@ msgstr "Formular aktualisiert." #: inc/global-settings/global-settings-defaults.php:166 #: inc/migrator/importers/cf7-importer.php:168 #: inc/migrator/importers/ninja-importer.php:1152 -#: inc/post-types.php:998 +#: inc/post-types.php:1004 msgid "Admin Notification Email" msgstr "Admin-Benachrichtigungs-E-Mail" @@ -801,7 +803,7 @@ msgstr "Admin-Benachrichtigungs-E-Mail" #. translators: %s: {form_title} smart tag placeholder. #: inc/global-settings/global-settings-defaults.php:173 #: inc/global-settings/global-settings.php:588 -#: inc/post-types.php:1005 +#: inc/post-types.php:1011 #: assets/build/settings.js:172 #, php-format,js-format msgid "New Form Submission - %s" @@ -8327,7 +8329,7 @@ msgstr "Formular ändern" #: inc/blocks/sform/block.php:42 #: inc/page-builders/bricks/elements/form-widget.php:508 #: inc/page-builders/elementor/form-widget.php:827 -#: inc/post-types.php:1318 +#: inc/post-types.php:1324 #: assets/build/blocks.js:172 msgid "This form has been deleted or is unavailable." msgstr "Dieses Formular wurde gelöscht oder ist nicht verfügbar." @@ -9323,7 +9325,7 @@ msgstr "Zurück" #: inc/global-settings/global-settings.php:530 #: inc/migrator/base-migrator.php:548 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 #: assets/build/formEditor.js:172 msgid "Thank you" msgstr "Danke" @@ -13462,7 +13464,7 @@ msgstr "Bearbeiten Sie dieses Formular" #: inc/global-settings/global-settings.php:531 #: inc/migrator/base-migrator.php:549 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 msgid "Your form has been submitted successfully. We'll review your details and get back to you soon." msgstr "Ihr Formular wurde erfolgreich eingereicht. Wir werden Ihre Angaben prüfen und uns bald bei Ihnen melden." @@ -15605,23 +15607,23 @@ msgstr "Optionale Formularmetadaten einschließlich Bestätigung, Compliance und msgid "Message displayed after successful submission." msgstr "Nach erfolgreicher Übermittlung angezeigte Nachricht." -#: inc/abilities/forms/create-form.php:245 +#: inc/abilities/forms/create-form.php:249 msgid "Form publish status." msgstr "Formularveröffentlichungsstatus." -#: inc/abilities/forms/create-form.php:290 +#: inc/abilities/forms/create-form.php:294 msgid "Form title is required." msgstr "Der Formular-Titel ist erforderlich." -#: inc/abilities/forms/create-form.php:298 +#: inc/abilities/forms/create-form.php:302 msgid "At least one form field is required." msgstr "Mindestens ein Formularfeld ist erforderlich." -#: inc/abilities/forms/create-form.php:326 +#: inc/abilities/forms/create-form.php:330 msgid "Failed to generate form fields from the provided data." msgstr "Fehler beim Generieren der Formularfelder aus den bereitgestellten Daten." -#: inc/abilities/forms/create-form.php:383 +#: inc/abilities/forms/create-form.php:387 msgid "Failed to create the form." msgstr "Erstellen des Formulars fehlgeschlagen." @@ -15641,7 +15643,7 @@ msgstr "Wenn wahr, wird das Formular dauerhaft gelöscht. Andernfalls wird es in #: inc/abilities/forms/get-form-stats.php:113 #: inc/abilities/forms/get-form.php:110 #: inc/abilities/forms/get-shortcode.php:95 -#: inc/abilities/forms/update-form.php:287 +#: inc/abilities/forms/update-form.php:291 msgid "Form not found." msgstr "Formular nicht gefunden." @@ -15841,8 +15843,8 @@ msgstr "Textfeld" #: inc/form-restriction.php:183 #: inc/form-restriction.php:184 -#: inc/post-types.php:1224 -#: inc/post-types.php:1264 +#: inc/post-types.php:1230 +#: inc/post-types.php:1270 msgid "This form is not yet available. Check back after the scheduled start time." msgstr "Dieses Formular ist noch nicht verfügbar. Schauen Sie nach der geplanten Startzeit wieder vorbei." @@ -16126,8 +16128,8 @@ msgstr "Webhook kann nicht gelöscht werden." msgid "Unable to connect to Stripe." msgstr "Verbindung zu Stripe nicht möglich." -#: inc/post-types.php:1225 -#: inc/post-types.php:1265 +#: inc/post-types.php:1231 +#: inc/post-types.php:1271 msgid "This form is closed. The submission period has ended." msgstr "Dieses Formular ist geschlossen. Die Einreichungsfrist ist abgelaufen." @@ -16662,78 +16664,78 @@ msgstr "Textfarbe auf primären Hintergründen als Hex (z.B. #FFFFFF)." msgid "Spacing between fields." msgstr "Abstand zwischen den Feldern." -#: inc/abilities/forms/create-form.php:161 -#: inc/abilities/forms/update-form.php:164 +#: inc/abilities/forms/create-form.php:165 +#: inc/abilities/forms/update-form.php:168 msgid "Email notification settings for the first notification. Merges with existing." msgstr "E-Mail-Benachrichtigungseinstellungen für die erste Benachrichtigung. Wird mit bestehenden zusammengeführt." -#: inc/abilities/forms/create-form.php:165 -#: inc/abilities/forms/update-form.php:168 +#: inc/abilities/forms/create-form.php:169 +#: inc/abilities/forms/update-form.php:172 msgid "Enable or disable the notification." msgstr "Benachrichtigung aktivieren oder deaktivieren." -#: inc/abilities/forms/create-form.php:169 -#: inc/abilities/forms/update-form.php:172 +#: inc/abilities/forms/create-form.php:173 +#: inc/abilities/forms/update-form.php:176 msgid "Notification name." msgstr "Benachrichtigungsname." -#: inc/abilities/forms/create-form.php:173 -#: inc/abilities/forms/update-form.php:176 +#: inc/abilities/forms/create-form.php:177 +#: inc/abilities/forms/update-form.php:180 msgid "Recipient email. Supports smart tags like {admin_email}." msgstr "Empfänger-E-Mail. Unterstützt intelligente Tags wie {admin_email}." -#: inc/abilities/forms/create-form.php:178 -#: inc/abilities/forms/update-form.php:181 +#: inc/abilities/forms/create-form.php:182 +#: inc/abilities/forms/update-form.php:185 msgid "Sender name. Supports smart tags like {site_title}." msgstr "Absendername. Unterstützt intelligente Tags wie {site_title}." -#: inc/abilities/forms/create-form.php:185 -#: inc/abilities/forms/update-form.php:188 +#: inc/abilities/forms/create-form.php:189 +#: inc/abilities/forms/update-form.php:192 msgid "Email subject. Supports smart tags like {form_title}." msgstr "E-Mail-Betreff. Unterstützt intelligente Tags wie {form_title}." -#: inc/abilities/forms/create-form.php:189 -#: inc/abilities/forms/update-form.php:192 +#: inc/abilities/forms/create-form.php:193 +#: inc/abilities/forms/update-form.php:196 msgid "Email body. Supports smart tags like {all_data}." msgstr "E-Mail-Text. Unterstützt intelligente Tags wie {all_data}." -#: inc/abilities/forms/create-form.php:195 -#: inc/abilities/forms/update-form.php:198 +#: inc/abilities/forms/create-form.php:199 +#: inc/abilities/forms/update-form.php:202 msgid "Form submission restrictions and scheduling." msgstr "Einschränkungen und Zeitplanung für die Formularübermittlung." -#: inc/abilities/forms/create-form.php:199 -#: inc/abilities/forms/update-form.php:202 +#: inc/abilities/forms/create-form.php:203 +#: inc/abilities/forms/update-form.php:206 msgid "Enable entry limit restriction." msgstr "Einschränkung des Zugangslimits aktivieren." -#: inc/abilities/forms/create-form.php:203 -#: inc/abilities/forms/update-form.php:206 +#: inc/abilities/forms/create-form.php:207 +#: inc/abilities/forms/update-form.php:210 msgid "Maximum number of entries allowed (0 = unlimited)." msgstr "Maximale Anzahl erlaubter Einträge (0 = unbegrenzt)." -#: inc/abilities/forms/create-form.php:207 -#: inc/abilities/forms/update-form.php:210 +#: inc/abilities/forms/create-form.php:211 +#: inc/abilities/forms/update-form.php:214 msgid "Expiry date (YYYY-MM-DD)." msgstr "Ablaufdatum (JJJJ-MM-TT)." -#: inc/abilities/forms/create-form.php:217 -#: inc/abilities/forms/update-form.php:220 +#: inc/abilities/forms/create-form.php:221 +#: inc/abilities/forms/update-form.php:224 msgid "Message shown when form is closed." msgstr "Nachricht wird angezeigt, wenn das Formular geschlossen wird." -#: inc/abilities/forms/create-form.php:221 -#: inc/abilities/forms/update-form.php:224 +#: inc/abilities/forms/create-form.php:225 +#: inc/abilities/forms/update-form.php:228 msgid "Enable form scheduling." msgstr "Formularplanung aktivieren." -#: inc/abilities/forms/create-form.php:225 -#: inc/abilities/forms/update-form.php:228 +#: inc/abilities/forms/create-form.php:229 +#: inc/abilities/forms/update-form.php:232 msgid "Schedule start date (YYYY-MM-DD)." msgstr "Startdatum des Zeitplans (JJJJ-MM-TT)." -#: inc/abilities/forms/create-form.php:239 -#: inc/abilities/forms/update-form.php:242 +#: inc/abilities/forms/create-form.php:243 +#: inc/abilities/forms/update-form.php:246 msgid "Custom CSS for the form." msgstr "Benutzerdefiniertes CSS für das Formular." @@ -17333,37 +17335,37 @@ msgstr "Wählen Sie dies, um ein Quiz mit bewerteten Fragen und benoteten Ergebn msgid "Survey Reports" msgstr "Umfrageberichte" -#: inc/frontend-assets.php:304 +#: inc/frontend-assets.php:311 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 1" msgstr "Überschrift 1" -#: inc/frontend-assets.php:305 +#: inc/frontend-assets.php:312 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 2" msgstr "Überschrift 2" -#: inc/frontend-assets.php:306 +#: inc/frontend-assets.php:313 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 3" msgstr "Überschrift 3" -#: inc/frontend-assets.php:307 +#: inc/frontend-assets.php:314 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 4" msgstr "Überschrift 4" -#: inc/frontend-assets.php:308 +#: inc/frontend-assets.php:315 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 5" msgstr "Überschrift 5" -#: inc/frontend-assets.php:309 +#: inc/frontend-assets.php:316 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 6" @@ -17510,6 +17512,9 @@ msgstr "Nächste: %s" msgid "Cancelled %s" msgstr "Storniert %s" +#: inc/page-builders/bricks/elements/payment-history-widget.php:51 +#: inc/page-builders/elementor/payment-history-widget.php:42 +#: inc/page-builders/elementor/payment-history-widget.php:91 #: inc/payments/payment-history-shortcode.php:499 msgid "Payment History" msgstr "Zahlungsverlauf" @@ -18829,6 +18834,21 @@ msgstr "Wählen Sie genau aus, welche Felder als Spalten erscheinen." msgid "Restrict who can view entries by user role." msgstr "Beschränken Sie, wer Einträge nach Benutzerrolle anzeigen kann." +#: inc/abilities/forms/create-form.php:159 +#: inc/abilities/forms/update-form.php:162 +msgid "When true, SureForms does not enqueue its default frontend styles for this form, so the active theme styling applies instead." +msgstr "Wenn wahr, stellt SureForms seine Standard-Frontend-Stile für dieses Formular nicht in die Warteschlange, sodass stattdessen das aktive Theme-Styling angewendet wird." + +#: inc/page-builders/bricks/elements/payment-history-widget.php:78 +#: inc/page-builders/elementor/payment-history-widget.php:98 +msgid "Items per page" +msgstr "Elemente pro Seite" + +#: inc/page-builders/bricks/elements/payment-history-widget.php:86 +#: inc/page-builders/elementor/payment-history-widget.php:108 +msgid "Show Subscriptions" +msgstr "Abonnements anzeigen" + #: inc/post-types.php:226 msgctxt "post type general name" msgid "Forms" @@ -19256,34 +19276,34 @@ msgctxt "block keyword" msgid "color" msgstr "Farbe" -#: inc/frontend-assets.php:303 +#: inc/frontend-assets.php:310 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgctxt "Quill heading picker: default paragraph style" msgid "Normal" msgstr "Normal" -#: inc/frontend-assets.php:310 +#: inc/frontend-assets.php:317 msgctxt "Quill link tooltip label" msgid "Visit URL:" msgstr "URL besuchen:" -#: inc/frontend-assets.php:311 +#: inc/frontend-assets.php:318 msgctxt "Quill link tooltip label" msgid "Enter link:" msgstr "Link eingeben:" -#: inc/frontend-assets.php:312 +#: inc/frontend-assets.php:319 msgctxt "Quill link tooltip action" msgid "Edit" msgstr "Bearbeiten" -#: inc/frontend-assets.php:313 +#: inc/frontend-assets.php:320 msgctxt "Quill link tooltip action" msgid "Save" msgstr "Speichern" -#: inc/frontend-assets.php:314 +#: inc/frontend-assets.php:321 msgctxt "Quill link tooltip action" msgid "Remove" msgstr "Entfernen" diff --git a/languages/sureforms-es_ES.mo b/languages/sureforms-es_ES.mo index 0bfb47bcf3ae4512b2080739e723dba32748fb53..abaa557026de97efc6291ad2b0f4cdb89cf9c178 100644 GIT binary patch delta 70938 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 diff --git a/languages/sureforms-es_ES.po b/languages/sureforms-es_ES.po index 4688b4d9c..7e09de8fb 100644 --- a/languages/sureforms-es_ES.po +++ b/languages/sureforms-es_ES.po @@ -20,7 +20,7 @@ msgstr "" #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 -#: inc/page-builders/bricks/service-provider.php:55 +#: inc/page-builders/bricks/service-provider.php:56 #: inc/page-builders/elementor/form-widget.php:144 #: inc/page-builders/elementor/form-widget.php:301 #: inc/page-builders/elementor/service-provider.php:74 @@ -562,7 +562,7 @@ msgid "I consent to have this website store my submitted information so they can msgstr "Doy mi consentimiento para que este sitio web almacene la información que he enviado para que puedan responder a mi consulta." #: inc/fields/inlinebutton-markup.php:184 -#: inc/generate-form-markup.php:640 +#: inc/generate-form-markup.php:654 msgid "Please verify that you are not a robot." msgstr "Por favor, verifica que no eres un robot." @@ -614,7 +614,7 @@ msgstr "Los datos del formulario no se encuentran." msgid "Sorry, you are not allowed to view the form." msgstr "Lo siento, no tienes permiso para ver el formulario." -#: inc/generate-form-markup.php:820 +#: inc/generate-form-markup.php:834 msgid "There was an error trying to submit your form. Please try again." msgstr "Hubo un error al intentar enviar su formulario. Por favor, inténtelo de nuevo." @@ -695,11 +695,13 @@ msgid "Form Title" msgstr "Título del formulario" #: inc/page-builders/elementor/form-widget.php:320 +#: inc/page-builders/elementor/payment-history-widget.php:110 #: assets/build/blocks.js:172 msgid "Show" msgstr "Mostrar" #: inc/page-builders/elementor/form-widget.php:321 +#: inc/page-builders/elementor/payment-history-widget.php:111 #: assets/build/blocks.js:172 msgid "Hide" msgstr "Ocultar" @@ -793,7 +795,7 @@ msgstr "Formulario actualizado." #: inc/global-settings/global-settings-defaults.php:166 #: inc/migrator/importers/cf7-importer.php:168 #: inc/migrator/importers/ninja-importer.php:1152 -#: inc/post-types.php:998 +#: inc/post-types.php:1004 msgid "Admin Notification Email" msgstr "Correo Electrónico de Notificación del Administrador" @@ -801,7 +803,7 @@ msgstr "Correo Electrónico de Notificación del Administrador" #. translators: %s: {form_title} smart tag placeholder. #: inc/global-settings/global-settings-defaults.php:173 #: inc/global-settings/global-settings.php:588 -#: inc/post-types.php:1005 +#: inc/post-types.php:1011 #: assets/build/settings.js:172 #, php-format,js-format msgid "New Form Submission - %s" @@ -8327,7 +8329,7 @@ msgstr "Cambiar formulario" #: inc/blocks/sform/block.php:42 #: inc/page-builders/bricks/elements/form-widget.php:508 #: inc/page-builders/elementor/form-widget.php:827 -#: inc/post-types.php:1318 +#: inc/post-types.php:1324 #: assets/build/blocks.js:172 msgid "This form has been deleted or is unavailable." msgstr "Este formulario ha sido eliminado o no está disponible." @@ -9323,7 +9325,7 @@ msgstr "Anterior" #: inc/global-settings/global-settings.php:530 #: inc/migrator/base-migrator.php:548 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 #: assets/build/formEditor.js:172 msgid "Thank you" msgstr "Gracias" @@ -13462,7 +13464,7 @@ msgstr "Edita este formulario" #: inc/global-settings/global-settings.php:531 #: inc/migrator/base-migrator.php:549 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 msgid "Your form has been submitted successfully. We'll review your details and get back to you soon." msgstr "Su formulario ha sido enviado con éxito. Revisaremos sus detalles y nos pondremos en contacto con usted pronto." @@ -15605,23 +15607,23 @@ msgstr "Metadatos opcionales del formulario, incluidos los ajustes de confirmaci msgid "Message displayed after successful submission." msgstr "Mensaje mostrado después de un envío exitoso." -#: inc/abilities/forms/create-form.php:245 +#: inc/abilities/forms/create-form.php:249 msgid "Form publish status." msgstr "Estado de publicación del formulario." -#: inc/abilities/forms/create-form.php:290 +#: inc/abilities/forms/create-form.php:294 msgid "Form title is required." msgstr "El título del formulario es obligatorio." -#: inc/abilities/forms/create-form.php:298 +#: inc/abilities/forms/create-form.php:302 msgid "At least one form field is required." msgstr "Se requiere al menos un campo de formulario." -#: inc/abilities/forms/create-form.php:326 +#: inc/abilities/forms/create-form.php:330 msgid "Failed to generate form fields from the provided data." msgstr "Error al generar los campos del formulario a partir de los datos proporcionados." -#: inc/abilities/forms/create-form.php:383 +#: inc/abilities/forms/create-form.php:387 msgid "Failed to create the form." msgstr "Error al crear el formulario." @@ -15641,7 +15643,7 @@ msgstr "Si es verdadero, elimina permanentemente el formulario. De lo contrario, #: inc/abilities/forms/get-form-stats.php:113 #: inc/abilities/forms/get-form.php:110 #: inc/abilities/forms/get-shortcode.php:95 -#: inc/abilities/forms/update-form.php:287 +#: inc/abilities/forms/update-form.php:291 msgid "Form not found." msgstr "Formulario no encontrado." @@ -15841,8 +15843,8 @@ msgstr "Campo de texto" #: inc/form-restriction.php:183 #: inc/form-restriction.php:184 -#: inc/post-types.php:1224 -#: inc/post-types.php:1264 +#: inc/post-types.php:1230 +#: inc/post-types.php:1270 msgid "This form is not yet available. Check back after the scheduled start time." msgstr "Este formulario aún no está disponible. Vuelve a consultar después de la hora de inicio programada." @@ -16126,8 +16128,8 @@ msgstr "No se puede eliminar el webhook." msgid "Unable to connect to Stripe." msgstr "No se puede conectar a Stripe." -#: inc/post-types.php:1225 -#: inc/post-types.php:1265 +#: inc/post-types.php:1231 +#: inc/post-types.php:1271 msgid "This form is closed. The submission period has ended." msgstr "Este formulario está cerrado. El período de presentación ha terminado." @@ -16662,78 +16664,78 @@ msgstr "Color de texto en fondos primarios como hex (por ejemplo, #FFFFFF)." msgid "Spacing between fields." msgstr "Espaciado entre campos." -#: inc/abilities/forms/create-form.php:161 -#: inc/abilities/forms/update-form.php:164 +#: inc/abilities/forms/create-form.php:165 +#: inc/abilities/forms/update-form.php:168 msgid "Email notification settings for the first notification. Merges with existing." msgstr "Configuración de notificaciones por correo electrónico para la primera notificación. Se fusiona con las existentes." -#: inc/abilities/forms/create-form.php:165 -#: inc/abilities/forms/update-form.php:168 +#: inc/abilities/forms/create-form.php:169 +#: inc/abilities/forms/update-form.php:172 msgid "Enable or disable the notification." msgstr "Habilitar o deshabilitar la notificación." -#: inc/abilities/forms/create-form.php:169 -#: inc/abilities/forms/update-form.php:172 +#: inc/abilities/forms/create-form.php:173 +#: inc/abilities/forms/update-form.php:176 msgid "Notification name." msgstr "Nombre de la notificación." -#: inc/abilities/forms/create-form.php:173 -#: inc/abilities/forms/update-form.php:176 +#: inc/abilities/forms/create-form.php:177 +#: inc/abilities/forms/update-form.php:180 msgid "Recipient email. Supports smart tags like {admin_email}." msgstr "Correo electrónico del destinatario. Admite etiquetas inteligentes como {admin_email}." -#: inc/abilities/forms/create-form.php:178 -#: inc/abilities/forms/update-form.php:181 +#: inc/abilities/forms/create-form.php:182 +#: inc/abilities/forms/update-form.php:185 msgid "Sender name. Supports smart tags like {site_title}." msgstr "Nombre del remitente. Admite etiquetas inteligentes como {site_title}." -#: inc/abilities/forms/create-form.php:185 -#: inc/abilities/forms/update-form.php:188 +#: inc/abilities/forms/create-form.php:189 +#: inc/abilities/forms/update-form.php:192 msgid "Email subject. Supports smart tags like {form_title}." msgstr "Asunto del correo electrónico. Admite etiquetas inteligentes como {form_title}." -#: inc/abilities/forms/create-form.php:189 -#: inc/abilities/forms/update-form.php:192 +#: inc/abilities/forms/create-form.php:193 +#: inc/abilities/forms/update-form.php:196 msgid "Email body. Supports smart tags like {all_data}." msgstr "Cuerpo del correo electrónico. Admite etiquetas inteligentes como {all_data}." -#: inc/abilities/forms/create-form.php:195 -#: inc/abilities/forms/update-form.php:198 +#: inc/abilities/forms/create-form.php:199 +#: inc/abilities/forms/update-form.php:202 msgid "Form submission restrictions and scheduling." msgstr "Restricciones y programación de envío de formularios." -#: inc/abilities/forms/create-form.php:199 -#: inc/abilities/forms/update-form.php:202 +#: inc/abilities/forms/create-form.php:203 +#: inc/abilities/forms/update-form.php:206 msgid "Enable entry limit restriction." msgstr "Habilitar restricción de límite de entrada." -#: inc/abilities/forms/create-form.php:203 -#: inc/abilities/forms/update-form.php:206 +#: inc/abilities/forms/create-form.php:207 +#: inc/abilities/forms/update-form.php:210 msgid "Maximum number of entries allowed (0 = unlimited)." msgstr "Número máximo de entradas permitidas (0 = ilimitado)." -#: inc/abilities/forms/create-form.php:207 -#: inc/abilities/forms/update-form.php:210 +#: inc/abilities/forms/create-form.php:211 +#: inc/abilities/forms/update-form.php:214 msgid "Expiry date (YYYY-MM-DD)." msgstr "Fecha de caducidad (AAAA-MM-DD)." -#: inc/abilities/forms/create-form.php:217 -#: inc/abilities/forms/update-form.php:220 +#: inc/abilities/forms/create-form.php:221 +#: inc/abilities/forms/update-form.php:224 msgid "Message shown when form is closed." msgstr "Mensaje mostrado cuando el formulario se cierra." -#: inc/abilities/forms/create-form.php:221 -#: inc/abilities/forms/update-form.php:224 +#: inc/abilities/forms/create-form.php:225 +#: inc/abilities/forms/update-form.php:228 msgid "Enable form scheduling." msgstr "Habilitar la programación de formularios." -#: inc/abilities/forms/create-form.php:225 -#: inc/abilities/forms/update-form.php:228 +#: inc/abilities/forms/create-form.php:229 +#: inc/abilities/forms/update-form.php:232 msgid "Schedule start date (YYYY-MM-DD)." msgstr "Fecha de inicio programada (AAAA-MM-DD)." -#: inc/abilities/forms/create-form.php:239 -#: inc/abilities/forms/update-form.php:242 +#: inc/abilities/forms/create-form.php:243 +#: inc/abilities/forms/update-form.php:246 msgid "Custom CSS for the form." msgstr "CSS personalizado para el formulario." @@ -17333,37 +17335,37 @@ msgstr "Selecciona esto para crear un cuestionario con preguntas puntuadas y res msgid "Survey Reports" msgstr "Informes de encuestas" -#: inc/frontend-assets.php:304 +#: inc/frontend-assets.php:311 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 1" msgstr "Encabezado 1" -#: inc/frontend-assets.php:305 +#: inc/frontend-assets.php:312 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 2" msgstr "Encabezado 2" -#: inc/frontend-assets.php:306 +#: inc/frontend-assets.php:313 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 3" msgstr "Encabezado 3" -#: inc/frontend-assets.php:307 +#: inc/frontend-assets.php:314 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 4" msgstr "Encabezado 4" -#: inc/frontend-assets.php:308 +#: inc/frontend-assets.php:315 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 5" msgstr "Encabezado 5" -#: inc/frontend-assets.php:309 +#: inc/frontend-assets.php:316 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 6" @@ -17510,6 +17512,9 @@ msgstr "Siguiente: %s" msgid "Cancelled %s" msgstr "Cancelado %s" +#: inc/page-builders/bricks/elements/payment-history-widget.php:51 +#: inc/page-builders/elementor/payment-history-widget.php:42 +#: inc/page-builders/elementor/payment-history-widget.php:91 #: inc/payments/payment-history-shortcode.php:499 msgid "Payment History" msgstr "Historial de pagos" @@ -18829,6 +18834,21 @@ msgstr "Elige exactamente qué campos aparecen como columnas." msgid "Restrict who can view entries by user role." msgstr "Restringe quién puede ver las entradas por rol de usuario." +#: inc/abilities/forms/create-form.php:159 +#: inc/abilities/forms/update-form.php:162 +msgid "When true, SureForms does not enqueue its default frontend styles for this form, so the active theme styling applies instead." +msgstr "Cuando es verdadero, SureForms no pone en cola sus estilos predeterminados de frontend para este formulario, por lo que se aplica el estilo del tema activo." + +#: inc/page-builders/bricks/elements/payment-history-widget.php:78 +#: inc/page-builders/elementor/payment-history-widget.php:98 +msgid "Items per page" +msgstr "Elementos por página" + +#: inc/page-builders/bricks/elements/payment-history-widget.php:86 +#: inc/page-builders/elementor/payment-history-widget.php:108 +msgid "Show Subscriptions" +msgstr "Mostrar suscripciones" + #: inc/post-types.php:226 msgctxt "post type general name" msgid "Forms" @@ -19253,34 +19273,34 @@ msgctxt "block keyword" msgid "color" msgstr "color" -#: inc/frontend-assets.php:303 +#: inc/frontend-assets.php:310 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgctxt "Quill heading picker: default paragraph style" msgid "Normal" msgstr "Normal" -#: inc/frontend-assets.php:310 +#: inc/frontend-assets.php:317 msgctxt "Quill link tooltip label" msgid "Visit URL:" msgstr "Visitar URL:" -#: inc/frontend-assets.php:311 +#: inc/frontend-assets.php:318 msgctxt "Quill link tooltip label" msgid "Enter link:" msgstr "Introduzca el enlace:" -#: inc/frontend-assets.php:312 +#: inc/frontend-assets.php:319 msgctxt "Quill link tooltip action" msgid "Edit" msgstr "Editar" -#: inc/frontend-assets.php:313 +#: inc/frontend-assets.php:320 msgctxt "Quill link tooltip action" msgid "Save" msgstr "Guardar" -#: inc/frontend-assets.php:314 +#: inc/frontend-assets.php:321 msgctxt "Quill link tooltip action" msgid "Remove" msgstr "Eliminar" diff --git a/languages/sureforms-fr_FR.mo b/languages/sureforms-fr_FR.mo index b549cd60fa009ba7fea9ed139a01fa1ea705b922..cd76759d598ff8c9e7627590c1e09e0e45681393 100644 GIT binary patch delta 70945 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 delta 70582 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 I+G~aW4~AwhwEzGB diff --git a/languages/sureforms-fr_FR.po b/languages/sureforms-fr_FR.po index 8bb82ba7f..40747b84d 100644 --- a/languages/sureforms-fr_FR.po +++ b/languages/sureforms-fr_FR.po @@ -20,7 +20,7 @@ msgstr "" #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 -#: inc/page-builders/bricks/service-provider.php:55 +#: inc/page-builders/bricks/service-provider.php:56 #: inc/page-builders/elementor/form-widget.php:144 #: inc/page-builders/elementor/form-widget.php:301 #: inc/page-builders/elementor/service-provider.php:74 @@ -551,7 +551,7 @@ msgid "I consent to have this website store my submitted information so they can msgstr "Je consens à ce que ce site web stocke les informations que j'ai soumises afin qu'ils puissent répondre à ma demande." #: inc/fields/inlinebutton-markup.php:184 -#: inc/generate-form-markup.php:640 +#: inc/generate-form-markup.php:654 msgid "Please verify that you are not a robot." msgstr "Veuillez vérifier que vous n'êtes pas un robot." @@ -603,7 +603,7 @@ msgstr "Les données du formulaire sont introuvables." msgid "Sorry, you are not allowed to view the form." msgstr "Désolé, vous n'êtes pas autorisé à voir le formulaire." -#: inc/generate-form-markup.php:820 +#: inc/generate-form-markup.php:834 msgid "There was an error trying to submit your form. Please try again." msgstr "Une erreur s'est produite lors de la soumission de votre formulaire. Veuillez réessayer." @@ -684,11 +684,13 @@ msgid "Form Title" msgstr "Titre du formulaire" #: inc/page-builders/elementor/form-widget.php:320 +#: inc/page-builders/elementor/payment-history-widget.php:110 #: assets/build/blocks.js:172 msgid "Show" msgstr "Afficher" #: inc/page-builders/elementor/form-widget.php:321 +#: inc/page-builders/elementor/payment-history-widget.php:111 #: assets/build/blocks.js:172 msgid "Hide" msgstr "Cacher" @@ -782,7 +784,7 @@ msgstr "Formulaire mis à jour." #: inc/global-settings/global-settings-defaults.php:166 #: inc/migrator/importers/cf7-importer.php:168 #: inc/migrator/importers/ninja-importer.php:1152 -#: inc/post-types.php:998 +#: inc/post-types.php:1004 msgid "Admin Notification Email" msgstr "Email de notification d'administration" @@ -790,7 +792,7 @@ msgstr "Email de notification d'administration" #. translators: %s: {form_title} smart tag placeholder. #: inc/global-settings/global-settings-defaults.php:173 #: inc/global-settings/global-settings.php:588 -#: inc/post-types.php:1005 +#: inc/post-types.php:1011 #: assets/build/settings.js:172 #, php-format,js-format msgid "New Form Submission - %s" @@ -8316,7 +8318,7 @@ msgstr "Changer de formulaire" #: inc/blocks/sform/block.php:42 #: inc/page-builders/bricks/elements/form-widget.php:508 #: inc/page-builders/elementor/form-widget.php:827 -#: inc/post-types.php:1318 +#: inc/post-types.php:1324 #: assets/build/blocks.js:172 msgid "This form has been deleted or is unavailable." msgstr "Ce formulaire a été supprimé ou est indisponible." @@ -9312,7 +9314,7 @@ msgstr "Précédent" #: inc/global-settings/global-settings.php:530 #: inc/migrator/base-migrator.php:548 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 #: assets/build/formEditor.js:172 msgid "Thank you" msgstr "Merci" @@ -13465,7 +13467,7 @@ msgstr "Modifier ce formulaire" #: inc/global-settings/global-settings.php:531 #: inc/migrator/base-migrator.php:549 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 msgid "Your form has been submitted successfully. We'll review your details and get back to you soon." msgstr "Votre formulaire a été soumis avec succès. Nous examinerons vos informations et vous répondrons bientôt." @@ -15608,23 +15610,23 @@ msgstr "Métadonnées de formulaire optionnelles, y compris les paramètres de c msgid "Message displayed after successful submission." msgstr "Message affichée après soumission réussie." -#: inc/abilities/forms/create-form.php:245 +#: inc/abilities/forms/create-form.php:249 msgid "Form publish status." msgstr "Statut de publication du formulaire." -#: inc/abilities/forms/create-form.php:290 +#: inc/abilities/forms/create-form.php:294 msgid "Form title is required." msgstr "Le titre du formulaire est requis." -#: inc/abilities/forms/create-form.php:298 +#: inc/abilities/forms/create-form.php:302 msgid "At least one form field is required." msgstr "Au moins un champ de formulaire est requis." -#: inc/abilities/forms/create-form.php:326 +#: inc/abilities/forms/create-form.php:330 msgid "Failed to generate form fields from the provided data." msgstr "Échec de la génération des champs de formulaire à partir des données fournies." -#: inc/abilities/forms/create-form.php:383 +#: inc/abilities/forms/create-form.php:387 msgid "Failed to create the form." msgstr "Échec de la création du formulaire." @@ -15644,7 +15646,7 @@ msgstr "Si vrai, supprime définitivement le formulaire. Sinon, le déplace dans #: inc/abilities/forms/get-form-stats.php:113 #: inc/abilities/forms/get-form.php:110 #: inc/abilities/forms/get-shortcode.php:95 -#: inc/abilities/forms/update-form.php:287 +#: inc/abilities/forms/update-form.php:291 msgid "Form not found." msgstr "Formulaire non trouvé." @@ -15844,8 +15846,8 @@ msgstr "Champ de texte" #: inc/form-restriction.php:183 #: inc/form-restriction.php:184 -#: inc/post-types.php:1224 -#: inc/post-types.php:1264 +#: inc/post-types.php:1230 +#: inc/post-types.php:1270 msgid "This form is not yet available. Check back after the scheduled start time." msgstr "Ce formulaire n'est pas encore disponible. Revenez après l'heure de début prévue." @@ -16129,8 +16131,8 @@ msgstr "Impossible de supprimer le webhook." msgid "Unable to connect to Stripe." msgstr "Impossible de se connecter à Stripe." -#: inc/post-types.php:1225 -#: inc/post-types.php:1265 +#: inc/post-types.php:1231 +#: inc/post-types.php:1271 msgid "This form is closed. The submission period has ended." msgstr "Ce formulaire est fermé. La période de soumission est terminée." @@ -16665,78 +16667,78 @@ msgstr "Couleur du texte sur les arrière-plans principaux en hexadécimal (par msgid "Spacing between fields." msgstr "Espacement entre les champs." -#: inc/abilities/forms/create-form.php:161 -#: inc/abilities/forms/update-form.php:164 +#: inc/abilities/forms/create-form.php:165 +#: inc/abilities/forms/update-form.php:168 msgid "Email notification settings for the first notification. Merges with existing." msgstr "Paramètres de notification par e-mail pour la première notification. Fusionne avec l'existant." -#: inc/abilities/forms/create-form.php:165 -#: inc/abilities/forms/update-form.php:168 +#: inc/abilities/forms/create-form.php:169 +#: inc/abilities/forms/update-form.php:172 msgid "Enable or disable the notification." msgstr "Activer ou désactiver la notification." -#: inc/abilities/forms/create-form.php:169 -#: inc/abilities/forms/update-form.php:172 +#: inc/abilities/forms/create-form.php:173 +#: inc/abilities/forms/update-form.php:176 msgid "Notification name." msgstr "Nom de la notification." -#: inc/abilities/forms/create-form.php:173 -#: inc/abilities/forms/update-form.php:176 +#: inc/abilities/forms/create-form.php:177 +#: inc/abilities/forms/update-form.php:180 msgid "Recipient email. Supports smart tags like {admin_email}." msgstr "Email du destinataire. Prend en charge les balises intelligentes comme {admin_email}." -#: inc/abilities/forms/create-form.php:178 -#: inc/abilities/forms/update-form.php:181 +#: inc/abilities/forms/create-form.php:182 +#: inc/abilities/forms/update-form.php:185 msgid "Sender name. Supports smart tags like {site_title}." msgstr "Nom de l'expéditeur. Prend en charge les balises intelligentes comme {site_title}." -#: inc/abilities/forms/create-form.php:185 -#: inc/abilities/forms/update-form.php:188 +#: inc/abilities/forms/create-form.php:189 +#: inc/abilities/forms/update-form.php:192 msgid "Email subject. Supports smart tags like {form_title}." msgstr "Objet de l'email. Prend en charge les balises intelligentes comme {form_title}." -#: inc/abilities/forms/create-form.php:189 -#: inc/abilities/forms/update-form.php:192 +#: inc/abilities/forms/create-form.php:193 +#: inc/abilities/forms/update-form.php:196 msgid "Email body. Supports smart tags like {all_data}." msgstr "Corps de l'email. Prend en charge les balises intelligentes comme {all_data}." -#: inc/abilities/forms/create-form.php:195 -#: inc/abilities/forms/update-form.php:198 +#: inc/abilities/forms/create-form.php:199 +#: inc/abilities/forms/update-form.php:202 msgid "Form submission restrictions and scheduling." msgstr "Restrictions et planification de la soumission de formulaire." -#: inc/abilities/forms/create-form.php:199 -#: inc/abilities/forms/update-form.php:202 +#: inc/abilities/forms/create-form.php:203 +#: inc/abilities/forms/update-form.php:206 msgid "Enable entry limit restriction." msgstr "Activer la restriction de limite d'entrée." -#: inc/abilities/forms/create-form.php:203 -#: inc/abilities/forms/update-form.php:206 +#: inc/abilities/forms/create-form.php:207 +#: inc/abilities/forms/update-form.php:210 msgid "Maximum number of entries allowed (0 = unlimited)." msgstr "Nombre maximum d'entrées autorisées (0 = illimité)." -#: inc/abilities/forms/create-form.php:207 -#: inc/abilities/forms/update-form.php:210 +#: inc/abilities/forms/create-form.php:211 +#: inc/abilities/forms/update-form.php:214 msgid "Expiry date (YYYY-MM-DD)." msgstr "Date d'expiration (AAAA-MM-JJ)." -#: inc/abilities/forms/create-form.php:217 -#: inc/abilities/forms/update-form.php:220 +#: inc/abilities/forms/create-form.php:221 +#: inc/abilities/forms/update-form.php:224 msgid "Message shown when form is closed." msgstr "Message affiché lorsque le formulaire est fermé." -#: inc/abilities/forms/create-form.php:221 -#: inc/abilities/forms/update-form.php:224 +#: inc/abilities/forms/create-form.php:225 +#: inc/abilities/forms/update-form.php:228 msgid "Enable form scheduling." msgstr "Activer la planification des formulaires." -#: inc/abilities/forms/create-form.php:225 -#: inc/abilities/forms/update-form.php:228 +#: inc/abilities/forms/create-form.php:229 +#: inc/abilities/forms/update-form.php:232 msgid "Schedule start date (YYYY-MM-DD)." msgstr "Date de début de l'horaire (AAAA-MM-JJ)." -#: inc/abilities/forms/create-form.php:239 -#: inc/abilities/forms/update-form.php:242 +#: inc/abilities/forms/create-form.php:243 +#: inc/abilities/forms/update-form.php:246 msgid "Custom CSS for the form." msgstr "CSS personnalisé pour le formulaire." @@ -17336,37 +17338,37 @@ msgstr "Sélectionnez ceci pour créer un quiz avec des questions notées et des msgid "Survey Reports" msgstr "Rapports d'enquête" -#: inc/frontend-assets.php:304 +#: inc/frontend-assets.php:311 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 1" msgstr "Titre 1" -#: inc/frontend-assets.php:305 +#: inc/frontend-assets.php:312 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 2" msgstr "Titre 2" -#: inc/frontend-assets.php:306 +#: inc/frontend-assets.php:313 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 3" msgstr "Titre 3" -#: inc/frontend-assets.php:307 +#: inc/frontend-assets.php:314 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 4" msgstr "Titre 4" -#: inc/frontend-assets.php:308 +#: inc/frontend-assets.php:315 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 5" msgstr "Titre 5" -#: inc/frontend-assets.php:309 +#: inc/frontend-assets.php:316 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 6" @@ -17513,6 +17515,9 @@ msgstr "Suivant : %s" msgid "Cancelled %s" msgstr "Annulé %s" +#: inc/page-builders/bricks/elements/payment-history-widget.php:51 +#: inc/page-builders/elementor/payment-history-widget.php:42 +#: inc/page-builders/elementor/payment-history-widget.php:91 #: inc/payments/payment-history-shortcode.php:499 msgid "Payment History" msgstr "Historique des paiements" @@ -18832,6 +18837,21 @@ msgstr "Choisissez exactement quels champs apparaissent comme colonnes." msgid "Restrict who can view entries by user role." msgstr "Restreindre qui peut voir les entrées par rôle d'utilisateur." +#: inc/abilities/forms/create-form.php:159 +#: inc/abilities/forms/update-form.php:162 +msgid "When true, SureForms does not enqueue its default frontend styles for this form, so the active theme styling applies instead." +msgstr "Lorsque cette option est activée, SureForms n'aligne pas ses styles frontend par défaut pour ce formulaire, de sorte que le style du thème actif s'applique à la place." + +#: inc/page-builders/bricks/elements/payment-history-widget.php:78 +#: inc/page-builders/elementor/payment-history-widget.php:98 +msgid "Items per page" +msgstr "Articles par page" + +#: inc/page-builders/bricks/elements/payment-history-widget.php:86 +#: inc/page-builders/elementor/payment-history-widget.php:108 +msgid "Show Subscriptions" +msgstr "Afficher les abonnements" + #: inc/post-types.php:226 msgctxt "post type general name" msgid "Forms" @@ -19259,34 +19279,34 @@ msgctxt "block keyword" msgid "color" msgstr "couleur" -#: inc/frontend-assets.php:303 +#: inc/frontend-assets.php:310 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgctxt "Quill heading picker: default paragraph style" msgid "Normal" msgstr "Normal" -#: inc/frontend-assets.php:310 +#: inc/frontend-assets.php:317 msgctxt "Quill link tooltip label" msgid "Visit URL:" msgstr "Visitez l'URL :" -#: inc/frontend-assets.php:311 +#: inc/frontend-assets.php:318 msgctxt "Quill link tooltip label" msgid "Enter link:" msgstr "Entrez le lien :" -#: inc/frontend-assets.php:312 +#: inc/frontend-assets.php:319 msgctxt "Quill link tooltip action" msgid "Edit" msgstr "Modifier" -#: inc/frontend-assets.php:313 +#: inc/frontend-assets.php:320 msgctxt "Quill link tooltip action" msgid "Save" msgstr "Sauvegarder" -#: inc/frontend-assets.php:314 +#: inc/frontend-assets.php:321 msgctxt "Quill link tooltip action" msgid "Remove" msgstr "Supprimer" diff --git a/languages/sureforms-it_IT.mo b/languages/sureforms-it_IT.mo index 18548b48011c7bf6b12129d4ae1d3e2bb087933b..62d386d38d9ec10b206cfbcf79176cefcacbb77d 100644 GIT binary patch delta 70944 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�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 Sq-SI^&A}V?Z%jLy{(k^ijCyGR delta 70582 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 diff --git a/languages/sureforms-it_IT.po b/languages/sureforms-it_IT.po index 2a4a71589..847260b8b 100644 --- a/languages/sureforms-it_IT.po +++ b/languages/sureforms-it_IT.po @@ -20,7 +20,7 @@ msgstr "" #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 -#: inc/page-builders/bricks/service-provider.php:55 +#: inc/page-builders/bricks/service-provider.php:56 #: inc/page-builders/elementor/form-widget.php:144 #: inc/page-builders/elementor/form-widget.php:301 #: inc/page-builders/elementor/service-provider.php:74 @@ -562,7 +562,7 @@ msgid "I consent to have this website store my submitted information so they can msgstr "Acconsento a che questo sito web memorizzi le informazioni che ho inviato in modo che possano rispondere alla mia richiesta." #: inc/fields/inlinebutton-markup.php:184 -#: inc/generate-form-markup.php:640 +#: inc/generate-form-markup.php:654 msgid "Please verify that you are not a robot." msgstr "Per favore verifica che tu non sia un robot." @@ -614,7 +614,7 @@ msgstr "I dati del modulo non sono stati trovati." msgid "Sorry, you are not allowed to view the form." msgstr "Spiacente, non sei autorizzato a visualizzare il modulo." -#: inc/generate-form-markup.php:820 +#: inc/generate-form-markup.php:834 msgid "There was an error trying to submit your form. Please try again." msgstr "C'è stato un errore nel tentativo di inviare il modulo. Per favore riprova." @@ -699,11 +699,13 @@ msgid "Form Title" msgstr "Titolo del modulo" #: inc/page-builders/elementor/form-widget.php:320 +#: inc/page-builders/elementor/payment-history-widget.php:110 #: assets/build/blocks.js:172 msgid "Show" msgstr "Mostra" #: inc/page-builders/elementor/form-widget.php:321 +#: inc/page-builders/elementor/payment-history-widget.php:111 #: assets/build/blocks.js:172 msgid "Hide" msgstr "Nascondi" @@ -797,7 +799,7 @@ msgstr "Modulo aggiornato." #: inc/global-settings/global-settings-defaults.php:166 #: inc/migrator/importers/cf7-importer.php:168 #: inc/migrator/importers/ninja-importer.php:1152 -#: inc/post-types.php:998 +#: inc/post-types.php:1004 msgid "Admin Notification Email" msgstr "Email di notifica amministrativa" @@ -805,7 +807,7 @@ msgstr "Email di notifica amministrativa" #. translators: %s: {form_title} smart tag placeholder. #: inc/global-settings/global-settings-defaults.php:173 #: inc/global-settings/global-settings.php:588 -#: inc/post-types.php:1005 +#: inc/post-types.php:1011 #: assets/build/settings.js:172 #, php-format,js-format msgid "New Form Submission - %s" @@ -8331,7 +8333,7 @@ msgstr "Modifica modulo" #: inc/blocks/sform/block.php:42 #: inc/page-builders/bricks/elements/form-widget.php:508 #: inc/page-builders/elementor/form-widget.php:827 -#: inc/post-types.php:1318 +#: inc/post-types.php:1324 #: assets/build/blocks.js:172 msgid "This form has been deleted or is unavailable." msgstr "Questo modulo è stato eliminato o non è disponibile." @@ -9327,7 +9329,7 @@ msgstr "Precedente" #: inc/global-settings/global-settings.php:530 #: inc/migrator/base-migrator.php:548 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 #: assets/build/formEditor.js:172 msgid "Thank you" msgstr "Grazie" @@ -13462,7 +13464,7 @@ msgstr "Modifica questo modulo" #: inc/global-settings/global-settings.php:531 #: inc/migrator/base-migrator.php:549 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 msgid "Your form has been submitted successfully. We'll review your details and get back to you soon." msgstr "Il tuo modulo è stato inviato con successo. Esamineremo i tuoi dettagli e ti ricontatteremo presto." @@ -15605,23 +15607,23 @@ msgstr "Metadati opzionali del modulo, inclusi impostazioni di conferma, conform msgid "Message displayed after successful submission." msgstr "Messaggio visualizzato dopo l'invio riuscito." -#: inc/abilities/forms/create-form.php:245 +#: inc/abilities/forms/create-form.php:249 msgid "Form publish status." msgstr "Stato di pubblicazione del modulo." -#: inc/abilities/forms/create-form.php:290 +#: inc/abilities/forms/create-form.php:294 msgid "Form title is required." msgstr "Il titolo del modulo è obbligatorio." -#: inc/abilities/forms/create-form.php:298 +#: inc/abilities/forms/create-form.php:302 msgid "At least one form field is required." msgstr "È richiesto almeno un campo del modulo." -#: inc/abilities/forms/create-form.php:326 +#: inc/abilities/forms/create-form.php:330 msgid "Failed to generate form fields from the provided data." msgstr "Impossibile generare i campi del modulo dai dati forniti." -#: inc/abilities/forms/create-form.php:383 +#: inc/abilities/forms/create-form.php:387 msgid "Failed to create the form." msgstr "Creazione del modulo non riuscita." @@ -15641,7 +15643,7 @@ msgstr "Se vero, elimina definitivamente il modulo. Altrimenti lo sposta nel ces #: inc/abilities/forms/get-form-stats.php:113 #: inc/abilities/forms/get-form.php:110 #: inc/abilities/forms/get-shortcode.php:95 -#: inc/abilities/forms/update-form.php:287 +#: inc/abilities/forms/update-form.php:291 msgid "Form not found." msgstr "Modulo non trovato." @@ -15841,8 +15843,8 @@ msgstr "Campo di testo" #: inc/form-restriction.php:183 #: inc/form-restriction.php:184 -#: inc/post-types.php:1224 -#: inc/post-types.php:1264 +#: inc/post-types.php:1230 +#: inc/post-types.php:1270 msgid "This form is not yet available. Check back after the scheduled start time." msgstr "Questo modulo non è ancora disponibile. Ricontrolla dopo l'orario di inizio programmato." @@ -16126,8 +16128,8 @@ msgstr "Impossibile eliminare il webhook." msgid "Unable to connect to Stripe." msgstr "Impossibile connettersi a Stripe." -#: inc/post-types.php:1225 -#: inc/post-types.php:1265 +#: inc/post-types.php:1231 +#: inc/post-types.php:1271 msgid "This form is closed. The submission period has ended." msgstr "Questo modulo è chiuso. Il periodo di invio è terminato." @@ -16662,78 +16664,78 @@ msgstr "Colore del testo su sfondi primari come esadecimale (ad esempio #FFFFFF) msgid "Spacing between fields." msgstr "Spaziatura tra i campi." -#: inc/abilities/forms/create-form.php:161 -#: inc/abilities/forms/update-form.php:164 +#: inc/abilities/forms/create-form.php:165 +#: inc/abilities/forms/update-form.php:168 msgid "Email notification settings for the first notification. Merges with existing." msgstr "Impostazioni di notifica email per la prima notifica. Si unisce a quelle esistenti." -#: inc/abilities/forms/create-form.php:165 -#: inc/abilities/forms/update-form.php:168 +#: inc/abilities/forms/create-form.php:169 +#: inc/abilities/forms/update-form.php:172 msgid "Enable or disable the notification." msgstr "Abilita o disabilita la notifica." -#: inc/abilities/forms/create-form.php:169 -#: inc/abilities/forms/update-form.php:172 +#: inc/abilities/forms/create-form.php:173 +#: inc/abilities/forms/update-form.php:176 msgid "Notification name." msgstr "Nome della notifica." -#: inc/abilities/forms/create-form.php:173 -#: inc/abilities/forms/update-form.php:176 +#: inc/abilities/forms/create-form.php:177 +#: inc/abilities/forms/update-form.php:180 msgid "Recipient email. Supports smart tags like {admin_email}." msgstr "Email del destinatario. Supporta tag intelligenti come {admin_email}." -#: inc/abilities/forms/create-form.php:178 -#: inc/abilities/forms/update-form.php:181 +#: inc/abilities/forms/create-form.php:182 +#: inc/abilities/forms/update-form.php:185 msgid "Sender name. Supports smart tags like {site_title}." msgstr "Nome del mittente. Supporta tag intelligenti come {site_title}." -#: inc/abilities/forms/create-form.php:185 -#: inc/abilities/forms/update-form.php:188 +#: inc/abilities/forms/create-form.php:189 +#: inc/abilities/forms/update-form.php:192 msgid "Email subject. Supports smart tags like {form_title}." msgstr "Oggetto dell'email. Supporta tag intelligenti come {form_title}." -#: inc/abilities/forms/create-form.php:189 -#: inc/abilities/forms/update-form.php:192 +#: inc/abilities/forms/create-form.php:193 +#: inc/abilities/forms/update-form.php:196 msgid "Email body. Supports smart tags like {all_data}." msgstr "Corpo dell'email. Supporta tag intelligenti come {all_data}." -#: inc/abilities/forms/create-form.php:195 -#: inc/abilities/forms/update-form.php:198 +#: inc/abilities/forms/create-form.php:199 +#: inc/abilities/forms/update-form.php:202 msgid "Form submission restrictions and scheduling." msgstr "Restrizioni e pianificazione dell'invio del modulo." -#: inc/abilities/forms/create-form.php:199 -#: inc/abilities/forms/update-form.php:202 +#: inc/abilities/forms/create-form.php:203 +#: inc/abilities/forms/update-form.php:206 msgid "Enable entry limit restriction." msgstr "Abilita la restrizione del limite di ingresso." -#: inc/abilities/forms/create-form.php:203 -#: inc/abilities/forms/update-form.php:206 +#: inc/abilities/forms/create-form.php:207 +#: inc/abilities/forms/update-form.php:210 msgid "Maximum number of entries allowed (0 = unlimited)." msgstr "Numero massimo di voci consentite (0 = illimitato)." -#: inc/abilities/forms/create-form.php:207 -#: inc/abilities/forms/update-form.php:210 +#: inc/abilities/forms/create-form.php:211 +#: inc/abilities/forms/update-form.php:214 msgid "Expiry date (YYYY-MM-DD)." msgstr "Data di scadenza (AAAA-MM-GG)." -#: inc/abilities/forms/create-form.php:217 -#: inc/abilities/forms/update-form.php:220 +#: inc/abilities/forms/create-form.php:221 +#: inc/abilities/forms/update-form.php:224 msgid "Message shown when form is closed." msgstr "Messaggio mostrato quando il modulo è chiuso." -#: inc/abilities/forms/create-form.php:221 -#: inc/abilities/forms/update-form.php:224 +#: inc/abilities/forms/create-form.php:225 +#: inc/abilities/forms/update-form.php:228 msgid "Enable form scheduling." msgstr "Abilita la pianificazione del modulo." -#: inc/abilities/forms/create-form.php:225 -#: inc/abilities/forms/update-form.php:228 +#: inc/abilities/forms/create-form.php:229 +#: inc/abilities/forms/update-form.php:232 msgid "Schedule start date (YYYY-MM-DD)." msgstr "Data di inizio pianificata (AAAA-MM-GG)." -#: inc/abilities/forms/create-form.php:239 -#: inc/abilities/forms/update-form.php:242 +#: inc/abilities/forms/create-form.php:243 +#: inc/abilities/forms/update-form.php:246 msgid "Custom CSS for the form." msgstr "CSS personalizzato per il modulo." @@ -17333,37 +17335,37 @@ msgstr "Seleziona questo per creare un quiz con domande a punteggio e risultati msgid "Survey Reports" msgstr "Rapporti di indagine" -#: inc/frontend-assets.php:304 +#: inc/frontend-assets.php:311 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 1" msgstr "Intestazione 1" -#: inc/frontend-assets.php:305 +#: inc/frontend-assets.php:312 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 2" msgstr "Intestazione 2" -#: inc/frontend-assets.php:306 +#: inc/frontend-assets.php:313 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 3" msgstr "Intestazione 3" -#: inc/frontend-assets.php:307 +#: inc/frontend-assets.php:314 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 4" msgstr "Intestazione 4" -#: inc/frontend-assets.php:308 +#: inc/frontend-assets.php:315 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 5" msgstr "Intestazione 5" -#: inc/frontend-assets.php:309 +#: inc/frontend-assets.php:316 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 6" @@ -17510,6 +17512,9 @@ msgstr "Prossimo: %s" msgid "Cancelled %s" msgstr "Annullato %s" +#: inc/page-builders/bricks/elements/payment-history-widget.php:51 +#: inc/page-builders/elementor/payment-history-widget.php:42 +#: inc/page-builders/elementor/payment-history-widget.php:91 #: inc/payments/payment-history-shortcode.php:499 msgid "Payment History" msgstr "Storico dei pagamenti" @@ -18829,6 +18834,21 @@ msgstr "Scegli esattamente quali campi appaiono come colonne." msgid "Restrict who can view entries by user role." msgstr "Limita chi può visualizzare le voci in base al ruolo utente." +#: inc/abilities/forms/create-form.php:159 +#: inc/abilities/forms/update-form.php:162 +msgid "When true, SureForms does not enqueue its default frontend styles for this form, so the active theme styling applies instead." +msgstr "Quando è vero, SureForms non mette in coda i suoi stili frontend predefiniti per questo modulo, quindi si applica lo stile del tema attivo." + +#: inc/page-builders/bricks/elements/payment-history-widget.php:78 +#: inc/page-builders/elementor/payment-history-widget.php:98 +msgid "Items per page" +msgstr "Elementi per pagina" + +#: inc/page-builders/bricks/elements/payment-history-widget.php:86 +#: inc/page-builders/elementor/payment-history-widget.php:108 +msgid "Show Subscriptions" +msgstr "Mostra abbonamenti" + #: inc/post-types.php:226 msgctxt "post type general name" msgid "Forms" @@ -19256,34 +19276,34 @@ msgctxt "block keyword" msgid "color" msgstr "colore" -#: inc/frontend-assets.php:303 +#: inc/frontend-assets.php:310 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgctxt "Quill heading picker: default paragraph style" msgid "Normal" msgstr "Normale" -#: inc/frontend-assets.php:310 +#: inc/frontend-assets.php:317 msgctxt "Quill link tooltip label" msgid "Visit URL:" msgstr "Visita l'URL:" -#: inc/frontend-assets.php:311 +#: inc/frontend-assets.php:318 msgctxt "Quill link tooltip label" msgid "Enter link:" msgstr "Inserisci il link:" -#: inc/frontend-assets.php:312 +#: inc/frontend-assets.php:319 msgctxt "Quill link tooltip action" msgid "Edit" msgstr "Modifica" -#: inc/frontend-assets.php:313 +#: inc/frontend-assets.php:320 msgctxt "Quill link tooltip action" msgid "Save" msgstr "Salva" -#: inc/frontend-assets.php:314 +#: inc/frontend-assets.php:321 msgctxt "Quill link tooltip action" msgid "Remove" msgstr "Rimuovi" diff --git a/languages/sureforms-nl_NL.mo b/languages/sureforms-nl_NL.mo index 1e806a21aa58770b2e55d90b00402b529b2d2b10..42b58563d17a665986d85f0121b6d43e0b75b267 100644 GIT binary patch delta 70965 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 delta 70578 zcmX`!dAycG|G@G4oRlR+o3!b)@B5;CFDlxjNQ*Y9B(xD%l(bM!aTgx}|L&%A!mKku2ju50Ey-G=RNPS6#vVgCzUFS zo2FPxrF!Mb_gca435GA`7=^Ya5~n&9atOBp##+@kdexV z4e?^^h}U2*%%4h9sTU|X!_DXb+woHT7O%iFSP=7H7G_cz4X7=WwAA(JOozn!BrHw+ zajcIUWBm*|aN&Ysyuo-8Wj|FfOUV?kkiG36O70r06XhxFCffp1d6?h5yKw)&I715bBL_6#n>qF4{ z?#HS)3tQk;bcWfAh4zclej8$SY=cf@40_*Z#gcHtVH!;JarA+Iq8D5dZp??a7sc#Y z4&Cila1mC+;xq2yZU=H&>Uq#07cOLE1*kL3(MhX9OL<4M8O$d zRyJ(5%II-vg{HPY8ql2B{xo)?z8OvNCFR1dZ;L~yPsLhz3f*+&%7+(O4|Fp>hl6no z)@A%uxeDP`+8dqeQ|L^WMqfcw`DUzthz|TY8d%4!itW%0EkgI$b7*GP zqI>8gOibNj3f}lLnu$Nr2Xa>m16+YuQ?H1=Q0_$6@-Z~v1!%y_&_GwCr)3Ko;0|;N z_My-H4;}AVCCw+ht2KitM(;yY2HVVR=Yh2yXa z&c_P)KDxVqL*I;rYsAfoQ>jml^?WtMhg8y)f+-z~9dHgB={IO*j-xaAC)Nwr3XxYr zGjk=n2d+lfxD&c0H=ui z;&5Cs2)1gcCmgvre}&ZXiw17vKn2=H?S`5L_gflYnG9^ z3X@V4e6tP3ES!$D@kOkIUt>co*gV)3t5KhczS*|mm3Rp4Fn^2iqUnWmsegxN_@0&_ z;D^xn%Ut9=k))oYkU_&U=of>fXeOSE_0{N1)}U*)A=ckVQ@S&H7|qzvvHlmj`Es@j zOHc%TzI3!EX7}^IS-hbQI&eocwLQ}}q*8-o`!F=X(ddlsMwetVy2dlmrFkN@zkul_ zKnH#U-81i^_kV(UJpYF%_{uzqMtmBb$$70q2bZ87{(!nSx4dt<#ep~EN84ws=bdl7wZ z4I1D(=u&-x?v-!R{)=A4`8UFHSA|GxpdB?tBW;NW)(L%}d$ezKD0=^Bbin)2&Gm3} zPIP|s88p++qy4VBDhVCDMuUNDjBblJ?2djK{Q}L<;aLAE`djpm=s7gtjH`neqX8B~ zUtnd>3D!?2IB**@MV-(G`$dPLDIJ3qaRScAmFPKd(>6Rm7M=M6=<`$27t>sHv#vq| z*nlp{`{=mI=M+rEH&`Bz#rmb~!b}UGk(NdWs1e)ip&eWm?Tp@cBf9x+K?fd&PGBq= z*!|Imk${ra6QPh=j;`fv=o)QA2Yd@n`TJ<<_M;vDfcN7c=zZhbhwBqCJ@Z&!h2H-* z8sIK8@PnAg^M90r@8UnO92UMNq^boPNGCM%>(CDRp-XieIRM+HD1smBXuu!Kws4# zq2DJi=okVli$$ncLq8+h$9g|ZY`B+#Gn#{Ll4a3X(HU<=H_c8om0!l|$I+$v105ho zr!Zhiw7*K|%p0LEs5bHXP0>M};?MshXfTDN(SgRJse2er{iA5ApTcy2XsTaD2U?E? zwj;U^eeN4Hqd%daF_(4@AHR9gzzTHc{Ci_@8k}iG^i6kVthYt4UxyCR4-I5EI`h%! za}T0{&5rGhWBujmo3Z^vH1GrHb6+R%hLh+(=g#!B}!DhHDUOyVI|AKy1 zJB$8clD}Ix##f==dOM;^xeT4a^XR~@BNIzfn<+TZw&*T2fc@zE;G0-K5j}%;lz>eq=^^6Y2Y@Yv7@rH5HiD-w9pbyNAEZ#xpt{$GUEd15 zuRA)!zGy$Uq63VH*C)p7Gvf7mXn@Nx@qv{T?06lz6x-3=y&Lo5;pngEjL+*4I>?DW zp9dZA3N)aKvAs^Lw}{uTjrE@K`hXts`~OH94B)FSs$x z@Dj9MI9dXIZciGPHyB=&t<`P370<41PpA{1csd z&R(IOAH7~KS_>VhY4ndLcs{9qp#AZ&<^iD*S1*S9j_lmKh3_6 z^`CGz^}o<#yW^%X&|Y-u4x>x@6FQMo$T$k=F zz39MGVtpPO_|oWFG{6them+B&`dhTWv(`QT=k*Q0dMShzsNaH>a5j45W^}WC5d9cU z{eE;U4@JL6m+mw=a7MrId`@(4U4|}AdGva9OiV=s3T3ewuEfDO81vnnks6Bk;JbJX z`{IlJ!-q+(0bzjFXg{6M0j@)5+B??ojP-lafFDHzT`(Yi{=YzjYx@eCfsfFRen(UH zFZw{Pfg$CE(f36~w8MJnsc03iUmLIAgg!qMeg1B=--pm8o;ffHuiV8nl%ZiAnwrDt zT7Hjqd@h=OQ0Oo>`pz$gZni7Y-ygI?Q{5l!XB5`M1kK1gyb5=s&lgArhtKb7*qDaS zXoNG-fuBJ4z+yDwmFUbj#rh5`Mg1T;z<+1}`ELo|e9EHtw?+H89_{~Tbn_;+Qm9Db zZZwt4(GQ{3*d0GWzq=J5l9B3#bh^(8|? zruHI%C8-}M*x^|;WjTk12n(PQ7Q-f3CAJSkPs3fYeh-?NiD<^=VryK8uKf`-u;0)( z>-ob&y%^^9^S>qqQ`8*oxFed0>(EVeb8H`hrfxKP{O&^sn23G_n~hH7b#x-z(fd9| z`~M6*B}cF!9>=nt|1!6Q&36q}rG5u`<1^@2r{~bkxDpL;3p(I-td8HK$G6ak5O_0m zt*=2dcs;sQgV78m=w^Qm6MwMSNWqyLKqEblPv9T;JI=|-uegizznwzsyTYcpcvN@+l|h$a54yHrqQ7i9 ziLPbN(c!mdS?B~>qnmgDR=}CDeLcE)KSz(-&**#R!ZD$}%oxtU9XF!E0ehku7>KUj zow0o^x+Ig()J}`nAB*kt(WP08KL1?2z9wGZ5Zkw+0qsJU?r1{68D2IvbWko@6>YDB z4%jBzIkxvgUpT|jz$c@dZ#H`WGIU8_#^SgceNi1o@BbBDg5)0xZj#dDLgcm4l(vZW zLpz>;m2oy!!Y%0g;0NrASKpnHdI}eyfz`Vw1kxEZsP~HX-sq+t5=>IVDVW-kXv)W+ zsT_}<=ZWZXdN{T}9@`h9_dkoyXeAox+UN(d{UEv|$IywLK{J$pymJ1EQ*gkF==rXJ zW~4saVOz9=8>2(eJu?PUp`Q(Zp#xrVU+Av}8hB$&+>Na$%)=Y8 z8lFZoQJT}CmC*-k$9h9_rY)nL&==HA=nO}qnY$l-ehwPIQ)s}e(4||OaQ;(>0mm9c#_)}y`=o!RN=KWINWri4H)M>A0jeLf4FP+4?o zYNN-r*%Z#dFQBfm;Z}4;_oAtvh1KvGbeHeJ>i8qNH;PRS@AUfUeLXM>r{J~t0#?F5 zusfET7XDCc92&^W(>VWjype_$xDQ>^Les-usD{qqYJ3G*i#S`WkfL z_t4bt!wPsD9XQ{N@ItDH2GkDyY)EdV(1pU}c*7TH>d&H^?ZTO1;A*&-dVidP=RF#} z13rT8nH}gG?^85@)99z<`Ln`OmBuXU&Cu)pkclO!yC|5FN6;6|QuKVTLua@Z{oMW( zo8T#|gEeP|rMU&&3%8+xOhfOVi>CNB%)*`MsW^#l-m_T1^Pf2Gy42Cyu$PU2?b~R8@h>d%ndh`MpIZ5 z4WMJR2m0pg7afGod?ZQ4fLj1zZD%|RBV3`eSS9D-vV@xtwblV^$E_ug*`NQOuj)oI2*li zUbtQeJvJ55JbJ!Do`iyH^)=eziFm_VbY?l9 z413{nw4<_UM>V3&W!#H)^dFYS!VANIO|dTZzUYjeK;M*a#r88;je6xpp`TvpXTnrW|NY-J z6nuf~KzHSLn1vTU9ju5R%l23ihoG7tA3vfUnVxenmU}58cHVEeWThC|a+Lo~AbF46l#v!_d=l zAKK3pbdx^5g!6BQPt#xqR-pAaV*M?2hC9(29EjIX#`d#lM>&=TFF^yo91W;E+HV8& zbhSbI>yD0d^U@?lG@J$_ya#>YLA0Y;XeOSC*Po5`SI~^DL1(%N4R8;-2ae*Em~UCw z_3hEqH4wdjIy%ncgn}Kthz|5xys!?P!Mo9Y(eKf{@fR9k=JL={UUWuR#CipEDe9mv zt~O}@z0ryG$Hy=kPQf+L_H0Pah3Ji!p(!hZrnn+{UoCW?`e@*-&<@+9GwL4ghwk>_ z=zSy6wI7Qv!DQtABsC`#Qcp)$q5-T$Z`_2w4?c+g4=Ymt10AsBb0L7L(R%1}EzrO_ zqW$$iH~USoeFCO`{+~+0$mXK2$|Y#(*Q9UYD;0YF_o8e33%Z8qJs$=xgbr9LS{+S& zW3-~Qrz<)CMt+0_Q!M*W@ndpFz#rk~oI4wpWT!s$t zGMd`8=u&M*H{Vz2z`w-yv*@Q}-c{jxTXekS1_}l+EH;coXEr^$2;KFoqubDqzd!@| z2F=J%=*RO}wB!6Qhdi(U;NGZjANq z(Y9^Y1{FXmAGg&<>lS9d$ql z?vAeg4d`*~jV?*wX#eP-=#c2J=m@-)`|m*e+lns5wl$o8Q?-W%XY@_H@H-k|<{Kf9 zTxjI^(GCiuGcJy)c$D7$D$dX7TXsl6pZB6cwsX-@E&wC9*X{m z26_(7P_DIMpdx6!e6%k5d@D4tj%eUL(aqTh9d9)HT=D<~XYy!lcp8oTB{cH4V*5U{ z!*9{RPND(*i*|g;y3k%0eLvJi18ExT?a(Fe8ryF~`bkoQV#Darkh&lJ5o;D2*mJS{ z4RlXzM?2by2J~64AY2yJf&az&pV9N)4146#H#z@qqCzy-K}9s;>hXq# zvAtDnZx`FUqr0~^8sIQ=M)#ooJP_+s&S` z^%?XOT(CLRi=k^@J=WWzGwp>2eoORDbdB%B()cKv={52C+h{;L6AI}ck1>me-($Vt zmM~yV^ugBX16|O8`l2%!8Xbw=HwIn%N$AWUM&Fo^qsMX{+Rrm+=8~5wTt{Iu_Qe8Q z!@#4_wVa9$I3xNb+VL`UpjGI7Z^Y{x(LlDN_kDr}`Xw5`5%jqqk^7R=X$rT{knvXd zSiBXRQQwR{_+K>p+u?f@ktd>yF#YHM%PF{q zucMJ~MR)Da^bPzhhxMtSj@PTclaacSdP8)LpFjh70SDkZw4Y+{hTkbyMEf6(W?&pT zu?H~y-~UdbU}SU92j`<5KNnq(KKLP;>QB)hNWQ?b_y;<`72CqhE23-M7;SHl26#O> z@E~-YVVI;NqTpIRfUeEMXbR_|$MZS#6s*H$xD&lU_x6zD%h3UAp&d7m?H$n5Gz5J< zK|d?zVr5*po%8Q6g+8ZY82*Mv+~>V;<50B22hc!gqM4Z&+gG3iuSM5-8`|Ihur&UK z4qW{G@TRPd{!(fXI?jXdCt)+qqQMT9qnl+bmcx&+0{)4f`;s4o88t*-xt-Bh^l;3= zN72o>8lCAjG?RzWO#F-nn0-gcZ1IGG9aYCNcul-;2RgHfXa}>k+!&TB0L-aW1{5Wi?JZQZ-`dnQ!gDud&I-&h`LGSBno$*tBD7dQ!p{HOh8u?N5 zeE){dpwOOBFNSv13O%mv(bLg2w)a8<=#K_4B3{2Ywoi`E#>A8?qTmdlMhEG1Jc)Lc|C7*h7P=(m(0(eT{nSR6vH{v(>sarIK7RwIGyVzZ-w4OjklqyWhDp&! zqYKcEo<}==72T9?qN)EF9rz2hpRdsSzeP9WNvwqbVim0TY4{aRkAy;18Xm*S_%2?9 zzoHQ~`7ETW6MCNepdH^6eGnaZW~@JkX5wkAiO-<{9YAOP6}p5!pc7C2q~IFg#rL3Z%Gp@R^S>fq zco%(T9z-Ml6@B&ojgzqW7vcIcG(#KE8SlV`cnHnV_P&ghcEA>z{LfYs4mS`STKlW1!+fNNsCd#v|DpBoq*9vy`)(Y;s| zr=i~y)}#F2Wc`@2PZqZzp+*6%?Beh^*r z8S(nUV3K-;f+>0jufjdp5)1t=baWHerhYq`spV*9HlTZ8EBbxmQ}meqj4sW^Ux&|( zy68-Mp}W368qi=&|Nigxc*E%Ey=clNp##oB1AI36I@-b8XvZH$_o5m30)75lbQAxI z2Ab`gU_o@RRKiO={|za)Td#@sKvOsXUDGk>fC;+Wr(kJ(1P$y}G?1<6eeam0k9If|eQq?mHzuMVQuEOn zzJb2bwxgTwQ1n0a4OsFh=ieI}91Rh*L+f49HSUjoAsK?M-OzY_M64%hAQREd%s>O2 z6YEc*fiK1exC&j8<7meIKAMDqGQSHSzZYO%F4RE>d>l>HeDquFQgmrHp=-Juo$--) z{l9qq@?&9!m9Qz-TcW3AA{yu;=$mqOLctU+MQ8E~I^f1we;18(H@Zjmp)>pjz3&8i ztWKc=WPYEK>WmkoDejFf1en4+C6{u3cyJzPr#& zOhkW_T7WLuMl`h_q8a)Um*6kh4d$zmxV}zwj83uX_HEQgFA= z`z7qwMd+Hnh|c^iG(&si^`q#_PN9J`{Wa{NHt25eiay^jIvUN`bhMv^SPEak#8>5~ z6pZ{|^tfI0TX=O|j0RQ{%}70Tv$jO{LMJq1ePVqu8o=n-J_+5V52KszG4%P>n2DQz z&dZw1sd?Hv7W4_VCvpNXSN?*qhsiRr_nv}51QKl(A}N;YN*Z)J8IN!OD$qMLF*2VO{|I<2N=#CC>GaB(|^xN#zczr3l*Wi=jzKb5$-?1td`8S-3cIY@m|BdHAp}|cx1AX8HybX8ZaBTcvcxA4{ ztEuN_%bAgz&=02(Xuz}452fX?{w~&{z8`C2&WuoRhUKX9-ydbB*Kz?3cDNi}g0<*c zeTH^?2wmf!(WN?r?t!!DrpnCBOfT8RXgxo=L|35wRF2ncp{JlBx~Z>8D7eNw(KQ(u zy(_j)MAvjCx+msGmtZyOE3g6{KvR5Pwh%xq>_fdVIKqLAS zUE}QMg?eE$z;b9uwa~R|jds*C*6&0!^$_~zoFCg)qZ8YPZo*H|aZVujC#gRuxO*?k z9&Rj*22cSVunC&7PH3S0&@T`p&<>}dfh?r}HohP;)f^|H6WWFjcmNIT1lnJ= z3p2xi|C^gade@=@UxDuOifBMpV|#5h&}OmT8E>Y36Z)#&ir&8sU8)b!34DPL{9~+V zzbFKB`9+!G@Bfvj!GUX`$EqnB;nlIdD>^`**ghb(k3e_zJvbU4!0LDw-4hjaW~RT| zwZe+j$DkkcORxlfos;wLG0ApuX8O-~`lD;Q5bae{=uUJdQ5wvduK8h$K?qHH`g}wO?D8o@Lx2L;+KR-8=!BjUU(Hw z!Yup<&CE~e()@;=nrxSbKnkGsD(Lm5vEB*ITyir7Uy(!5H{U|6fg90Ce?n7sah@6g<~UqN~vkx5oMh=)j+&GyM@O z;a})(FPkqsUpd+khtl2-&D0xcpl_ki?~fkE^xywEWq}KPYNU@#F3h5ug>|txx`tzM z1x~|ZSf@Zp^%C^?7h`=rn#p%!`;J)Oj~TQdM!&NC4-;=ZLcujVfo_u1Xa=%h7CO$2 zwW$|D_e3Ym!V$6lD0&KBMo-J9Xn%jAdmyu5=;wU&l;lU3ph`i`zbULkgQ;qWz6aW% zk#<2l>V^i|JJtuH&y9%fqtFh=qnVf#orNy#BJ}x}(EDD;QMjof=f6CKnwN(TZ$ty= zhekdeeRbY}2J!&f(ZlFznS&0n8tr&1`uzK`{ZnjA{Rq~>5`{v5H(*!lgOk{>5$))G zbin;+Mvh@s{0$wTMBxxX1$2q3qXX7Qf1+uPF2%iQe@pNtT#YX6B}KyXS?E$E&Rr~wR}iTu zsd5y~aiKbz(kfSkUEdzfzynwjA3;A%)}fjB5M7E-q6g3czK))Vo{FA}W-lHCNz3^! zNTCG}W}%y^Kf2aK(l_vn2sDuKXht5w`|-(my+~G=d1-X>H9|i-+M*x7{n38MqW#>5 zW@sv2$oQ#w6g)Od(3x(>viK`H)5}YQrO84EsEU5kXo!`t3%X?Yp&6Koejk{J{V{LJ z@V*$0j&}qf#r~zJd&6l8M%bft7_c{*+CkCb=m2-2_dOV$js`R!_FNr_kpvEgzmM zjAo(&`svpo);nPd>iv)alhi~CZi;E>u~`)B>(EF)ihhn};9E41AJG|}K?gpE-k+mF z*nD}>>t&*q(LGfQ?Wb9~o%3*YywDN-V$lmt&DiKP^tdfRXZ#vE&^zeByU`4Nf$0FT zBlREA=NeTEYu^um|;qSOMpvdto!0;oaz|`k@NvzZ!)cRl@^yu`2bM=%?9ctcbs(--Jq33m=<3 z&{y`0*b{$7zofRUo|*m??G$W7{Qx${5;elW{W1NihwW&eSA+BK?mR++=kqMOxeC?{ znslKSg-1OACuW7jLg zQYH&1)T3cFI`b2lKF{cPy-VwbwW@|gsJBBm-|Og-yo(0@H~PlRtRMEm1?Z+Mj7_mD zI-xZ z6Y)BH9uJ_In%F4pp=mhG^ZyzJFJv_i1C&EEPz?>NF?zin8hK~*Acv!23_N==n{N_elPeI?dN;+J@O0s2E3qU=r<`r!ChVhz0d~JsYf3ifNsKJ z*aydLbqNLjx$W?3?2CSwjE+u0 zADEA3;w3boH)DMV8qfiB?Z1oHe?~KQ4t-CY-#XMULo-+cFZTS`qu_w8(T=W4~&UvQ?P{XdPq zu--s3mF%V91IN(RWON8K&V#=3s-ke0G&Z|bnR}84vyFFjP<+G0Vkt@ z&A~fxA@;>fyM*f_@p8}qeH6^V6X;s4i1n@Larp$(FA_A>=XVWXy^5kK?~0~;AR6F! z{2Cv}>u_whO#WnyuifbT;pXmPqVq7>Lc5BefHgf7|9gn|Q}LnF;|Ls*+q(c010 zXa=rBkK-UTwRfX?`Jd;{;pJpIC+cob_;Ux+v3ZoCm|-pqKO z{}~jj;*a^;mfMEysOK3FewgfqC#f&Pc{pcaX6h%bJSa2$&vh0aoSFKQ`m1Or zSKgAD{=4KG(f%(O65bD2qCd=b#>5MEQ7~mou@8QNt+Cdv;Zt!O`r=rMwebsN?NYgi zhSXk#S=1juU(rj^pNMv&{pT7MGSCPcQojvd$`!*n{~oi~Y3PWXuo+%FJZz$C(bVR=P!`kg3!Ff`A^INKgjx7D`f+{)t7Fcw z;Wwd;a3u8&_=V@c^0>_OKOCOp?(m}6heq}_I`9wZtMxbZRs0{iyYt=?-e4us0cv4o z9DwerS!h5{qOb0!(HGIuczqS7|Nj3Q6v}epU9{sL&>R28JeXsANd4vLu5W}6Fbe(N za4#D049tb|;`L|I09K(BcpdHcE%dpMF#YfUeM#YR8h%7)nEl?+;pOP(c1iTutwB?N z0`2&B%#G*J0CL_Jo-crAt~|PA)zIs;V|x>H=~`p@_kSJYh5pf7(V2`y2TIUPJQ&-b zKxeWX4d@j#kWI1vA^QCO=utESr=lrNozLY;IR8Fahz27pkFHfsG{tq%nKVH=XpIKa z1-*X&re7#nn)(X#x!vd+Za>zOzI2K z02ZSiKZmCDW%T~n(F|=yGx9EG;OWmre>FCN0qGy|5gP!SeVt+VA^V2+yGRUGiXN`Y$|IL_R$e z{`t%h@l)tmqV;G;AEN{P8Lc=uyxDqT1={CeC44isA4lHkZLNv_^^b%Hub&bAWOFdOBwwQg z{}k(I(3xk<4A=9anJJD2P!+x268(7ZK9lo5mBL*#+>8HWI~@OLX8J#!cn{4;uUTQn z{m~hZL<7DLeZfpc1KNZg@O^Y51!spnRR!IAS7J519en{UPAGURK1Cz?8BbvDIhm>N z@GOqT|IH0v|5!LJyKpn@KcWM!d_24l{z7N`9~xlpCxV60nU_WbtsC2u78G2QYth|* z6S}s8;|-(G6i+}OoPzF&xo9R9q64i!Ps18CfbD31pT+B6MNdS}ApInKUWOYlL?g_H zo`N#yfX$=Vq676r0~in;js`p${S=*q-nS5aH9sF+g>Jgnz{{h1r2$sO=I9#Vj#;=2?ROjc9@t}@@l!ugFqMCx z51uzays>JbGw6lR@a9;*9ld`vI@5`0MrNR&2~Wi93(%!|0S#<*yuJ<%_)SdgU^fK^ zIuQL|^gA@-AJKFDCptjRr$PXiqHA0XUGs8ie|2NMIT~oE*nSh*&n;-C?|F*z--5z) z8azh3@F~n#5F%cP2DS_xctxzgj0Uy_oyl9V{s}tcuh6AChR*y?Y=i%y&$U??0`Ibr z^X~(_XfW~t=nW%c{hsJ#bSAT+i_nf&qI=?1G|<=4el|q6q5Xf1v+#3tDQ{gAo*$b~ z@BwvqPD5{4gd^}pG{r@q4lk;*csKP|(EBSb4g)tu18j{>pbMI@-sn5OKi-KG@G3lw z)iG)GOo(temZD)4mcu8p0=|WQq4*)zFI*DpwXh29ozWLpf~{~7x*3mPOZ*Y5VfCfq z{=PVl`V3@Z{QK|ALMrk`3!$4S3r%UY*j^{LH$rFH9^FJe(0BVS=qvYL^fP4+y4hBs z8GaMZ%=_r`pW=nSf4-(*ihe+EykL3gI3N1muoRlA@z@7n!b6zr+04`?Jb`!NbI*lL z6n#Equp!oMq8r0(M)`WZlaUu41Pxg&3qy3@?3Zy z^(*iJT!`H45bJ}`Kt`aOcobH|IamodzsUK|qHux+KZbL@6n1Ob=ym8WpN#H- z*U<-$#`Zj`!i%dZy36mtj<^Kd;cqwq8^4^H{(nqef}N>nyb?BP*Mx#=cPl!>-#m3b45(er;c+IU^~ zdE$O7P5V38+w=cDg}O9cwLWZ~`_MI;fF*GD{%outL_ehRYzjYgw!-?1H3OXL`XeN5&ago3-d3;Igzi+n7mhNB&fLEmUo(7@)R z@A4PWj$T6Fn6F2-pdIf(PsLvJxb2VE&!OX-|86kJPr+Ab32cEC(O2bgERNIB4p*Wb zzJ{i79h&N0=y5%YPUHtPu(Rm>scqqUcC??I(R|3bNvcR}D2+Z)8SS7(tk*-=x-mLX zceJA$(MwMl-kooxpN5kQHbK*P(%ajxOOL^uAy563_qd6ny9BcrQd)6TP7UmcXXy z=IIyfBe5*?Ip`+bfD>>h7UJWy!~3D1TRzB4^`-rGH1Ivx7QaOIOt~GLf9_A!q|g&< zV`-d$4*VL{!Ozf4(%=LAume7VweS!+@TDJzlnzEyHv-Mj-RMjgpebH~ZoW6rz~7Jc z{jq)=z3)#nkn@r~VJ(ZGfwYMAzUWdsgbw&(yuKD)!_Tk+7T=qhx*gkMciez?;-#Nt zrvDF2Ct)}0Kj27g_G$R=dKC>MIZ2^0g}=~qUGKAy`VQy}ZjO#bmu3RG`=_C&WEQ$P z7e!a0$8IyaBnM;tcXapX+ZVnmmB*Hz{~i?5#|PVU!*aCaU$GPZi<7bA{&3$uG$UW3 zd*lRq|G&|kpNIP{N83xF?~SVHX0MHJVqGlm`9Dp;)aCyor0Q1mtI`(iiv)I2>k{2i(3j%28}fLSHU7^ zhvm@@t6>(_NB71JSOSNj0Zd0v!@^i!iT?cXCOVP*vHfH;^*_#kSuW)LU%0V8n%b`D zr{5sF5yxNy`~)Av++T+jFGFYYHhLPqL0?1{e-mcj89haVa4=3sHeKozw!tfpB;ogd z_Zjf=$=@Lo$yt>8nYb_ zFSu*467@ULz@J8*OH$8M@N4u2yai9=GkDX9@Q(fu%|yu`!rrKh9=Enw1^b{Gd?flT zdjA$Iho7Jm_#69R=8vI%Gp7Ij-ysz2@GdmP)6nmD%g_h6M&CnIybJx1Ie_&r^;0-4 z^|2V$HfTl$L`P#6>Qk@=9>k`Y>m+}4n^3rxg1h@cbj`NnP53XqfW3YWk>~g&yt@aX zYq|vc;d|&Ntnh1?NHugKtFTQ_T1XEHwC_yD@w zA4UUuCbqwbZK%J2Zl-^5G3GdxnHq!7;m26~bl6+Jp!JGpLPr0_k<^#}&iQxK75pRo zCsGs87sXFF1iSwkBHw~Fsqe*FnEh;@Xk+yFU5gGd2t7TM(0&)A-vd^oU&TH`1N}M3 z-~aw6c8m^K1(S%#Bf6P>#pYOm;XUuyqnm3yn##|y9R7_huvA92^rzb(Y(jknUX91m ziPg-^md@k=yps9@n0Vs`3JdWpK95gi%a)#bv-7g0&v|F`P1qZq=|IfDp=f4?qvv{L ztlxu9BtiGiB=osQ(ItEw-F(Z>%a){X*hqt^eh=O4pQ9aqk3R4ly2csVL&y2iB`buc zxHS4gs)7#GAYN~V2HqZh&vZeTs4x25E!mUM@yK|?B=mvV=w^8m4Qv(q;1+c4-a`jE zhz|TU8t_RpBY&a;o{QHn$`R@X(f-Py6RMj~a3*ch2fLvI^*}rB8?O(K?PJl5OhE6S z8S8V<4ws;TtU;gOg1zt~9Do(h4}r`;Cy*?lU@DfN9jru`U^AM5U3fJfLD#bK1=-TO zxgI)DJ2c>K=!|=zYkw;`u@Pv%cSon9_s>Hzk)#$=Fy*VG>!Mqt+tChpM)#r}>_-Fm zDta89$SE{4=g{X;7luq^N89s8FT?bI|5G?!;KKrqxHLM`YG@`JqHEqBO=%x=;9>Fl zX!NMNj2SrdKZ)ki;k+Q;ktaTWDp=y*kPasGWrXXVP4{t#)1KG-cf2%X7T zbfAf7CZ?lrzy(+lx1cjWfoA4+bmr$`dyd>8;Jj#{CDHyW<>vhRKrI@KygnLPGj!nA zvAq+Pp?*CY@V!_W=V2CZLq7$-!^Zd@8ero~vZY_)eX#-cCD;zX#0Ri^a%r~om&lb^ zfrf9m!61JTVn4xRZ_G~magi_zn_63xVVG;{BuOZh4K9WnWu zf@}IWdfqR}8y+l<4qOEd=xQ`G9nlWEp#%3u1GyDFUU#GY-G`?7kyw8meSUeYuR!`q zQtK%g(L3l2K1Ne_5dDz(7c()HFASUwy)PH~+~w%0DHH3BqHWO(T!#iS0L{Q~H1PY< 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 A1poj5 delta 70582 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 Ku|4(m{QnQn3hhw< diff --git a/languages/sureforms-pl_PL.po b/languages/sureforms-pl_PL.po index ec0998b63..529ff4995 100644 --- a/languages/sureforms-pl_PL.po +++ b/languages/sureforms-pl_PL.po @@ -20,7 +20,7 @@ msgstr "" #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 -#: inc/page-builders/bricks/service-provider.php:55 +#: inc/page-builders/bricks/service-provider.php:56 #: inc/page-builders/elementor/form-widget.php:144 #: inc/page-builders/elementor/form-widget.php:301 #: inc/page-builders/elementor/service-provider.php:74 @@ -562,7 +562,7 @@ msgid "I consent to have this website store my submitted information so they can msgstr "Wyrażam zgodę na przechowywanie przez tę stronę moich przesłanych informacji, aby mogli odpowiedzieć na moje zapytanie." #: inc/fields/inlinebutton-markup.php:184 -#: inc/generate-form-markup.php:640 +#: inc/generate-form-markup.php:654 msgid "Please verify that you are not a robot." msgstr "Proszę zweryfikować, że nie jesteś robotem." @@ -614,7 +614,7 @@ msgstr "Dane formularza nie zostały znalezione." msgid "Sorry, you are not allowed to view the form." msgstr "Przepraszam, nie masz uprawnień do przeglądania formularza." -#: inc/generate-form-markup.php:820 +#: inc/generate-form-markup.php:834 msgid "There was an error trying to submit your form. Please try again." msgstr "Wystąpił błąd podczas próby przesłania formularza. Proszę spróbować ponownie." @@ -699,11 +699,13 @@ msgid "Form Title" msgstr "Tytuł formularza" #: inc/page-builders/elementor/form-widget.php:320 +#: inc/page-builders/elementor/payment-history-widget.php:110 #: assets/build/blocks.js:172 msgid "Show" msgstr "Pokaż" #: inc/page-builders/elementor/form-widget.php:321 +#: inc/page-builders/elementor/payment-history-widget.php:111 #: assets/build/blocks.js:172 msgid "Hide" msgstr "Ukryj" @@ -797,7 +799,7 @@ msgstr "Formularz zaktualizowany." #: inc/global-settings/global-settings-defaults.php:166 #: inc/migrator/importers/cf7-importer.php:168 #: inc/migrator/importers/ninja-importer.php:1152 -#: inc/post-types.php:998 +#: inc/post-types.php:1004 msgid "Admin Notification Email" msgstr "Powiadomienie e-mail dla administratora" @@ -805,7 +807,7 @@ msgstr "Powiadomienie e-mail dla administratora" #. translators: %s: {form_title} smart tag placeholder. #: inc/global-settings/global-settings-defaults.php:173 #: inc/global-settings/global-settings.php:588 -#: inc/post-types.php:1005 +#: inc/post-types.php:1011 #: assets/build/settings.js:172 #, php-format,js-format msgid "New Form Submission - %s" @@ -8331,7 +8333,7 @@ msgstr "Zmień formularz" #: inc/blocks/sform/block.php:42 #: inc/page-builders/bricks/elements/form-widget.php:508 #: inc/page-builders/elementor/form-widget.php:827 -#: inc/post-types.php:1318 +#: inc/post-types.php:1324 #: assets/build/blocks.js:172 msgid "This form has been deleted or is unavailable." msgstr "Ten formularz został usunięty lub jest niedostępny." @@ -9327,7 +9329,7 @@ msgstr "Poprzedni" #: inc/global-settings/global-settings.php:530 #: inc/migrator/base-migrator.php:548 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 #: assets/build/formEditor.js:172 msgid "Thank you" msgstr "Dziękuję" @@ -13462,7 +13464,7 @@ msgstr "Edytuj ten formularz" #: inc/global-settings/global-settings.php:531 #: inc/migrator/base-migrator.php:549 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 msgid "Your form has been submitted successfully. We'll review your details and get back to you soon." msgstr "Twój formularz został pomyślnie przesłany. Przejrzymy Twoje dane i wkrótce się z Tobą skontaktujemy." @@ -15605,23 +15607,23 @@ msgstr "Opcjonalne metadane formularza, w tym ustawienia potwierdzenia, zgodnoś msgid "Message displayed after successful submission." msgstr "Komunikat wyświetlany po pomyślnym przesłaniu." -#: inc/abilities/forms/create-form.php:245 +#: inc/abilities/forms/create-form.php:249 msgid "Form publish status." msgstr "Status publikacji formularza." -#: inc/abilities/forms/create-form.php:290 +#: inc/abilities/forms/create-form.php:294 msgid "Form title is required." msgstr "Tytuł formularza jest wymagany." -#: inc/abilities/forms/create-form.php:298 +#: inc/abilities/forms/create-form.php:302 msgid "At least one form field is required." msgstr "Wymagane jest co najmniej jedno pole formularza." -#: inc/abilities/forms/create-form.php:326 +#: inc/abilities/forms/create-form.php:330 msgid "Failed to generate form fields from the provided data." msgstr "Nie udało się wygenerować pól formularza z dostarczonych danych." -#: inc/abilities/forms/create-form.php:383 +#: inc/abilities/forms/create-form.php:387 msgid "Failed to create the form." msgstr "Nie udało się utworzyć formularza." @@ -15641,7 +15643,7 @@ msgstr "Jeśli prawda, trwale usuwa formularz. W przeciwnym razie przenosi do ko #: inc/abilities/forms/get-form-stats.php:113 #: inc/abilities/forms/get-form.php:110 #: inc/abilities/forms/get-shortcode.php:95 -#: inc/abilities/forms/update-form.php:287 +#: inc/abilities/forms/update-form.php:291 msgid "Form not found." msgstr "Formularz nie znaleziony." @@ -15841,8 +15843,8 @@ msgstr "Pole tekstowe" #: inc/form-restriction.php:183 #: inc/form-restriction.php:184 -#: inc/post-types.php:1224 -#: inc/post-types.php:1264 +#: inc/post-types.php:1230 +#: inc/post-types.php:1270 msgid "This form is not yet available. Check back after the scheduled start time." msgstr "Ten formularz nie jest jeszcze dostępny. Sprawdź ponownie po zaplanowanym czasie rozpoczęcia." @@ -16126,8 +16128,8 @@ msgstr "Nie można usunąć webhooka." msgid "Unable to connect to Stripe." msgstr "Nie można połączyć się ze Stripe." -#: inc/post-types.php:1225 -#: inc/post-types.php:1265 +#: inc/post-types.php:1231 +#: inc/post-types.php:1271 msgid "This form is closed. The submission period has ended." msgstr "Formularz jest zamknięty. Okres składania wniosków zakończył się." @@ -16662,78 +16664,78 @@ msgstr "Kolor tekstu na głównych tłach jako hex (np. #FFFFFF)." msgid "Spacing between fields." msgstr "Odstępy między polami." -#: inc/abilities/forms/create-form.php:161 -#: inc/abilities/forms/update-form.php:164 +#: inc/abilities/forms/create-form.php:165 +#: inc/abilities/forms/update-form.php:168 msgid "Email notification settings for the first notification. Merges with existing." msgstr "Ustawienia powiadomień e-mail dla pierwszego powiadomienia. Łączy się z istniejącymi." -#: inc/abilities/forms/create-form.php:165 -#: inc/abilities/forms/update-form.php:168 +#: inc/abilities/forms/create-form.php:169 +#: inc/abilities/forms/update-form.php:172 msgid "Enable or disable the notification." msgstr "Włącz lub wyłącz powiadomienie." -#: inc/abilities/forms/create-form.php:169 -#: inc/abilities/forms/update-form.php:172 +#: inc/abilities/forms/create-form.php:173 +#: inc/abilities/forms/update-form.php:176 msgid "Notification name." msgstr "Nazwa powiadomienia." -#: inc/abilities/forms/create-form.php:173 -#: inc/abilities/forms/update-form.php:176 +#: inc/abilities/forms/create-form.php:177 +#: inc/abilities/forms/update-form.php:180 msgid "Recipient email. Supports smart tags like {admin_email}." msgstr "Email odbiorcy. Obsługuje inteligentne tagi, takie jak {admin_email}." -#: inc/abilities/forms/create-form.php:178 -#: inc/abilities/forms/update-form.php:181 +#: inc/abilities/forms/create-form.php:182 +#: inc/abilities/forms/update-form.php:185 msgid "Sender name. Supports smart tags like {site_title}." msgstr "Nazwa nadawcy. Obsługuje inteligentne tagi, takie jak {site_title}." -#: inc/abilities/forms/create-form.php:185 -#: inc/abilities/forms/update-form.php:188 +#: inc/abilities/forms/create-form.php:189 +#: inc/abilities/forms/update-form.php:192 msgid "Email subject. Supports smart tags like {form_title}." msgstr "Temat wiadomości e-mail. Obsługuje inteligentne tagi, takie jak {form_title}." -#: inc/abilities/forms/create-form.php:189 -#: inc/abilities/forms/update-form.php:192 +#: inc/abilities/forms/create-form.php:193 +#: inc/abilities/forms/update-form.php:196 msgid "Email body. Supports smart tags like {all_data}." msgstr "Treść e-maila. Obsługuje inteligentne tagi, takie jak {all_data}." -#: inc/abilities/forms/create-form.php:195 -#: inc/abilities/forms/update-form.php:198 +#: inc/abilities/forms/create-form.php:199 +#: inc/abilities/forms/update-form.php:202 msgid "Form submission restrictions and scheduling." msgstr "Ograniczenia i harmonogram przesyłania formularzy." -#: inc/abilities/forms/create-form.php:199 -#: inc/abilities/forms/update-form.php:202 +#: inc/abilities/forms/create-form.php:203 +#: inc/abilities/forms/update-form.php:206 msgid "Enable entry limit restriction." msgstr "Włącz ograniczenie limitu wejść." -#: inc/abilities/forms/create-form.php:203 -#: inc/abilities/forms/update-form.php:206 +#: inc/abilities/forms/create-form.php:207 +#: inc/abilities/forms/update-form.php:210 msgid "Maximum number of entries allowed (0 = unlimited)." msgstr "Maksymalna liczba dozwolonych wpisów (0 = nieograniczona)." -#: inc/abilities/forms/create-form.php:207 -#: inc/abilities/forms/update-form.php:210 +#: inc/abilities/forms/create-form.php:211 +#: inc/abilities/forms/update-form.php:214 msgid "Expiry date (YYYY-MM-DD)." msgstr "Data ważności (RRRR-MM-DD)." -#: inc/abilities/forms/create-form.php:217 -#: inc/abilities/forms/update-form.php:220 +#: inc/abilities/forms/create-form.php:221 +#: inc/abilities/forms/update-form.php:224 msgid "Message shown when form is closed." msgstr "Komunikat wyświetlany po zamknięciu formularza." -#: inc/abilities/forms/create-form.php:221 -#: inc/abilities/forms/update-form.php:224 +#: inc/abilities/forms/create-form.php:225 +#: inc/abilities/forms/update-form.php:228 msgid "Enable form scheduling." msgstr "Włącz planowanie formularza." -#: inc/abilities/forms/create-form.php:225 -#: inc/abilities/forms/update-form.php:228 +#: inc/abilities/forms/create-form.php:229 +#: inc/abilities/forms/update-form.php:232 msgid "Schedule start date (YYYY-MM-DD)." msgstr "Data rozpoczęcia harmonogramu (RRRR-MM-DD)." -#: inc/abilities/forms/create-form.php:239 -#: inc/abilities/forms/update-form.php:242 +#: inc/abilities/forms/create-form.php:243 +#: inc/abilities/forms/update-form.php:246 msgid "Custom CSS for the form." msgstr "Niestandardowy CSS dla formularza." @@ -17333,37 +17335,37 @@ msgstr "Wybierz to, aby utworzyć quiz z punktowanymi pytaniami i ocenianymi wyn msgid "Survey Reports" msgstr "Raporty z ankiet" -#: inc/frontend-assets.php:304 +#: inc/frontend-assets.php:311 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 1" msgstr "Nagłówek 1" -#: inc/frontend-assets.php:305 +#: inc/frontend-assets.php:312 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 2" msgstr "Nagłówek 2" -#: inc/frontend-assets.php:306 +#: inc/frontend-assets.php:313 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 3" msgstr "Nagłówek 3" -#: inc/frontend-assets.php:307 +#: inc/frontend-assets.php:314 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 4" msgstr "Nagłówek 4" -#: inc/frontend-assets.php:308 +#: inc/frontend-assets.php:315 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 5" msgstr "Nagłówek 5" -#: inc/frontend-assets.php:309 +#: inc/frontend-assets.php:316 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 6" @@ -17510,6 +17512,9 @@ msgstr "Następny: %s" msgid "Cancelled %s" msgstr "Anulowano %s" +#: inc/page-builders/bricks/elements/payment-history-widget.php:51 +#: inc/page-builders/elementor/payment-history-widget.php:42 +#: inc/page-builders/elementor/payment-history-widget.php:91 #: inc/payments/payment-history-shortcode.php:499 msgid "Payment History" msgstr "Historia płatności" @@ -18829,6 +18834,21 @@ msgstr "Wybierz dokładnie, które pola pojawią się jako kolumny." msgid "Restrict who can view entries by user role." msgstr "Ogranicz, kto może przeglądać wpisy według roli użytkownika." +#: inc/abilities/forms/create-form.php:159 +#: inc/abilities/forms/update-form.php:162 +msgid "When true, SureForms does not enqueue its default frontend styles for this form, so the active theme styling applies instead." +msgstr "Gdy jest prawdziwe, SureForms nie dodaje do kolejki swoich domyślnych stylów frontendowych dla tego formularza, więc zamiast tego stosowane jest stylowanie aktywnego motywu." + +#: inc/page-builders/bricks/elements/payment-history-widget.php:78 +#: inc/page-builders/elementor/payment-history-widget.php:98 +msgid "Items per page" +msgstr "Elementy na stronę" + +#: inc/page-builders/bricks/elements/payment-history-widget.php:86 +#: inc/page-builders/elementor/payment-history-widget.php:108 +msgid "Show Subscriptions" +msgstr "Pokaż subskrypcje" + #: inc/post-types.php:226 msgctxt "post type general name" msgid "Forms" @@ -19253,34 +19273,34 @@ msgctxt "block keyword" msgid "color" msgstr "kolor" -#: inc/frontend-assets.php:303 +#: inc/frontend-assets.php:310 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgctxt "Quill heading picker: default paragraph style" msgid "Normal" msgstr "Normalny" -#: inc/frontend-assets.php:310 +#: inc/frontend-assets.php:317 msgctxt "Quill link tooltip label" msgid "Visit URL:" msgstr "Odwiedź URL:" -#: inc/frontend-assets.php:311 +#: inc/frontend-assets.php:318 msgctxt "Quill link tooltip label" msgid "Enter link:" msgstr "Wprowadź link:" -#: inc/frontend-assets.php:312 +#: inc/frontend-assets.php:319 msgctxt "Quill link tooltip action" msgid "Edit" msgstr "Edytuj" -#: inc/frontend-assets.php:313 +#: inc/frontend-assets.php:320 msgctxt "Quill link tooltip action" msgid "Save" msgstr "Zapisz" -#: inc/frontend-assets.php:314 +#: inc/frontend-assets.php:321 msgctxt "Quill link tooltip action" msgid "Remove" msgstr "Usuń" diff --git a/languages/sureforms-pt_PT.mo b/languages/sureforms-pt_PT.mo index 04f537b12381d7a4ded7c7d712adab6172bd0fe8..00d79910a1007753894d9b61ffa91b4a561455b1 100644 GIT binary patch delta 70960 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 diff --git a/languages/sureforms-pt_PT.po b/languages/sureforms-pt_PT.po index b7de1c3d2..e9ab1e785 100644 --- a/languages/sureforms-pt_PT.po +++ b/languages/sureforms-pt_PT.po @@ -20,7 +20,7 @@ msgstr "" #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 -#: inc/page-builders/bricks/service-provider.php:55 +#: inc/page-builders/bricks/service-provider.php:56 #: inc/page-builders/elementor/form-widget.php:144 #: inc/page-builders/elementor/form-widget.php:301 #: inc/page-builders/elementor/service-provider.php:74 @@ -562,7 +562,7 @@ msgid "I consent to have this website store my submitted information so they can msgstr "Eu consinto que este site armazene as informações que enviei para que possam responder à minha consulta." #: inc/fields/inlinebutton-markup.php:184 -#: inc/generate-form-markup.php:640 +#: inc/generate-form-markup.php:654 msgid "Please verify that you are not a robot." msgstr "Por favor, verifique se você não é um robô." @@ -614,7 +614,7 @@ msgstr "Os dados do formulário não foram encontrados." msgid "Sorry, you are not allowed to view the form." msgstr "Desculpe, você não tem permissão para visualizar o formulário." -#: inc/generate-form-markup.php:820 +#: inc/generate-form-markup.php:834 msgid "There was an error trying to submit your form. Please try again." msgstr "Houve um erro ao tentar enviar seu formulário. Por favor, tente novamente." @@ -699,11 +699,13 @@ msgid "Form Title" msgstr "Título do Formulário" #: inc/page-builders/elementor/form-widget.php:320 +#: inc/page-builders/elementor/payment-history-widget.php:110 #: assets/build/blocks.js:172 msgid "Show" msgstr "Mostrar" #: inc/page-builders/elementor/form-widget.php:321 +#: inc/page-builders/elementor/payment-history-widget.php:111 #: assets/build/blocks.js:172 msgid "Hide" msgstr "Esconder" @@ -797,7 +799,7 @@ msgstr "Formulário atualizado." #: inc/global-settings/global-settings-defaults.php:166 #: inc/migrator/importers/cf7-importer.php:168 #: inc/migrator/importers/ninja-importer.php:1152 -#: inc/post-types.php:998 +#: inc/post-types.php:1004 msgid "Admin Notification Email" msgstr "Email de Notificação do Administrador" @@ -805,7 +807,7 @@ msgstr "Email de Notificação do Administrador" #. translators: %s: {form_title} smart tag placeholder. #: inc/global-settings/global-settings-defaults.php:173 #: inc/global-settings/global-settings.php:588 -#: inc/post-types.php:1005 +#: inc/post-types.php:1011 #: assets/build/settings.js:172 #, php-format,js-format msgid "New Form Submission - %s" @@ -8331,7 +8333,7 @@ msgstr "Alterar Formulário" #: inc/blocks/sform/block.php:42 #: inc/page-builders/bricks/elements/form-widget.php:508 #: inc/page-builders/elementor/form-widget.php:827 -#: inc/post-types.php:1318 +#: inc/post-types.php:1324 #: assets/build/blocks.js:172 msgid "This form has been deleted or is unavailable." msgstr "Este formulário foi excluído ou está indisponível." @@ -9327,7 +9329,7 @@ msgstr "Anterior" #: inc/global-settings/global-settings.php:530 #: inc/migrator/base-migrator.php:548 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 #: assets/build/formEditor.js:172 msgid "Thank you" msgstr "Obrigado" @@ -13462,7 +13464,7 @@ msgstr "Edite este formulário" #: inc/global-settings/global-settings.php:531 #: inc/migrator/base-migrator.php:549 -#: inc/post-types.php:1081 +#: inc/post-types.php:1087 msgid "Your form has been submitted successfully. We'll review your details and get back to you soon." msgstr "Seu formulário foi enviado com sucesso. Analisaremos seus dados e entraremos em contato em breve." @@ -15605,23 +15607,23 @@ msgstr "Metadados opcionais do formulário, incluindo configurações de confirm msgid "Message displayed after successful submission." msgstr "Mensagem exibida após o envio bem-sucedido." -#: inc/abilities/forms/create-form.php:245 +#: inc/abilities/forms/create-form.php:249 msgid "Form publish status." msgstr "Status de publicação do formulário." -#: inc/abilities/forms/create-form.php:290 +#: inc/abilities/forms/create-form.php:294 msgid "Form title is required." msgstr "O título do formulário é obrigatório." -#: inc/abilities/forms/create-form.php:298 +#: inc/abilities/forms/create-form.php:302 msgid "At least one form field is required." msgstr "Pelo menos um campo de formulário é necessário." -#: inc/abilities/forms/create-form.php:326 +#: inc/abilities/forms/create-form.php:330 msgid "Failed to generate form fields from the provided data." msgstr "Falha ao gerar campos do formulário a partir dos dados fornecidos." -#: inc/abilities/forms/create-form.php:383 +#: inc/abilities/forms/create-form.php:387 msgid "Failed to create the form." msgstr "Falha ao criar o formulário." @@ -15641,7 +15643,7 @@ msgstr "Se verdadeiro, exclui permanentemente o formulário. Caso contrário, mo #: inc/abilities/forms/get-form-stats.php:113 #: inc/abilities/forms/get-form.php:110 #: inc/abilities/forms/get-shortcode.php:95 -#: inc/abilities/forms/update-form.php:287 +#: inc/abilities/forms/update-form.php:291 msgid "Form not found." msgstr "Formulário não encontrado." @@ -15841,8 +15843,8 @@ msgstr "Campo de texto" #: inc/form-restriction.php:183 #: inc/form-restriction.php:184 -#: inc/post-types.php:1224 -#: inc/post-types.php:1264 +#: inc/post-types.php:1230 +#: inc/post-types.php:1270 msgid "This form is not yet available. Check back after the scheduled start time." msgstr "Este formulário ainda não está disponível. Verifique novamente após o horário de início programado." @@ -16126,8 +16128,8 @@ msgstr "Não foi possível excluir o webhook." msgid "Unable to connect to Stripe." msgstr "Não foi possível conectar ao Stripe." -#: inc/post-types.php:1225 -#: inc/post-types.php:1265 +#: inc/post-types.php:1231 +#: inc/post-types.php:1271 msgid "This form is closed. The submission period has ended." msgstr "Este formulário está fechado. O período de envio terminou." @@ -16662,78 +16664,78 @@ msgstr "Cor do texto em fundos primários como hex (por exemplo, #FFFFFF)." msgid "Spacing between fields." msgstr "Espaçamento entre campos." -#: inc/abilities/forms/create-form.php:161 -#: inc/abilities/forms/update-form.php:164 +#: inc/abilities/forms/create-form.php:165 +#: inc/abilities/forms/update-form.php:168 msgid "Email notification settings for the first notification. Merges with existing." msgstr "Configurações de notificação por email para a primeira notificação. Mescla com as existentes." -#: inc/abilities/forms/create-form.php:165 -#: inc/abilities/forms/update-form.php:168 +#: inc/abilities/forms/create-form.php:169 +#: inc/abilities/forms/update-form.php:172 msgid "Enable or disable the notification." msgstr "Ativar ou desativar a notificação." -#: inc/abilities/forms/create-form.php:169 -#: inc/abilities/forms/update-form.php:172 +#: inc/abilities/forms/create-form.php:173 +#: inc/abilities/forms/update-form.php:176 msgid "Notification name." msgstr "Nome da notificação." -#: inc/abilities/forms/create-form.php:173 -#: inc/abilities/forms/update-form.php:176 +#: inc/abilities/forms/create-form.php:177 +#: inc/abilities/forms/update-form.php:180 msgid "Recipient email. Supports smart tags like {admin_email}." msgstr "Email do destinatário. Suporta tags inteligentes como {admin_email}." -#: inc/abilities/forms/create-form.php:178 -#: inc/abilities/forms/update-form.php:181 +#: inc/abilities/forms/create-form.php:182 +#: inc/abilities/forms/update-form.php:185 msgid "Sender name. Supports smart tags like {site_title}." msgstr "Nome do remetente. Suporta tags inteligentes como {site_title}." -#: inc/abilities/forms/create-form.php:185 -#: inc/abilities/forms/update-form.php:188 +#: inc/abilities/forms/create-form.php:189 +#: inc/abilities/forms/update-form.php:192 msgid "Email subject. Supports smart tags like {form_title}." msgstr "Assunto do email. Suporta tags inteligentes como {form_title}." -#: inc/abilities/forms/create-form.php:189 -#: inc/abilities/forms/update-form.php:192 +#: inc/abilities/forms/create-form.php:193 +#: inc/abilities/forms/update-form.php:196 msgid "Email body. Supports smart tags like {all_data}." msgstr "Corpo do email. Suporta tags inteligentes como {all_data}." -#: inc/abilities/forms/create-form.php:195 -#: inc/abilities/forms/update-form.php:198 +#: inc/abilities/forms/create-form.php:199 +#: inc/abilities/forms/update-form.php:202 msgid "Form submission restrictions and scheduling." msgstr "Restrições e agendamento de envio de formulários." -#: inc/abilities/forms/create-form.php:199 -#: inc/abilities/forms/update-form.php:202 +#: inc/abilities/forms/create-form.php:203 +#: inc/abilities/forms/update-form.php:206 msgid "Enable entry limit restriction." msgstr "Ativar restrição de limite de entrada." -#: inc/abilities/forms/create-form.php:203 -#: inc/abilities/forms/update-form.php:206 +#: inc/abilities/forms/create-form.php:207 +#: inc/abilities/forms/update-form.php:210 msgid "Maximum number of entries allowed (0 = unlimited)." msgstr "Número máximo de entradas permitidas (0 = ilimitado)." -#: inc/abilities/forms/create-form.php:207 -#: inc/abilities/forms/update-form.php:210 +#: inc/abilities/forms/create-form.php:211 +#: inc/abilities/forms/update-form.php:214 msgid "Expiry date (YYYY-MM-DD)." msgstr "Data de validade (AAAA-MM-DD)." -#: inc/abilities/forms/create-form.php:217 -#: inc/abilities/forms/update-form.php:220 +#: inc/abilities/forms/create-form.php:221 +#: inc/abilities/forms/update-form.php:224 msgid "Message shown when form is closed." msgstr "Mensagem exibida quando o formulário é fechado." -#: inc/abilities/forms/create-form.php:221 -#: inc/abilities/forms/update-form.php:224 +#: inc/abilities/forms/create-form.php:225 +#: inc/abilities/forms/update-form.php:228 msgid "Enable form scheduling." msgstr "Ativar agendamento de formulário." -#: inc/abilities/forms/create-form.php:225 -#: inc/abilities/forms/update-form.php:228 +#: inc/abilities/forms/create-form.php:229 +#: inc/abilities/forms/update-form.php:232 msgid "Schedule start date (YYYY-MM-DD)." msgstr "Data de início agendada (AAAA-MM-DD)." -#: inc/abilities/forms/create-form.php:239 -#: inc/abilities/forms/update-form.php:242 +#: inc/abilities/forms/create-form.php:243 +#: inc/abilities/forms/update-form.php:246 msgid "Custom CSS for the form." msgstr "CSS personalizado para o formulário." @@ -17333,37 +17335,37 @@ msgstr "Selecione isto para criar um questionário com perguntas pontuadas e res msgid "Survey Reports" msgstr "Relatórios de Pesquisa" -#: inc/frontend-assets.php:304 +#: inc/frontend-assets.php:311 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 1" msgstr "Título 1" -#: inc/frontend-assets.php:305 +#: inc/frontend-assets.php:312 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 2" msgstr "Título 2" -#: inc/frontend-assets.php:306 +#: inc/frontend-assets.php:313 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 3" msgstr "Título 3" -#: inc/frontend-assets.php:307 +#: inc/frontend-assets.php:314 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 4" msgstr "Título 4" -#: inc/frontend-assets.php:308 +#: inc/frontend-assets.php:315 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 5" msgstr "Título 5" -#: inc/frontend-assets.php:309 +#: inc/frontend-assets.php:316 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Heading 6" @@ -17510,6 +17512,9 @@ msgstr "Próximo: %s" msgid "Cancelled %s" msgstr "Cancelado %s" +#: inc/page-builders/bricks/elements/payment-history-widget.php:51 +#: inc/page-builders/elementor/payment-history-widget.php:42 +#: inc/page-builders/elementor/payment-history-widget.php:91 #: inc/payments/payment-history-shortcode.php:499 msgid "Payment History" msgstr "Histórico de Pagamentos" @@ -18829,6 +18834,21 @@ msgstr "Escolha exatamente quais campos aparecem como colunas." msgid "Restrict who can view entries by user role." msgstr "Restrinja quem pode ver as entradas por função de usuário." +#: inc/abilities/forms/create-form.php:159 +#: inc/abilities/forms/update-form.php:162 +msgid "When true, SureForms does not enqueue its default frontend styles for this form, so the active theme styling applies instead." +msgstr "Quando verdadeiro, o SureForms não enfileira seus estilos de frontend padrão para este formulário, então o estilo do tema ativo é aplicado em vez disso." + +#: inc/page-builders/bricks/elements/payment-history-widget.php:78 +#: inc/page-builders/elementor/payment-history-widget.php:98 +msgid "Items per page" +msgstr "Itens por página" + +#: inc/page-builders/bricks/elements/payment-history-widget.php:86 +#: inc/page-builders/elementor/payment-history-widget.php:108 +msgid "Show Subscriptions" +msgstr "Mostrar Assinaturas" + #: inc/post-types.php:226 msgctxt "post type general name" msgid "Forms" @@ -19253,34 +19273,34 @@ msgctxt "block keyword" msgid "color" msgstr "cor" -#: inc/frontend-assets.php:303 +#: inc/frontend-assets.php:310 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgctxt "Quill heading picker: default paragraph style" msgid "Normal" msgstr "Normal" -#: inc/frontend-assets.php:310 +#: inc/frontend-assets.php:317 msgctxt "Quill link tooltip label" msgid "Visit URL:" msgstr "Visitar URL:" -#: inc/frontend-assets.php:311 +#: inc/frontend-assets.php:318 msgctxt "Quill link tooltip label" msgid "Enter link:" msgstr "Insira o link:" -#: inc/frontend-assets.php:312 +#: inc/frontend-assets.php:319 msgctxt "Quill link tooltip action" msgid "Edit" msgstr "Editar" -#: inc/frontend-assets.php:313 +#: inc/frontend-assets.php:320 msgctxt "Quill link tooltip action" msgid "Save" msgstr "Salvar" -#: inc/frontend-assets.php:314 +#: inc/frontend-assets.php:321 msgctxt "Quill link tooltip action" msgid "Remove" msgstr "Remover" From ba0d05f9e39d8dafeb04446f740ada58f243440e Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 20 Jul 2026 14:33:27 +0530 Subject: [PATCH 14/19] chore: set 2.12.2 release date to 20th July 2026 Update the 2.12.2 changelog date in readme.txt and README.md. --- README.md | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d9f40d44..877723e28 100644 --- a/README.md +++ b/README.md @@ -458,7 +458,7 @@ Yes. SureForms Business includes fully functional user registration forms and lo You can report security issues through our [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/). We collaborate with Patchstack to provide opportunities for researchers to report vulnerabilities. The Patchstack team will help validate, triage, and handle any reported security issues. ## Changelog ## -### 2.12.2 - 16th July 2026 ### +### 2.12.2 - 20th July 2026 ### * New: Added a Payment History widget for Elementor and Bricks. * Fix: Custom CSS now applies correctly on the single form view. * Fix: The SureForms Description panel now opens by default in the form editor so field descriptions are easier to find. diff --git a/readme.txt b/readme.txt index cef13c39e..6f8c19843 100644 --- a/readme.txt +++ b/readme.txt @@ -458,7 +458,7 @@ Yes. SureForms Business includes fully functional user registration forms and lo You can report security issues through our [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/). We collaborate with Patchstack to provide opportunities for researchers to report vulnerabilities. The Patchstack team will help validate, triage, and handle any reported security issues. == Changelog == -= 2.12.2 - 16th July 2026 = += 2.12.2 - 20th July 2026 = * New: Added a Payment History widget for Elementor and Bricks. * Fix: Custom CSS now applies correctly on the single form view. * Fix: The SureForms Description panel now opens by default in the form editor so field descriptions are easier to find. From c8145b5f740fb3ff2dd93b7e8b482054c645558d Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 20 Jul 2026 14:42:34 +0530 Subject: [PATCH 15/19] chore: update 2.12.2 changelog in readme - Set the 2.12.2 release date to 20th July 2026. - Remove the 'Description panel opens by default' fix line from the 2.12.2 changelog. Applied to both readme.txt and README.md. --- README.md | 1 - readme.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/README.md b/README.md index 877723e28..fd78d40e8 100644 --- a/README.md +++ b/README.md @@ -461,7 +461,6 @@ You can report security issues through our [Bug Bounty Program](https://brainsto ### 2.12.2 - 20th July 2026 ### * New: Added a Payment History widget for Elementor and Bricks. * Fix: Custom CSS now applies correctly on the single form view. -* Fix: The SureForms Description panel now opens by default in the form editor so field descriptions are easier to find. ### 2.12.1 - 8th July 2026 ### * New: Added an AI-powered quick draft flow on the dashboard to generate forms faster, with built-in usage tracking. * Improvement: Email fields now enforce standard RFC 5321 length limits (a 64-character local part and 255-character domain) to prevent oversized submissions, with a filter to customize the limits. diff --git a/readme.txt b/readme.txt index 6f8c19843..984714789 100644 --- a/readme.txt +++ b/readme.txt @@ -461,7 +461,6 @@ You can report security issues through our [Bug Bounty Program](https://brainsto = 2.12.2 - 20th July 2026 = * New: Added a Payment History widget for Elementor and Bricks. * Fix: Custom CSS now applies correctly on the single form view. -* Fix: The SureForms Description panel now opens by default in the form editor so field descriptions are easier to find. = 2.12.1 - 8th July 2026 = * New: Added an AI-powered quick draft flow on the dashboard to generate forms faster, with built-in usage tracking. * Improvement: Email fields now enforce standard RFC 5321 length limits (a 64-character local part and 255-character domain) to prevent oversized submissions, with a filter to customize the limits. From de0b1bac636998ddb2b379bce42312a58f6e3ce6 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 20 Jul 2026 15:11:32 +0530 Subject: [PATCH 16/19] ci: pin Node 24.16.0 in deploy/asset workflows so npm ci matches the lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WordPress.org deploy (New tag) started failing after #80ececd25 switched these workflows to 'npm ci'. Neither sets up Node, so they ran the runner's default (older) npm, which re-resolves the tree and reports 'Missing zod@4.4.3 from lock file'. With Node 24.16.0 (the repo's Volta pin, npm 11 — same as every other workflow and the lockfile generator), 'npm ci' is up to date and the deploy passes. No lock churn, no --force. --- .github/workflows/push-asset-readme-update.yml | 4 ++++ .github/workflows/push-to-deploy.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/push-asset-readme-update.yml b/.github/workflows/push-asset-readme-update.yml index fed4455c2..de4769aaf 100644 --- a/.github/workflows/push-asset-readme-update.yml +++ b/.github/workflows/push-asset-readme-update.yml @@ -13,6 +13,10 @@ jobs: environment: production steps: - uses: actions/checkout@master + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.16.0' - name: Build run: | npm ci diff --git a/.github/workflows/push-to-deploy.yml b/.github/workflows/push-to-deploy.yml index 8e518fe41..e10b21ba8 100644 --- a/.github/workflows/push-to-deploy.yml +++ b/.github/workflows/push-to-deploy.yml @@ -16,6 +16,10 @@ jobs: echo "::error::Tag $GITHUB_REF_NAME ($GITHUB_SHA) is not reachable from master. Refusing to deploy." exit 1 fi + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.16.0' - name: Build run: | npm ci From ea86cfa34d7ef0603e3eae53db29c2d2d5ee3438 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 20 Jul 2026 15:30:17 +0530 Subject: [PATCH 17/19] ci: revert deploy/asset workflows to npm install --force Revert the npm ci change from #80ececd25. npm ci hard-fails on the wp.org deploy runner ('Missing: zod@4.4.3 from lock file') because the runner's npm resolves the tree differently than the lockfile generator, blocking the release. npm install --force (the long-standing, proven behavior) installs successfully. Also drops the Node pin added in #2962 to restore the exact prior working state. --- .github/workflows/push-asset-readme-update.yml | 6 +----- .github/workflows/push-to-deploy.yml | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/push-asset-readme-update.yml b/.github/workflows/push-asset-readme-update.yml index de4769aaf..8e5de8169 100644 --- a/.github/workflows/push-asset-readme-update.yml +++ b/.github/workflows/push-asset-readme-update.yml @@ -13,13 +13,9 @@ jobs: environment: production steps: - uses: actions/checkout@master - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - name: Build run: | - npm ci + npm install --force npm run build - name: WordPress.org plugin asset/readme update uses: 10up/action-wordpress-plugin-asset-update@stable diff --git a/.github/workflows/push-to-deploy.yml b/.github/workflows/push-to-deploy.yml index e10b21ba8..87c43d058 100644 --- a/.github/workflows/push-to-deploy.yml +++ b/.github/workflows/push-to-deploy.yml @@ -16,13 +16,9 @@ jobs: echo "::error::Tag $GITHUB_REF_NAME ($GITHUB_SHA) is not reachable from master. Refusing to deploy." exit 1 fi - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - name: Build run: | - npm ci + npm install --force npm run build - name: WordPress Plugin Deploy uses: 10up/action-wordpress-plugin-deploy@master From 229740b73afb574a45f743ed9b1df3f539851eb1 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 20 Jul 2026 16:54:47 +0530 Subject: [PATCH 18/19] ci: use npm ci with Node 24.16.0 in deploy/asset workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the npm install --force fallback (#2963) back to npm ci, pinning Node 24.16.0 (the repo's Volta version, npm 11 — same as the lockfile generator) via setup-node so npm ci resolves the tree consistently and stays reproducible. Restores the #2962 approach. --- .github/workflows/push-asset-readme-update.yml | 6 +++++- .github/workflows/push-to-deploy.yml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push-asset-readme-update.yml b/.github/workflows/push-asset-readme-update.yml index 8e5de8169..de4769aaf 100644 --- a/.github/workflows/push-asset-readme-update.yml +++ b/.github/workflows/push-asset-readme-update.yml @@ -13,9 +13,13 @@ jobs: environment: production steps: - uses: actions/checkout@master + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.16.0' - name: Build run: | - npm install --force + npm ci npm run build - name: WordPress.org plugin asset/readme update uses: 10up/action-wordpress-plugin-asset-update@stable diff --git a/.github/workflows/push-to-deploy.yml b/.github/workflows/push-to-deploy.yml index 87c43d058..e10b21ba8 100644 --- a/.github/workflows/push-to-deploy.yml +++ b/.github/workflows/push-to-deploy.yml @@ -16,9 +16,13 @@ jobs: echo "::error::Tag $GITHUB_REF_NAME ($GITHUB_SHA) is not reachable from master. Refusing to deploy." exit 1 fi + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.16.0' - name: Build run: | - npm install --force + npm ci npm run build - name: WordPress Plugin Deploy uses: 10up/action-wordpress-plugin-deploy@master From d80cf1a699e917ab452d0131391ab6a84d9bbf0e Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 20 Jul 2026 17:39:51 +0530 Subject: [PATCH 19/19] chore: strip internal-only paths from public mirror sync --- .claude/agents/code-simplifier.md | 48 - .claude/agents/srfm-block-checker.md | 60 - .claude/commands/sureforms:investigate.md | 67 - .claude/commands/sureforms:sync-public.md | 299 ---- .claude/commands/sureforms:update-readme.md | 327 ----- .claude/commands/sureforms:version-bump.md | 204 --- .claude/commands/sync-release-branches.md | 96 -- .claude/settings.json | 73 - .../workflows/push-asset-readme-update.yml | 28 - .github/workflows/push-to-deploy.yml | 31 - .github/workflows/release-pr-template.yml | 41 - .github/workflows/release-tag-draft.yml | 83 -- .github/workflows/update-translations.yml | 205 --- .scripts/git-hooks/pre-commit | 106 -- ARCHITECTURE.md | 355 ----- CLAUDE.md | 165 --- COMPREHENSIVE_ANALYSIS.md | 304 ----- PRODUCT_ANALYSIS.md | 251 ---- TECHNICAL_OVERVIEW.md | 393 ------ bin/build-zip.sh | 47 - bin/checkout-and-build | 21 - bin/i18n.sh | 75 -- docs/01-getting-started/GETTING-STARTED.md | 647 --------- docs/04-backend/DATABASE-SCHEMA.md | 962 ------------- docs/04-backend/HOOKS-REFERENCE.md | 1055 --------------- docs/05-frontend/FRONTEND-GUIDE.md | 1197 ----------------- docs/FORM_MIGRATION_OVERVIEW.md | 77 -- docs/FORM_MIGRATION_PLAN.md | 458 ------- docs/wiki/AI-Form-Builder.md | 103 -- docs/wiki/Admin-Dashboard.md | 121 -- docs/wiki/Architecture-Overview.md | 294 ---- docs/wiki/Block-Editor-Controls.md | 109 -- docs/wiki/Changelog.md | 39 - docs/wiki/Contributing-Guide.md | 205 --- docs/wiki/Data-Export-Import.md | 120 -- docs/wiki/Database-Schema.md | 173 --- docs/wiki/Deployment-Guide.md | 109 -- docs/wiki/Email-Notifications.md | 86 -- docs/wiki/Environment-Configuration.md | 197 --- docs/wiki/Form-Fields-Architecture.md | 137 -- docs/wiki/Form-Submission-Flow.md | 191 --- docs/wiki/Frontend-Assets.md | 122 -- docs/wiki/Getting-Started.md | 149 -- docs/wiki/Gutenberg-Blocks.md | 129 -- docs/wiki/Home.md | 58 - docs/wiki/Payment-Integration.md | 127 -- docs/wiki/REST-API-Reference.md | 114 -- docs/wiki/State-Management.md | 109 -- docs/wiki/Testing-Guide.md | 172 --- docs/wiki/Troubleshooting-FAQ.md | 232 ---- docs/wiki/Using-WPML-With-SureForms.md | 243 ---- docs/wiki/WordPress-Hooks-Reference.md | 152 --- docs/wiki/_Footer.md | 3 - docs/wiki/_Sidebar.md | 36 - inc/abilities/CLAUDE.md | 66 - internal-docs/README.md | 160 --- internal-docs/ai-agent-guide.md | 459 ------- internal-docs/apis.md | 418 ------ internal-docs/architecture.md | 329 ----- internal-docs/codebase-map.md | 303 ----- internal-docs/coding-standards.md | 312 ----- internal-docs/faq.md | 1003 -------------- internal-docs/glossary.md | 723 ---------- internal-docs/maintenance.md | 842 ------------ internal-docs/onboarding.md | 1086 --------------- internal-docs/product-vision.md | 688 ---------- internal-docs/troubleshooting.md | 1031 -------------- internal-docs/ui-and-copy.md | 1077 --------------- tests/play/specs/CLAUDE.md | 324 ----- tests/play/specs/TODO.md | 244 ---- 70 files changed, 20270 deletions(-) delete mode 100644 .claude/agents/code-simplifier.md delete mode 100644 .claude/agents/srfm-block-checker.md delete mode 100644 .claude/commands/sureforms:investigate.md delete mode 100644 .claude/commands/sureforms:sync-public.md delete mode 100644 .claude/commands/sureforms:update-readme.md delete mode 100644 .claude/commands/sureforms:version-bump.md delete mode 100644 .claude/commands/sync-release-branches.md delete mode 100644 .claude/settings.json delete mode 100644 .github/workflows/push-asset-readme-update.yml delete mode 100644 .github/workflows/push-to-deploy.yml delete mode 100644 .github/workflows/release-pr-template.yml delete mode 100644 .github/workflows/release-tag-draft.yml delete mode 100644 .github/workflows/update-translations.yml delete mode 100644 .scripts/git-hooks/pre-commit delete mode 100644 ARCHITECTURE.md delete mode 100644 CLAUDE.md delete mode 100644 COMPREHENSIVE_ANALYSIS.md delete mode 100644 PRODUCT_ANALYSIS.md delete mode 100644 TECHNICAL_OVERVIEW.md delete mode 100644 bin/build-zip.sh delete mode 100755 bin/checkout-and-build delete mode 100644 bin/i18n.sh delete mode 100644 docs/01-getting-started/GETTING-STARTED.md delete mode 100644 docs/04-backend/DATABASE-SCHEMA.md delete mode 100644 docs/04-backend/HOOKS-REFERENCE.md delete mode 100644 docs/05-frontend/FRONTEND-GUIDE.md delete mode 100644 docs/FORM_MIGRATION_OVERVIEW.md delete mode 100644 docs/FORM_MIGRATION_PLAN.md delete mode 100644 docs/wiki/AI-Form-Builder.md delete mode 100644 docs/wiki/Admin-Dashboard.md delete mode 100644 docs/wiki/Architecture-Overview.md delete mode 100644 docs/wiki/Block-Editor-Controls.md delete mode 100644 docs/wiki/Changelog.md delete mode 100644 docs/wiki/Contributing-Guide.md delete mode 100644 docs/wiki/Data-Export-Import.md delete mode 100644 docs/wiki/Database-Schema.md delete mode 100644 docs/wiki/Deployment-Guide.md delete mode 100644 docs/wiki/Email-Notifications.md delete mode 100644 docs/wiki/Environment-Configuration.md delete mode 100644 docs/wiki/Form-Fields-Architecture.md delete mode 100644 docs/wiki/Form-Submission-Flow.md delete mode 100644 docs/wiki/Frontend-Assets.md delete mode 100644 docs/wiki/Getting-Started.md delete mode 100644 docs/wiki/Gutenberg-Blocks.md delete mode 100644 docs/wiki/Home.md delete mode 100644 docs/wiki/Payment-Integration.md delete mode 100644 docs/wiki/REST-API-Reference.md delete mode 100644 docs/wiki/State-Management.md delete mode 100644 docs/wiki/Testing-Guide.md delete mode 100644 docs/wiki/Troubleshooting-FAQ.md delete mode 100644 docs/wiki/Using-WPML-With-SureForms.md delete mode 100644 docs/wiki/WordPress-Hooks-Reference.md delete mode 100644 docs/wiki/_Footer.md delete mode 100644 docs/wiki/_Sidebar.md delete mode 100644 inc/abilities/CLAUDE.md delete mode 100644 internal-docs/README.md delete mode 100644 internal-docs/ai-agent-guide.md delete mode 100644 internal-docs/apis.md delete mode 100644 internal-docs/architecture.md delete mode 100644 internal-docs/codebase-map.md delete mode 100644 internal-docs/coding-standards.md delete mode 100644 internal-docs/faq.md delete mode 100644 internal-docs/glossary.md delete mode 100644 internal-docs/maintenance.md delete mode 100644 internal-docs/onboarding.md delete mode 100644 internal-docs/product-vision.md delete mode 100644 internal-docs/troubleshooting.md delete mode 100644 internal-docs/ui-and-copy.md delete mode 100644 tests/play/specs/CLAUDE.md delete mode 100644 tests/play/specs/TODO.md diff --git a/.claude/agents/code-simplifier.md b/.claude/agents/code-simplifier.md deleted file mode 100644 index 2dfa2039d..000000000 --- a/.claude/agents/code-simplifier.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: code-simplifier -description: Reviews recent code changes and suggests simplifications — reduces complexity, removes dead code, flattens nesting, applies DRY. -tools: Read, Glob, Grep -model: sonnet ---- - -You are a code simplification expert for a WordPress/React plugin. - -## Your Task - -Review the recently changed files and suggest concrete simplifications. Do NOT make changes — only report findings. - -## What to Look For - -### PHP -- Duplicated logic that can be extracted into a shared method -- Deeply nested conditionals that can be flattened with early returns -- Dead code (unused variables, unreachable branches, commented-out code) -- Verbose array/string operations that WordPress has helpers for -- Overly complex conditionals that can be simplified - -### JavaScript/React -- Components doing too much — can they be split? -- Duplicated JSX or logic across components -- State that could be derived instead of stored -- useEffect with dependencies that could be simplified -- Unnecessary re-renders from inline objects/functions in props - -## Output Format - -For each finding: - -``` -### file-path:line-number -**Type:** DRY | Complexity | Dead Code | Optimization -**Current:** Brief description of current code -**Suggestion:** How to simplify (1-2 sentences) -**Impact:** Low | Medium | High -``` - -End with a summary: total findings by type and priority. - -## Rules -- Only report findings with clear, concrete improvements -- No subjective style preferences — only measurable simplifications -- Skip third-party code (`inc/lib/`, `node_modules/`, `vendor/`) -- Be precise with line numbers diff --git a/.claude/agents/srfm-block-checker.md b/.claude/agents/srfm-block-checker.md deleted file mode 100644 index ec7bbf8b9..000000000 --- a/.claude/agents/srfm-block-checker.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: srfm-block-checker -description: Audits SureForms block render files for security issues — checks output escaping, input sanitization, ABSPATH guards, and WPCS compliance. -tools: Read, Glob, Grep -model: sonnet ---- - -You are a WordPress security reviewer specialized in Gutenberg block rendering. - -## Your Task - -Audit SureForms block PHP files for common security and quality issues. - -## Files to Check - -1. All block render files: `inc/blocks/*/block.php` -2. All field markup files: `inc/fields/*.php` -3. The base block class: `inc/blocks/base.php` - -## Checklist for Each File - -For every file, check and report: - -### Security -- [ ] **ABSPATH guard** — File starts with `if ( ! defined( 'ABSPATH' ) ) { exit; }` -- [ ] **Output escaping** — All `echo` statements use `esc_html()`, `esc_attr()`, `wp_kses_post()`, or `wp_kses()`. Flag any raw `echo $var` without escaping (note: `// phpcs:ignore` comments may indicate intentional bypass — flag these for review) -- [ ] **Attribute sanitization** — Block attributes accessed from `$attributes` are sanitized before use -- [ ] **No direct `$_GET`/`$_POST`/`$_REQUEST`** — Block render should never read superglobals directly - -### Quality -- [ ] **`@since` tag present** — Class and methods have `@since` docblock tags -- [ ] **Namespace follows convention** — Uses `SRFM\Inc\Blocks\{BlockName}` pattern -- [ ] **Extends Base** — Block class extends `SRFM\Inc\Blocks\Base` -- [ ] **Return type** — `render()` method returns `string|bool` - -## Output Format - -For each file, output: - -``` -### block-name/block.php -- PASS: ABSPATH guard present -- WARN: Line 33 — raw echo with phpcs:ignore, verify markup() output is pre-escaped -- PASS: No superglobals used -- FAIL: Missing @since tag on render() method -``` - -End with a summary table: - -| Block | Security | Quality | Issues | -|-------|----------|---------|--------| -| input | WARN | FAIL | 2 | -| email | PASS | PASS | 0 | - -## Important - -- Be precise — include line numbers for every issue -- Distinguish FAIL (must fix) from WARN (review needed) -- Do NOT suggest code changes — only report findings -- Check ALL block files, not just a sample diff --git a/.claude/commands/sureforms:investigate.md b/.claude/commands/sureforms:investigate.md deleted file mode 100644 index d171756ec..000000000 --- a/.claude/commands/sureforms:investigate.md +++ /dev/null @@ -1,67 +0,0 @@ -# Investigate Issue - -Investigate a reported issue using parallel subagents for speed. - -## Input - -$ARGUMENTS — GitHub issue URL, Jira ticket ID, or a description of the issue. - -If no arguments provided, stop and ask: "Please provide an issue URL, Jira ticket ID, or describe the issue." - -## Step 1 — Read the Issue - -- If a GitHub URL: use `gh issue view` to read the issue -- If a Jira ticket: use the Jira MCP tools to read the ticket -- If a description: use the provided text directly - -Extract: title, description, steps to reproduce, expected vs actual behavior, screenshots, labels. - -## Step 2 — Classify - -Determine the type: -- **Bug** — something is broken, unexpected behavior, error, regression -- **Feature** — new functionality requested -- **Improvement** — enhancement to existing functionality - -Tell the user: "This looks like a **[type]**. Proceeding with [investigation/planning]." - -## Step 3 — Investigate with Subagents - -Use subagents to parallelize the investigation: - -### For Bugs: -Launch these subagents in parallel: -1. **Code Search Agent** — Search the codebase for files related to the bug (grep for keywords, function names, error messages, related hooks/filters) -2. **Test Coverage Agent** — Check if there are existing tests covering this area, identify gaps -3. **Impact Analysis Agent** — Trace all usages of the affected code to understand blast radius - -Once all agents report back, synthesize findings: -- Present the probable root cause with file path and line number -- Show which other areas might be affected -- Suggest a fix with a clear explanation -- Ask the user: "Should I apply this fix?" -- If yes — apply the fix, run linters, and suggest running relevant tests - -### For Features: -1. Enter plan mode -2. Launch subagents in parallel: - - **Pattern Discovery Agent** — Find similar existing features in the codebase to follow the same patterns - - **Impact Analysis Agent** — Identify all files that will need changes -3. Use findings to outline implementation steps -4. Present the plan for approval - -### For Improvements: -1. Launch subagents in parallel: - - **Code Search Agent** — Read the relevant existing code - - **Impact Analysis Agent** — Trace dependencies to understand what might break -2. Suggest the minimal changes needed -3. Ask the user: "Should I implement these changes?" -4. If yes — apply changes and run linters - -## Rules -- Always use subagents for investigation to maximize speed -- Show your reasoning — which files you checked and why -- Be precise with file paths and line numbers -- For bugs, prioritize finding the root cause over applying a quick patch -- Never make changes without asking first -- After any code change, run the verification checklist from CLAUDE.md diff --git a/.claude/commands/sureforms:sync-public.md b/.claude/commands/sureforms:sync-public.md deleted file mode 100644 index 08f2bba4c..000000000 --- a/.claude/commands/sureforms:sync-public.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -allowed-tools: Bash(git:*), Bash(gh:*), Bash(rm:*), Bash(rmdir:*), Bash(mktemp:*), Bash(echo:*), Bash(cd:*), Bash(test:*) -description: Sync private master to the sureforms-public mirror, stripping internal-only paths in one commit ---- - -# SureForms — Sync to Public Mirror - -Sync `brainstormforce/sureforms@master` (private, this repo) to `brainstormforce/sureforms-public@master` (public WordPress.org-facing mirror), stripping internal-only paths. The sync branch is bulk re-signed (verified signatures) and capped with a merge commit against the mirror so the public PR diff stays clean. - -This skill is the **only sanctioned way** to sync the public mirror. Running raw `git push mirror …` skips the strip and re-leaks internal artifacts. - -## Stripped paths (single source of truth) - -Edit this list when the leak surface changes — there is no other place to update. - -``` -.claude -.scripts/git-hooks -internal-docs -docs -CLAUDE.md # ALL CLAUDE.md at ANY depth (root + nested, e.g. inc/abilities/, tests/play/specs/) — stripped via `git ls-files '*CLAUDE.md'` (matches the root file too), NOT just the root path -ARCHITECTURE.md -COMPREHENSIVE_ANALYSIS.md -PRODUCT_ANALYSIS.md -TECHNICAL_OVERVIEW.md -tests/play/specs/TODO.md -.github/workflows/push-to-deploy.yml -.github/workflows/push-asset-readme-update.yml -.github/workflows/release-tag-draft.yml -.github/workflows/update-translations.yml -.github/workflows/release-pr-template.yml -bin/build-zip.sh -bin/checkout-and-build -bin/i18n.sh -``` - -These paths must NOT appear on the public mirror. They legitimately exist on private `master` and stay there (internal release CI, AI tooling, internal team wiki, internal dev docs under `docs/`). - -> **README.md is the ONLY markdown doc that should appear in the public PR diff.** After stripping, sanity-check with `git ls-files '*.md'` — anything beyond `README.md`, third-party `inc/lib/**` readmes, `modules/gutenberg/readme.md`, and `tests/play/README.md` is a likely leak. (A prior sync leaked `inc/abilities/CLAUDE.md`, `tests/play/specs/CLAUDE.md`, and the whole `docs/` tree because the strip matched only the root `CLAUDE.md`.) - -## Preconditions - -Working directory: any worktree of this repo. - -Required remotes: -- `origin` → `https://github.com/brainstormforce/sureforms` (private) -- public-mirror remote → `https://github.com/brainstormforce/sureforms-public` (public). May be named `mirror` **or** `public` — detect with `git remote -v` and use whichever points at `sureforms-public`. Examples below use `$MIRROR`. - -Verify with `git remote -v`. Abort if neither a `mirror` nor `public` remote points at `sureforms-public`. - -### ⚠️ `sureforms-public` ruleset constraints (READ FIRST) - -The public repo enforces a branch ruleset (since 2026-06-01) that the previous version of this skill violated. This flow is built around them: - -1. **Every commit needs a *verified* signature.** Signing is not enough — GitHub must mark it **Verified**, which requires BOTH the signing key registered on the pushing account as a **Signing Key** (an *Authentication* key does NOT count) AND the **committer email** verified on that account. **The skill uses the identity of whoever runs it** — your local `git config user.name` / `user.email` / `user.signingkey`. Before running, confirm: your `user.signingkey` points at an SSH public-key file (`*.pub`, since `gpg.format=ssh`) that is registered as a **Signing Key** on *your* GitHub account, and your `user.email` is a **verified** email on that same account. Hardware-backed keys (e.g. Secretive) verify fine but prompt for a tap **per commit**, so they cannot bulk-sign ~150 commits — use an on-disk passphrase-less key for the bulk re-sign. -2. **`sync/master` is force-push protected.** Push each sync to a **fresh** branch `sync/master-` (`-v2`, `-v3`… if re-pushing the same day — reusing a branch makes the ruleset re-evaluate the mirror's own unsigned ancestors and reject). -3. **Claude's auto-mode guardrail blocks the agent from pushing to the public mirror.** The agent prepares everything and hands the user the exact `git push` to run themselves. `gh pr` / `gh api` ARE agent-allowed. - -## Instructions - -Follow steps sequentially. If any step fails, jump to **Error Recovery**. - -### Step 1: Record current state - -```bash -ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD) -HAD_STASH=false -if [ -n "$(git status --porcelain)" ]; then - git stash push -m "sureforms-sync-public auto-stash" - HAD_STASH=true -fi -``` - -### Step 2: Detect the mirror remote + signing identity, then fetch - -Detect the public-mirror remote (named `mirror` or `public`) once, and capture the running user's signing identity. These vars are used throughout: - -```bash -MIRROR=$(git remote -v | awk '/sureforms-public(\.git)?[[:space:]]/{print $1; exit}') -[ -n "$MIRROR" ] || { echo "No remote points at sureforms-public — add one and retry"; exit 1; } - -# Identity of whoever runs the skill (must satisfy the ruleset — see Preconditions) -SIGN_NAME=$(git config user.name) -SIGN_EMAIL=$(git config user.email) -SIGN_KEY=$(git config user.signingkey) # expected: path to an SSH *.pub registered as a Signing Key -[ -n "$SIGN_NAME" ] && [ -n "$SIGN_EMAIL" ] && [ -n "$SIGN_KEY" ] || { echo "git user.name/user.email/user.signingkey must all be set"; exit 1; } - -git fetch origin master -git fetch "$MIRROR" master sync/master 2>/dev/null || git fetch "$MIRROR" master -``` - -### Step 3: Detect no-op - -```bash -PRIVATE_TIP=$(git rev-parse origin/master) -PUBLIC_TIP=$(git rev-parse "$MIRROR/master") -``` - -If `PRIVATE_TIP` == `PUBLIC_TIP`, report "Public mirror is already up to date with private master — nothing to sync." Run **Step 8 cleanup** and exit. - -Capture the upstream commit range and count for reporting: - -```bash -COMMIT_COUNT=$(git rev-list --count "$PUBLIC_TIP..$PRIVATE_TIP") -``` - -### Step 4: Stage cleaned tree on a temp worktree - -Use a temp worktree so the developer's current working tree is never modified. - -```bash -WORKTREE=$(mktemp -d)/srfm-sync-public -git worktree add "$WORKTREE" "$PRIVATE_TIP" -cd "$WORKTREE" -``` - -### Step 5: Strip internal paths idempotently - -For each path in the **Stripped paths** list above, only act if it exists: - -```bash -for p in \ - .claude \ - .scripts/git-hooks \ - internal-docs \ - docs \ - ARCHITECTURE.md \ - COMPREHENSIVE_ANALYSIS.md \ - PRODUCT_ANALYSIS.md \ - TECHNICAL_OVERVIEW.md \ - tests/play/specs/TODO.md \ - .github/workflows/push-to-deploy.yml \ - .github/workflows/push-asset-readme-update.yml \ - .github/workflows/release-tag-draft.yml \ - .github/workflows/update-translations.yml \ - .github/workflows/release-pr-template.yml \ - bin/build-zip.sh \ - bin/checkout-and-build \ - bin/i18n.sh \ -; do - if [ -e "$p" ]; then - git rm -r --quiet "$p" - fi -done - -# Strip ALL CLAUDE.md at any depth (root + nested) — NOT just the root path. -# No pipe-to-while: a subshell would swallow a failing `git rm`. These paths never -# contain spaces, so word-splitting the list is safe. -CLAUDE_FILES=$(git ls-files '*CLAUDE.md') -if [ -n "$CLAUDE_FILES" ]; then - git rm --quiet $CLAUDE_FILES -fi - -# Remove now-empty .scripts directory if applicable -if [ -d .scripts ] && [ -z "$(ls -A .scripts 2>/dev/null)" ]; then - rmdir .scripts -fi - -# Sanity check (FAIL-CLOSED): README.md must be the ONLY non-third-party markdown -# doc left. This control already leaked once — a detected anomaly must HALT the -# sync, not just warn. The check runs in the temp worktree before any push, so -# aborting here means nothing internal reaches the mirror. -LEAKS=$(git ls-files '*.md' | grep -vE '^(README\.md|inc/lib/.*|modules/gutenberg/readme\.md|tests/play/README\.md|inc/abilities/ABILITIES\.md)$') || true -if [ -n "$LEAKS" ]; then - printf 'ABORT: unexpected markdown would be published:\n%s\n' "$LEAKS" - exit 1 -fi -echo "OK: only README.md + known readmes" -``` - -> If the sanity check aborts (`exit 1`), treat it as a failed step: jump to **Error Recovery** to tear down the temp worktree and restore the developer's branch/stash before investigating which path needs adding to the **Stripped paths** list. - -### Step 6: Commit the strip, then bulk re-sign ALL commits - -First commit the strip (committer is the running user — must be the verified email, see ruleset constraints): - -```bash -STRIPPED=false -if ! git diff --cached --quiet; then - STRIPPED=true - STRIPPED_PATHS=$(git diff --cached --name-only | tr '\n' ' ') - git commit -m "chore: strip internal-only paths from public mirror sync" -fi -``` - -Then re-sign **every** commit new to the mirror so they all carry verified signatures. Use `filter-branch` (it reuses each commit's tree + parents and only adds a signature → zero conflicts; `rebase --rebase-merges` re-merges and hits conflicts): - -> **Note:** this rewrites the **committer** of all re-signed commits to `$SIGN_EMAIL` (the original **author** of each commit is preserved). That committer/attribution change on the mirror is intentional — it's what lets the running user's registered key produce verified signatures. - -```bash -BASE=$(git merge-base "$MIRROR/master" "$PRIVATE_TIP") -FILTER_BRANCH_SQUELCH_WARNING=1 git \ - -c gpg.format=ssh -c user.signingkey="$SIGN_KEY" \ - filter-branch -f \ - --env-filter "export GIT_COMMITTER_NAME=\"$SIGN_NAME\"; export GIT_COMMITTER_EMAIL=\"$SIGN_EMAIL\"" \ - --commit-filter "git -c gpg.format=ssh -c user.signingkey=\"$SIGN_KEY\" commit-tree -S \"\$@\"" \ - -- "$BASE..HEAD" -``` - -### Step 7: Cap with a merge commit on the mirror, push to a fresh branch - -Cap the branch with a merge commit whose **first parent is `$MIRROR/master`**. This makes GitHub diff the PR against `$MIRROR/master` (which has no internal files), so the public PR shows **only real upstream changes — no internal-file deletions**. - -```bash -SIGNED_TIP=$(git rev-parse HEAD) -MERGE=$(git -c gpg.format=ssh -c user.signingkey="$SIGN_KEY" \ - -c user.name="$SIGN_NAME" -c user.email="$SIGN_EMAIL" \ - commit-tree -S "$(git rev-parse HEAD^{tree})" -p "$MIRROR/master" -p "$SIGNED_TIP" \ - -m "Sync master from upstream") -BRANCH="sync/master-$(date +%Y%m%d)" -git branch -f "$BRANCH" "$MERGE" -``` - -Then **the user runs the push** (the agent's push to the public mirror is blocked by Claude's guardrail). Push to a **fresh** branch — never `sync/master` (force-protected), and never reuse an existing branch (its prior value makes the ruleset re-evaluate the mirror's unsigned ancestors): - -```bash -git push "$MIRROR" "$BRANCH:refs/heads/$BRANCH" -``` - -If the push is rejected for "verified signatures", isolate before re-signing ~150 commits: push a single test commit signed the same way to a throwaway branch and confirm it's accepted (and `gh api .../commits/ -q .commit.verification.verified` is `true`). - -### Step 8: Open or update the sync PR - -Check whether an open PR already exists for the current sync branch → `master` on `brainstormforce/sureforms-public`: - -```bash -EXISTING_PR=$(gh pr list -R brainstormforce/sureforms-public \ - --base master --head "$BRANCH" --state open \ - --json number -q '.[0].number') -``` - -- **If `EXISTING_PR` is non-empty:** comment a refresh note: - - ```bash - gh pr comment "$EXISTING_PR" -R brainstormforce/sureforms-public \ - --body "Refreshed: synced \`$COMMIT_COUNT\` commit(s) from private master ($PUBLIC_TIP..$PRIVATE_TIP). Strip applied: $STRIPPED." - ``` - -- **If `EXISTING_PR` is empty:** open a new PR. - - Title: `Sync master from upstream` - - Body: upstream commit range, count, strip status, and a "## Highlights" section (`git log --oneline "$PUBLIC_TIP..$PRIVATE_TIP"`). Note that the diff is computed against `public/master` (merge-cap) so no internal files appear. - - ```bash - gh pr create -R brainstormforce/sureforms-public \ - --base master --head "$BRANCH" \ - --title "Sync master from upstream" \ - --body "$PR_BODY" - ``` - - Close/delete any superseded sync branch + PR: - - ```bash - gh pr close -R brainstormforce/sureforms-public - gh api -X DELETE repos/brainstormforce/sureforms-public/git/refs/heads/ - ``` - -### Step 9: Tear down the temp worktree - -```bash -cd - -git worktree remove --force "$WORKTREE" -``` - -### Step 10: Restore developer state - -```bash -git checkout "$ORIGINAL_BRANCH" -if [ "$HAD_STASH" = "true" ]; then - git stash pop -fi -``` - -## Output to user - -Report at the end: - -- Upstream range: `..` (`$COMMIT_COUNT` commits) -- Whether the strip commit was created (`$STRIPPED`); if true, list the stripped paths -- PR URL — new or existing - -## Error Recovery - -If any step fails: - -1. **`cd` back to the original repo dir** if currently inside `$WORKTREE`. -2. **Remove temp worktree** if it was created: `git worktree remove --force "$WORKTREE"` (ignore errors). -3. **Restore branch and stash** as in Step 10. -4. Surface the failing command and its error to the user — do not retry blindly. - -Common failure modes: - -- **Push rejected, "Commits must have verified signatures"** — a commit is signed by an unregistered key or committed under an unverified email. Confirm your `git config user.signingkey` (the `*.pub`) is registered as a **Signing Key** (not Authentication) on your GitHub account and your `git config user.email` is a verified email there. Test with a single throwaway commit before re-signing ~150. -- **Push rejected, "Cannot force-push to this branch"** — you targeted `sync/master`; use a fresh `sync/master-` branch instead. -- **Push rejected naming a few commits already on `master`** — you reused/updated an existing branch; push to a brand-new branch name so the ruleset only evaluates the new commits. -- **Agent push "denied by auto mode classifier"** — expected; the user must run the `git push` to the mirror themselves. -- **`gh pr create` fails with "PR already exists"** — Step 8 detection missed a draft PR. Re-run Step 8 with `--state all` and reuse / reopen the existing PR. -- **No mirror remote** — add it and re-run: `git remote add public https://github.com/brainstormforce/sureforms-public`. diff --git a/.claude/commands/sureforms:update-readme.md b/.claude/commands/sureforms:update-readme.md deleted file mode 100644 index b3d9d8a6f..000000000 --- a/.claude/commands/sureforms:update-readme.md +++ /dev/null @@ -1,327 +0,0 @@ ---- -description: Audit and update the WordPress.org readme.txt — analyze new features, research competitors, draft changes, and create a PR ---- - -# SureForms readme.txt Update - -Comprehensive audit and update of the `readme.txt` (and `README.md`) for the WordPress.org listing. - -## Arguments - -Parse from: $ARGUMENTS - -Expected format: `[version] [--full]` - -- `version` — optional. Baseline version to analyze changes since (e.g., `2.5.0`). If not provided, auto-detect from the most recent `= X.X.X` changelog header in `readme.txt`. -- `--full` — optional flag. Perform a full rewrite of all sections instead of an incremental update. - -If no arguments are given, auto-detect the baseline and proceed with incremental mode. - -## Working Directory - -All commands and file paths are relative to the sureforms plugin root — the directory containing `sureforms.php`. - ---- - -## Phase 1: Analyze What's New - -### Step 1.1 — Determine Baseline Version - -- If `version` argument was provided, use that as the baseline. -- Otherwise, read the `== Changelog ==` section of `readme.txt`, extract the most recent version header (`= X.X.X - ... =`), and use that version as the baseline. -- Resolve the git tag: `v` (e.g., `v2.5.0`). Verify it exists with `git tag -l "v"`. -- If the tag does not exist, list available tags (`git tag --sort=-version:refname | head -10`) and ask the user to pick one. - -### Step 1.2 — Gather Changes Since Baseline - -Launch **3 Explore subagents in parallel** (single message, 3 Agent tool calls): - -**Agent 1 — Git History Analysis:** -- Run `git log v..HEAD --oneline` to get all commits since the baseline. -- Categorize commits: new features, improvements, bug fixes, refactors. -- Focus on commits touching: `inc/blocks/`, `inc/fields/`, `inc/payments/`, `inc/ai-form-builder/`, `modules/`, `src/blocks/`, `inc/page-builders/`. -- Report: list of significant user-facing changes with commit hashes and affected areas. - -**Agent 2 — Free Codebase Feature Scan:** -- List all blocks in `inc/blocks/` — compare against the readme's "Input Fields" and feature bullets. -- Check `inc/payments/` for payment providers. -- Check `inc/ai-form-builder/` for AI feature updates. -- Check `modules/` for any new modules. -- Check `inc/global-settings/` for new settings sections. -- Check `inc/rest-api.php` for new REST endpoints. -- Check `inc/page-builders/` for page builder compatibility additions. -- Report: features present in code but missing from readme. - -**Agent 3 — Pro Codebase Feature Scan:** -- Check `../sureforms-pro/inc/pro/native-integrations/integrations/` — list ALL integration JSON files and compare against the readme's integration list. -- Check `../sureforms-pro/inc/blocks/` for pro-only blocks. -- Check `../sureforms-pro/inc/pro/` subdirectories for pro features (conversational-form, save-resume, signature, zapier, etc.). -- Check `../sureforms-pro/inc/extensions/` for conditional logic, field-restrictions, etc. -- If `../sureforms-pro/` does not exist, skip this agent and warn: "Pro plugin not found. Only free features will be analyzed." -- Report: pro features/integrations present in code but missing from readme. - -### Step 1.3 — Present Gap Analysis - -Merge all agent results and present a table: - -``` -┌─────────────────────────────────────┬────────┬──────────────────────────────┐ -│ Area │ Status │ Details │ -├─────────────────────────────────────┼────────┼──────────────────────────────┤ -│ Free field types match readme │ ✅/❌ │ Code: N, Readme: M │ -│ Pro blocks mentioned │ ✅/❌ │ Missing: ... │ -│ Integration list up to date │ ✅/❌ │ Missing: ... │ -│ Pro features all listed │ ✅/❌ │ Missing: ... │ -│ Payment providers accurate │ ✅/❌ │ │ -│ Page builder compat listed │ ✅/❌ │ │ -│ "Tested up to" WP version │ ✅/❌ │ Readme: X.X, Current: Y.Y │ -│ New features since v │ ℹ️ │ N features found │ -└─────────────────────────────────────┴────────┴──────────────────────────────┘ -``` - -Tell the user: "Phase 1 complete. Found **N gaps** and **M new features** to incorporate. Proceeding to competitor research." - ---- - -## Phase 2: Competitor & Market Research - -### Step 2.1 — Research Competitor READMEs - -Use **WebSearch** to run these searches in parallel: - -1. `"WPForms" site:wordpress.org/plugins description features` -2. `"Gravity Forms" site:wordpress.org/plugins description features` -3. `"Fluent Forms" site:wordpress.org/plugins fluentform features` -4. `"Formidable Forms" site:wordpress.org/plugins description features` -5. `"Contact Form 7" site:wordpress.org/plugins description` -6. `"WordPress.org readme.txt best practices 2025 2026"` - -For each competitor, use **WebFetch** on the top result URL if needed to extract: -- How they structure feature lists -- Marketing language and benefit-focused copy -- How they distinguish free vs pro features -- FAQ structure and common questions -- Trust signals (install counts, star ratings, testimonials) - -### Step 2.2 — Present Research Summary - -Show a comparison table: - -``` -Feature Highlighted by Competitors │ SureForms Has It? │ In Readme? -──────────────────────────────────────┼───────────────────┼────────── -Conditional Logic │ ✅ (Pro) │ ✅/❌ -File Upload │ ✅ (Pro) │ ✅/❌ -Drag & Drop Builder │ ✅ (Free) │ ✅/❌ -... │ │ -``` - -Also note: -- SEO-relevant keywords competitors use that SureForms is missing. -- Structural patterns that work well (comparison tables, benefit-focused headers). -- Any WordPress.org formatting trends. - -Tell the user: "Phase 2 complete. Ready to draft the update. Shall I proceed?" - -**⏸️ WAIT for user confirmation before proceeding to Phase 3.** - ---- - -## Phase 3: Draft the Update - -### Step 3.1 — Determine Scope - -- If `--full` flag was passed: draft ALL sections from scratch. -- Otherwise: draft only sections that need changes based on Phase 1 and Phase 2 findings. - -### Step 3.2 — Draft Each Section - -For each section that needs updating, present in this format: - -``` -━━━ SECTION: [Section Name] ━━━ - -── CURRENT ── -[Current text from readme.txt, abbreviated if very long] - -── PROPOSED ── -[New text] - -── CHANGES ── -• Added: [what was added] -• Removed: [what was removed, if any] -• Reworded: [what was reworded and why] -``` - -Sections to evaluate (in order): - -1. **Plugin header** — Stable tag, Tested up to, Tags (optimize for WordPress.org search) -2. **Short description** — Must be under 150 characters. Make it compelling and keyword-rich. -3. **Long description** — Feature bullets, AI section, payment section, field types list -4. **Premium features section** — Integration list, pro-only features, pro field types -5. **FAQ** — Add questions for new features, update outdated answers -6. **3rd Party Services** — Add any new external services used -7. **Compatibility lists** — Themes and plugins - -### Step 3.3 — WordPress.org Compliance Check - -Verify the draft against WordPress.org readme standards: -- ✅ Short description is under 150 characters -- ✅ No disallowed HTML in description -- ✅ `== Changelog ==` section exists and is properly formatted -- ✅ `== Frequently Asked Questions ==` uses correct `= Question =` format -- ✅ No broken markdown that WordPress.org would render incorrectly -- ✅ Stable tag matches the version in the main plugin file - -### Step 3.4 — Present Full Draft - -Present all drafted sections together with a summary: - -``` -📋 Readme Update Summary -───────────────────────────────────── -Sections modified: N -Lines added: +X -Lines removed: -Y -New features listed: Z -New integrations: W -New FAQ entries: Q -WP.org compliance: ✅ All checks pass -``` - -Tell the user: "Please review the draft above. You can: -1. **Approve** — proceed to apply changes and create PR -2. **Request edits** — tell me what to change -3. **Abort** — cancel the update" - -**⏸️ WAIT for explicit user approval. Do NOT proceed without it.** - ---- - -## Phase 4: User Review & Iteration - -This phase is a loop: - -1. If user **requests edits** → apply them to the draft and re-present the affected sections. Ask again for approval. -2. If user **approves** → proceed to Phase 5. -3. If user **aborts** → stop and report "Readme update cancelled. No changes were made." - ---- - -## Phase 5: Apply & Create PR - -### Step 5.1 — Create Branch - -```bash -git fetch origin -git checkout dev -git pull origin dev -git checkout -b chore/update-readme -``` - -If `chore/update-readme` already exists, append a date suffix: `chore/update-readme-YYYYMMDD`. - -### Step 5.2 — Apply Changes - -Edit `readme.txt` with the approved draft changes. Apply section by section — do NOT rewrite sections that weren't modified. - -### Step 5.3 — Regenerate README.md - -```bash -npx grunt readme -``` - -Verify `README.md` was updated. If `grunt readme` fails, check that `npm install` has been run and `grunt-wp-readme-to-markdown` is in `devDependencies`. - -### Step 5.4 — Verify - -Read the updated `readme.txt` and confirm: -- Stable tag matches current plugin version -- Tested up to is correct -- Short description is under 150 characters -- All sections are properly formatted -- No stray draft markers or debug text - -### Step 5.5 — Commit and Push - -```bash -git add readme.txt README.md -git commit -m "chore: update readme.txt for WordPress.org listing - -Update plugin description, feature lists, integrations, FAQ, -and compatibility information to reflect current capabilities. - -Co-Authored-By: Claude Opus 4.6 (1M context) " -git push -u origin chore/update-readme -``` - -### Step 5.6 — Create PR - -```bash -gh pr create \ - --title "chore: Update readme.txt for WordPress.org" \ - --base dev \ - --body "$(cat <<'EOF' -## Summary -- Updated readme.txt to reflect current plugin capabilities -- Regenerated README.md via `grunt readme` - -## What Changed -[List each section that was modified and what changed] - -## Research Notes -- Competitor READMEs reviewed: WPForms, Gravity Forms, Fluent Forms, Formidable, CF7 -- Gaps identified and addressed: [count] - -## Test Plan -- [ ] Preview on WordPress.org readme validator -- [ ] Verify all links in the readme are working -- [ ] Confirm feature claims match actual plugin capabilities -- [ ] Verify short description is under 150 characters - -🤖 Generated with [Claude Code](https://claude.com/claude-code) -EOF -)" -``` - -### Step 5.7 — Final Report - -``` -✅ Readme Update Complete -───────────────────────────────────────── -✅ Phase 1: Gap analysis complete -✅ Phase 2: Competitor research complete -✅ Phase 3: Draft created -✅ Phase 4: User review passed -✅ Phase 5: Changes applied -✅ Branch: chore/update-readme (from dev) -✅ readme.txt updated -✅ README.md regenerated -✅ PR: - -Sections updated: - • [List each modified section] -``` - ---- - -## Rules - -- **NEVER modify readme.txt without user approval.** Always present the full draft first. -- **NEVER invent features.** Only document what actually exists in the codebase. -- **NEVER modify changelog entries.** The changelog is managed by `/sureforms:version-bump`. -- **ALWAYS branch from `dev`**, never from `master` or `next-release`. -- **ALWAYS run `npx grunt readme`** after modifying `readme.txt` to regenerate `README.md`. -- **ALWAYS verify feature claims** by checking the actual codebase, not just commit messages. -- **Keep the 150-character limit** for the WordPress.org short description. -- **Maintain the existing voice and tone** — professional, benefit-focused, not overly salesy. - -## Error Recovery - -| Symptom | Fix | -|---------|-----| -| `grunt readme` fails | Run `npm install` first; verify `grunt-wp-readme-to-markdown` is in `devDependencies` | -| Branch already exists | Append date suffix: `chore/update-readme-YYYYMMDD` | -| Pro plugin not found at `../sureforms-pro/` | Skip pro analysis, warn user, document only free features | -| Git tag not found | List available tags and ask user to pick one | -| WebSearch unavailable | Skip Phase 2, proceed with codebase analysis only, note the limitation | diff --git a/.claude/commands/sureforms:version-bump.md b/.claude/commands/sureforms:version-bump.md deleted file mode 100644 index 5f48a2e1f..000000000 --- a/.claude/commands/sureforms:version-bump.md +++ /dev/null @@ -1,204 +0,0 @@ -# SureForms Release Version Bump - -Automates the SureForms version bump release checklist end-to-end. - -## Arguments - -Parse from: $ARGUMENTS - -Expected format: ` [pro-version] [branch]` - -- `version` — **required**. Version number to release (e.g., `2.5.2`). If not provided, stop immediately and tell the user: `"Version number is required. Usage: /sureforms:version-bump [pro-version] [branch]"` -- `pro-version` — optional. Version to set for `SRFM_PRO_RECOMMENDED_VER`. If not provided, ask: `"What should SRFM_PRO_RECOMMENDED_VER be set to? (default: )"` and default to the core version if the user presses Enter or skips. -- `branch` — optional. Source branch to cut from — must be `dev` or `next-release`. Default: `next-release`. - -## Working Directory - -All commands and file paths are relative to the sureforms plugin root. Detect it as the directory containing `sureforms.php` — typically the current working directory or a `sureforms/` subdirectory of it. - ---- - -## Steps - -### Step 1 — Validate Arguments - -- Extract `version`, `pro-version`, and `branch` from `$ARGUMENTS`. -- Abort with usage message if `version` is missing. -- If `branch` is provided but not `dev` or `next-release`, abort: `"Invalid branch. Must be 'dev' or 'next-release'."` -- If `pro-version` was not passed as an argument, prompt the user for it (default: same as `version`). - ---- - -### Step 2 — Create Version Bump Branch - -Run from the sureforms plugin root: - -```bash -git fetch origin -git checkout -git pull origin -git checkout -b version-bump- -``` - -Confirm the new branch was created successfully before continuing. - ---- - -### Step 3 — Run Grunt Version Bump - -```bash -npx grunt version-bump --ver= -``` - -This runs `bumpup` (updates `package.json`) and `replace` (updates version strings in `readme.txt` and `sureforms.php`). - ---- - -### Step 4 — Verify package.json and package-lock.json - -- Read `package.json` → confirm `"version"` equals ``. -- Read `package-lock.json` → confirm the top-level `"version"` and `packages[""].version` both equal ``. -- If `package-lock.json` is out of sync, patch **only** the two version string values — do not alter any whitespace, indentation, line endings, or surrounding characters: - - Top-level: replace only the value in `"version": ""` → `"version": ""` - - Inside `"packages": { "": { ... } }`: replace only the value in `"version": ""` → `"version": ""` - - The resulting diff must show **exactly 2 lines changed** — only the version string on each line. No other bytes in the file should change. - - Do **not** run `npm install` — dependency conflicts can cause it to fail and it is not needed for this change. - ---- - -### Step 5 — Verify readme.txt — Stable tag - -- Read `readme.txt` and confirm `Stable tag: `. -- If incorrect, edit the line to the correct value. - ---- - -### Step 6 — Verify readme.txt — Tested up to - -- Fetch the latest WordPress version by running: - ```bash - curl -s "https://api.wordpress.org/core/version-check/1.7/" | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4 - ``` -- Store the result as ``. -- Read `readme.txt` and check the current `Tested up to:` value. -- If it does **not** match ``, update the line to `Tested up to: `. -- Confirm the final value in the file after any edit. - ---- - -### Step 7 — Verify sureforms.php — Plugin Header Version - -- Read `sureforms.php` lines 1–15. -- Confirm `Version: ` in the plugin header comment. -- If incorrect, edit to the correct value. - ---- - -### Step 8 — Verify SRFM_VER - -- Confirm `define( 'SRFM_VER', '' );` in `sureforms.php`. -- If incorrect, edit to the correct value. - ---- - -### Step 9 — Verify SRFM_PRO_RECOMMENDED_VER - -- Confirm `define( 'SRFM_PRO_RECOMMENDED_VER', '' );` in `sureforms.php`. -- If incorrect, edit to the correct value. - ---- - -### Step 10 — Verify Changelog Entry in readme.txt - -- Read the `== Changelog ==` section of `readme.txt`. -- **Check for a placeholder entry** matching `= x.x.x =` (case-insensitive): - - If **found**: ask the user: `"What is the release date for ? (e.g. 19th February 2026)"`. Replace the placeholder header with `= - =`, matching the exact formatting of existing changelog entries (spaces around the `=`, date style matching what is already present). -- **Check for an entry** matching `= -`: - - If **missing** after the above: warn the user and **pause** — they must add a changelog entry for `` before the process can continue. The expected format is: - ``` - = - DD Month YYYY = - * New: ... - * Improvement: ... - * Fix: ... - ``` - - If **found**: confirm the date is present and the format matches SureForms standards. -- **Polish each bullet line** of the `` entry: - - Fix any grammatical or spelling errors. - - Rewrite passive or vague phrasing into clear, active, benefit-led language (e.g. "Fixed bug where X" → "Fix: Resolved an issue where X to ensure Y"). - - Keep the improvements **subtle and factual** — do not invent features or exaggerate. The prefix (`New:`, `Improvement:`, `Fix:`) must stay unchanged. - - Show the user a before/after diff of any lines you changed and ask for confirmation before writing. -- Then sort the bullet lines of the `` entry so they follow this group order: - 1. `* New:` lines - 2. `* Improvement:` lines - 3. `* Fix:` lines - 4. Any other prefixes come last -- Within each group, sort lines **alphabetically** (case-insensitive) by the text that follows the prefix. -- Write the re-ordered lines back to `readme.txt` if any reordering was needed, and report whether changes were made. - ---- - -### Step 11 — Trim Changelog to 3 Entries - -- Parse the `== Changelog ==` section of `readme.txt` to identify all version entries (lines matching `= X.X.X - ...=`). -- Keep only the **3 most recent** entries (the new `` entry plus the 2 entries immediately before it), including all their bullet lines. -- Remove all older entries that follow the 3rd entry, up to but not including the next `==` section header. -- Write the trimmed content back to `readme.txt`. -- Report how many entries were removed (e.g., "Removed 4 old changelog entries"). - ---- - -### Step 12 — Generate README.md - -```bash -npx grunt readme -``` - ---- - -### Step 13 — Generate POT File - -```bash -npm run makepot -``` - ---- - -### Step 14 — Commit and Open PR - -```bash -git add -A -git commit -m "Version Bump " -git push origin version-bump- -gh pr create \ - --repo brainstormforce/sureforms \ - --title "Version Bump " \ - --base \ - --label "Release PR,skip-title-check" -``` - ---- - -### Step 15 — Final Report - -Print a status summary for every step, then the PR URL: - -``` -Release Bump: -───────────────────────────────────────── -✅ Branch created: version-bump- (from ) -✅ grunt version-bump: completed -✅ package.json: -✅ package-lock.json: -✅ readme.txt Stable tag: -✅ readme.txt Tested up to: -✅ sureforms.php Version: -✅ SRFM_VER: -✅ SRFM_PRO_RECOMMENDED_VER: -✅ Changelog entry polished, confirmed, and sorted (New → Improvement → Fix, alphabetical within groups) -✅ Changelog trimmed to 3 entries (N removed) -✅ README.md generated -✅ POT file generated -✅ PR opened: -``` - -If any step failed (❌) or needs attention (⚠️), list the required actions at the bottom. diff --git a/.claude/commands/sync-release-branches.md b/.claude/commands/sync-release-branches.md deleted file mode 100644 index 210f07139..000000000 --- a/.claude/commands/sync-release-branches.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -allowed-tools: Bash(git:*) -auto-approve: true -description: Sync master into dev and dev into next-release after a release ---- - -# Sync Release Branches - -After a release, sync the three main branches: master → dev → next-release. - -## Instructions - -Follow these steps sequentially. If any step fails, jump to the Error Recovery section. - -### Step 1: Record current state - -1. Record the current branch: -```bash -git rev-parse --abbrev-ref HEAD -``` -Store this as ORIGINAL_BRANCH. - -2. Check for uncommitted changes: -```bash -git status --porcelain -``` -If there is ANY output, stash the changes: -```bash -git stash -``` -Store HAD_STASH=true. Otherwise HAD_STASH=false. - -### Step 2: Fetch latest from remote - -```bash -git fetch origin -``` - -### Step 3: Update master - -```bash -git checkout master -git pull origin master -``` - -### Step 4: Merge master into dev - -```bash -git checkout dev -git pull origin dev -git pull origin master -``` - -If there are merge conflicts, STOP. Tell the user: "Merge conflicts while merging master into dev. Please resolve them manually." List the conflicting files. - -If merge succeeds: -```bash -git push origin dev -``` - -### Step 5: Merge dev into next-release - -```bash -git checkout next-release -git pull origin next-release -git pull origin dev -``` - -If there are merge conflicts, STOP. Tell the user: "Merge conflicts while merging dev into next-release. Please resolve them manually." List the conflicting files. - -If merge succeeds: -```bash -git push origin next-release -``` - -### Step 6: Return to original branch - -```bash -git checkout ${ORIGINAL_BRANCH} -``` - -If HAD_STASH=true: -```bash -git stash pop -``` - -Report success: "Branch sync complete. master → dev → next-release are now in sync." - -## Error Recovery - -If any step fails after switching branches: -1. Switch back to original branch: `git checkout ${ORIGINAL_BRANCH}` -2. Restore stash if needed: `git stash pop` (only if HAD_STASH=true) -3. Report the error to the user. - -NEVER force-push. If a merge has conflicts, stop and let the user resolve them. diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 02b620f27..000000000 --- a/.claude/settings.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "hooks": { - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": "if echo \"$CLAUDE_BASH_COMMAND\" | grep -q '^git commit'; then echo '--- PreCommit: Running checks ---' && composer lint 2>&1 | tail -5 && composer phpstan 2>&1 | tail -5 && composer insights 2>&1 | tail -5; fi" - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Write|Edit", - "hooks": [ - { - "type": "command", - "command": "case \"$CLAUDE_FILE_PATH\" in *.php) vendor/bin/phpcbf \"$CLAUDE_FILE_PATH\" 2>/dev/null || true;; *.js|*.jsx) npx wp-scripts lint-js --fix \"$CLAUDE_FILE_PATH\" 2>/dev/null || true;; *.scss|*.css) npx wp-scripts lint-style --fix \"$CLAUDE_FILE_PATH\" 2>/dev/null || true;; esac" - } - ] - } - ] - }, - "permissions": { - "allow": [ - "Bash(npm run lint-js*)", - "Bash(npm run lint-css*)", - "Bash(npm run pretty*)", - "Bash(npm run build*)", - "Bash(npm run start)", - "Bash(npm run test:unit)", - "Bash(npm run test:e2e)", - "Bash(npm run play:*)", - "Bash(npm run makepot)", - "Bash(npm run i18n:*)", - "Bash(composer lint*)", - "Bash(composer format*)", - "Bash(composer test*)", - "Bash(composer phpstan*)", - "Bash(composer insights*)", - "Bash(npx playwright test*)", - "Bash(npx grunt *)", - "Bash(composer install*)", - "Bash(npm install*)", - "Bash(curl *)", - "Bash(git status*)", - "Bash(git diff*)", - "Bash(git log*)", - "Bash(git branch*)", - "Bash(git show*)", - "Bash(git stash*)", - "Bash(git checkout*)", - "Bash(git add*)", - "Bash(git commit*)", - "Bash(git merge*)", - "Bash(git rebase*)", - "Bash(git fetch*)", - "Bash(git pull*)", - "Bash(gh pr *)", - "Bash(gh issue *)", - "Bash(gh api *)", - "Bash(ls *)", - "Bash(wc *)", - "Bash(which *)", - "Bash(node -e *)", - "Bash(php -r *)", - "Bash(wp *)" - ], - "deny": [] - } -} diff --git a/.github/workflows/push-asset-readme-update.yml b/.github/workflows/push-asset-readme-update.yml deleted file mode 100644 index de4769aaf..000000000 --- a/.github/workflows/push-asset-readme-update.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Plugin asset/readme update -on: - push: - branches: - - master - paths: - - 'readme.txt' - - '.wordpress-org/**' -jobs: - master: - name: Push to main - runs-on: ubuntu-22.04 - environment: production - steps: - - uses: actions/checkout@master - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - - name: Build - run: | - npm ci - npm run build - - name: WordPress.org plugin asset/readme update - uses: 10up/action-wordpress-plugin-asset-update@stable - env: - SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} - SVN_USERNAME: ${{ secrets.SVN_USERNAME }} diff --git a/.github/workflows/push-to-deploy.yml b/.github/workflows/push-to-deploy.yml deleted file mode 100644 index e10b21ba8..000000000 --- a/.github/workflows/push-to-deploy.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Deploy to WordPress.org -on: - release: - types: [published] -jobs: - tag: - name: New tag - runs-on: ubuntu-22.04 - environment: production - steps: - - uses: actions/checkout@master - - name: Verify release tag is reachable from master - run: | - git fetch --no-tags --depth=50 origin master:refs/remotes/origin/master - if ! git merge-base --is-ancestor "$GITHUB_SHA" refs/remotes/origin/master; then - echo "::error::Tag $GITHUB_REF_NAME ($GITHUB_SHA) is not reachable from master. Refusing to deploy." - exit 1 - fi - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - - name: Build - run: | - npm ci - npm run build - - name: WordPress Plugin Deploy - uses: 10up/action-wordpress-plugin-deploy@master - env: - SVN_USERNAME: ${{ secrets.SVN_USERNAME }} - SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} diff --git a/.github/workflows/release-pr-template.yml b/.github/workflows/release-pr-template.yml deleted file mode 100644 index b296fcd31..000000000 --- a/.github/workflows/release-pr-template.yml +++ /dev/null @@ -1,41 +0,0 @@ - -name: Update checklist for Release PR - -on: - pull_request: - types: [ labeled ] - -jobs: - update_pr: - name: Update checklist for Release PR - if: ${{ github.event.label.name == 'Release PR' }} - runs-on: ubuntu-latest - steps: - - name: Check Branch - id: check-branch - run: | - if [[ ${{ github.base_ref }} == master || ${{ github.base_ref }} == dev || ${{ github.base_ref }} == next-release ]]; then - echo ::set-output name=match::true - fi - - uses: tzkhan/pr-update-action@v2 - if: steps.check-branch.outputs.match == 'true' - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - base-branch-regex: '[A-Za-z\d-_.\\/]+' - head-branch-regex: '[A-Za-z\d-_.\\/]+' - body-template: | - --- - ### Release Checklist: - - [ ] Run to update version number : `grunt version-bump --ver=` - - [ ] Verify the version number in `package.json` and `package-lock.json` - - [ ] Verify `Stable tag` is `` in readme.txt - - [ ] Verify `Tested upto` is set to latest tested version of WordPress - - [ ] Update version in `sureforms.php` in plugin description - - [ ] Verify constant `SRFM_VER` in `sureforms.php` with latest version of SureForms - - [ ] Verify constant `SRFM_PRO_RECOMMENDED_VER` in `sureforms.php` with compatible version of SureForms Pro - - [ ] Verify changelog `date` and `content` as per SureForms standards - - [ ] Generate README.md : `grunt readme` - - [ ] Generate POT file : `npm run makepot` - --- - body-update-action: 'suffix' - body-uppercase-base-match: false diff --git a/.github/workflows/release-tag-draft.yml b/.github/workflows/release-tag-draft.yml deleted file mode 100644 index 55a6449bf..000000000 --- a/.github/workflows/release-tag-draft.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Create a release tag - -on: - push: - branches: - - master - paths: - # Release happens only if the version number in main file is updated. - - 'sureforms.php' - -jobs: - build-artifact: - name: Build Release Artifact - runs-on: ubuntu-22.04 - outputs: - current_version: ${{ steps.get_version.outputs.current_version }} - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - ref: master - - - name: Initialize mandatory git config - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Compute current version - id: get_version - run: | - CURRENT_VERSION=$(jq --raw-output '.version' package.json) - echo "current_version=${CURRENT_VERSION}" >> $GITHUB_OUTPUT - - - name: Install Node.js and npm - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Build plugin ZIP file - run: bash ./bin/build-zip.sh - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: sureforms-plugin - path: ./artifact/sureforms.zip - - create-release: - name: Create Release Draft and Attach Asset - needs: [build-artifact] - runs-on: ubuntu-latest - steps: - - name: Set Release Version - id: get_release_version - env: - VERSION: ${{ needs.build-artifact.outputs.current_version }} - run: echo "version=$(echo $VERSION | cut -d / -f 3 | sed 's/-rc./ RC/' )" >> $GITHUB_OUTPUT - - - name: Download Plugin Zip Artifact - uses: actions/download-artifact@v8 - with: - name: sureforms-plugin - - - name: Create Release Draft - id: create_release - uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: Version ${{ steps.get_release_version.outputs.version }} - tag_name: "v${{ steps.get_release_version.outputs.version }}" - target_commitish: master - draft: true - prerelease: false - body: 'Replace changelog here' - files: ./sureforms.zip diff --git a/.github/workflows/update-translations.yml b/.github/workflows/update-translations.yml deleted file mode 100644 index 38ca26957..000000000 --- a/.github/workflows/update-translations.yml +++ /dev/null @@ -1,205 +0,0 @@ -name: Update Translations - -on: - issue_comment: - types: [created] - -# Only allow one i18n job per PR at a time. -concurrency: - group: i18n-${{ github.event.issue.number }} - cancel-in-progress: true - -permissions: - contents: write - pull-requests: write - issues: write - -jobs: - i18n: - # Only trusted users may trigger a run — this job executes branch build - # scripts with secrets, so gate on the commenter's association. - if: | - github.event.comment.body == '/i18n' && - github.event.issue.pull_request != null && - contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) - runs-on: ubuntu-latest - - steps: - # Acknowledge the command with a reaction - - name: Add reaction to comment - uses: peter-evans/create-or-update-comment@v5 - with: - comment-id: ${{ github.event.comment.id }} - reactions: rocket - - # Get PR metadata (head branch, fork detection) - - name: Get PR details - id: pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_JSON=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}) - HEAD_REF=$(echo "$PR_JSON" | jq -r '.head.ref') - HEAD_REPO=$(echo "$PR_JSON" | jq -r '.head.repo.full_name') - IS_FORK=$(echo "$PR_JSON" | jq -r '.head.repo.full_name != .base.repo.full_name') - - echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT" - echo "head_repo=$HEAD_REPO" >> "$GITHUB_OUTPUT" - echo "is_fork=$IS_FORK" >> "$GITHUB_OUTPUT" - - # Block fork PRs — secrets are not available - - name: Block fork PRs - if: steps.pr.outputs.is_fork == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**i18n update skipped**: This PR is from a fork. Translation updates require repository secrets and cannot run on fork PRs." - exit 1 - - # Notify that the workflow has started - - name: Comment starting - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**Translations update started** :hourglass_flowing_sand: - Building and generating translations for 7 languages (nl, fr, de, es, it, pt, pl)... - [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" - - # Checkout the actual PR branch (not default branch) - - name: Checkout PR branch - uses: actions/checkout@v6 - with: - ref: ${{ steps.pr.outputs.head_ref }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - cache: 'npm' - - # Pin WP-CLI to a fixed release. `wp i18n make-json` stamps - # "generator":"WP-CLI/" into every languages/*.json, so - # installing "latest" rewrites all JSON files whenever WP-CLI - # ships a new version. Bump this pin deliberately. - - name: Install WP-CLI - run: | - curl -sSfL -o wp-cli.phar https://github.com/wp-cli/wp-cli/releases/download/v2.12.0/wp-cli-2.12.0.phar - chmod +x wp-cli.phar - sudo mv wp-cli.phar /usr/local/bin/wp - wp --info - - - name: Install dependencies - run: npm ci - - # Build first so makepot picks up all the latest strings - - name: Build - run: npm run build - - # Run the full i18n pipeline (makepot -> po -> gpt-translate -> mo -> json) - - name: Run i18n script - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: | - set -euo pipefail - bash bin/i18n.sh - - # Check if any files changed - - name: Check for changes - id: changes - run: | - if git diff --quiet; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - CHANGED_FILES=$(git diff --name-only | wc -l | tr -d ' ') - echo "file_count=$CHANGED_FILES" >> "$GITHUB_OUTPUT" - fi - - # Push the i18n branch at the current PR-head tip. This only creates a - # branch ref pointing at an existing commit — no new (unverified) commit - # object is introduced here. - - name: Create i18n branch - if: steps.changes.outputs.changed == 'true' - env: - # Pass via env so the branch name can't inject shell commands. - HEAD_REF: ${{ steps.pr.outputs.head_ref }} - run: | - BRANCH="i18n/$HEAD_REF" - echo "BRANCH=$BRANCH" >> "$GITHUB_ENV" - git push --force origin "HEAD:refs/heads/$BRANCH" - - # Commit the generated translation files through the GitHub GraphQL API - # so the commit is signed by GitHub's GPG key and shows as "Verified" - # (a plain `git push` over HTTPS is never signed). Pinned to the v0.2.20 - # commit SHA for supply-chain safety. - - name: Commit translations (verified) - if: steps.changes.outputs.changed == 'true' - uses: planetscale/ghcommit-action@25309d8005ac7c3bcd61d3fe19b69e0fe47dbdde # v0.2.20 - with: - commit_message: | - chore: update i18n translations - - Auto-generated by /i18n command on PR #${{ github.event.issue.number }} - repo: ${{ github.repository }} - branch: ${{ env.BRANCH }} - file_pattern: '.' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Open the translations PR against the original PR's head branch. - - name: Create PR - if: steps.changes.outputs.changed == 'true' - id: create_pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Pass via env so the base branch name can't inject shell commands. - HEAD_REF: ${{ steps.pr.outputs.head_ref }} - run: | - PR_URL=$(gh pr create \ - --title "chore: update i18n translations" \ - --body "This PR updates translation files for 7 languages (nl, fr, de, es, it, pt, pl). - - Triggered by \`/i18n\` command on #${{ github.event.issue.number }}." \ - --base "$HEAD_REF" \ - --head "$BRANCH") - - echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" - - # Comment on the original PR with a link to the translations PR - - name: Comment success - if: steps.changes.outputs.changed == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**Translations PR created** :white_check_mark: - ${{ steps.changes.outputs.file_count }} files changed across 7 languages. - Review and merge: ${{ steps.create_pr.outputs.pr_url }}" - - - name: Comment no changes - if: steps.changes.outputs.changed == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**No translation changes** :white_check_mark: - All translation files are already up to date." - - # On any failure, comment with a link to the logs - - name: Comment failure - if: failure() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**Translation update failed** :x: - [View workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details." diff --git a/.scripts/git-hooks/pre-commit b/.scripts/git-hooks/pre-commit deleted file mode 100644 index ce0ba23ea..000000000 --- a/.scripts/git-hooks/pre-commit +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/sh - -# Check PHPCS before commit -# Ref - -# https://github.com/smgladkovskiy/phpcs-git-pre-commit/blob/master/src/pre-commit -# https://dev.to/akdevcraft/git-pre-hook-pre-commit-hook-with-npm-project-44p2 - -echo "***** Running pre-commit ******" - -PROJECT=$(php -r "echo dirname(dirname(dirname(realpath('$0'))));") -STAGED_FILES_CMD=$(git diff --cached --name-only --diff-filter=ACMR HEAD | grep \\.php) -UNSTAGED_FILES_CMD=$(git diff --name-only --diff-filter=ACMR | grep \\.php) - -# Determine if a file list is passed -if [ "$#" -eq 1 ] -then - oIFS=$IFS - IFS=' - ' - SFILES="$1" - IFS=$oIFS -fi - -SFILES=${SFILES:-$STAGED_FILES_CMD} - -STAGED_BUT_MODIFIED_FILES=$(php -r "\$sfiles=(explode(\"\\n\", '$SFILES'));\$usfiles=(explode(\"\\n\", '$UNSTAGED_FILES_CMD'));echo implode(\"\\n\",array_intersect(\$usfiles,\$sfiles));") - -if [ -z "$STAGED_BUT_MODIFIED_FILES" ]; then - echo "OK" -else - echo "Files staged but then modified:\n" - echo "${STAGED_BUT_MODIFIED_FILES}" - exit 1 -fi - -# echo $PROJECT -# echo $SFILES - -echo "Checking files for PHP Lint..." -for FILE in $SFILES -do - # echo "$PROJECT/$FILE" - php -l -d display_errors=0 "./$FILE" - if [ $? != 0 ] - then - echo "Fix the error before commit." - exit 1 - fi - FILES="$FILES ./$FILE" -done - -if [ "$FILES" = "" ] -then - echo "All good..." - exit 0 -fi - -if [ "$FILES" != "" ] -then - echo "Running PHPCS (Code Sniffer) Check..." - composer lint -n $FILES - if [ $? != 0 ] - then - echo "Please fix the PHPCS errors before commit!" - echo " => Run this command for automatic fixes." - echo " composer format-- $FILES" - exit 1 - fi -fi - -if [ "$FILES" != "" ] -then - echo "Running PHPStan Check..." - composer phpstan - if [ $? != 0 ] - then - echo "Please fix the PHPStan errors before commit!" - echo " => Run this command to check PHPStan errors." - echo " composer phpstan" - exit 1 - fi -fi - -if [ "$FILES" != "" ] -then - echo "Running CSS linting issues..." - npm run lint-css - if [ $? != 0 ] - then - echo "Please fix the CSS issues before commit!" - exit 1 - fi -fi - -if [ "$FILES" != "" ] -then - echo "Running JS linting issues..." - npm run lint-js - if [ $? != 0 ] - then - echo "Please fix the JS issues before commit!" - exit 1 - fi -fi - -exit $? diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 2afeea1d5..000000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,355 +0,0 @@ -# SureForms Architecture Documentation - -## Table of Contents - -1. [Introduction](#introduction) -2. [System Architecture](#system-architecture) -3. [Core Components](#core-components) -4. [Data Flow](#data-flow) -5. [Form Building Process](#form-building-process) -6. [AI Integration](#ai-integration) -7. [Form Submission and Processing](#form-submission-and-processing) -8. [Security Features](#security-features) -9. [Extension Points](#extension-points) -10. [Technical Debt and Improvement Areas](#technical-debt-and-improvement-areas) -11. [Development Guidelines](#development-guidelines) - -## Introduction - -SureForms is a modern WordPress form builder plugin designed to provide an intuitive, no-code form building experience. It leverages WordPress's native block editor (Gutenberg) to create a seamless form building experience that integrates naturally with the WordPress ecosystem. - -### Key Features - -- **Native WordPress Integration**: Built on top of the WordPress block editor -- **AI-Powered Form Generation**: Create forms quickly using AI suggestions -- **No-Code Flexibility**: Advanced features without requiring coding knowledge -- **Mobile-First Design**: Responsive forms that work across all devices -- **Modern UI/UX**: Clean, intuitive interface for both admins and users -- **Security Features**: Anti-spam protection, GDPR compliance, and data security -- **Extensibility**: Developer-friendly architecture with hooks and filters - -## System Architecture - -SureForms follows a modular architecture pattern, separating concerns into distinct components that interact through well-defined interfaces. - -```mermaid -graph TD - A[WordPress Core] --> B[SureForms Plugin] - B --> C[Admin Interface] - B --> D[Form Builder] - B --> E[Form Renderer] - B --> F[Form Processor] - B --> G[Database Layer] - B --> H[AI Integration] - - C --> I[Settings Management] - C --> J[Form Management] - C --> K[Entry Management] - - D --> L[Block Editor Integration] - D --> M[Form Templates] - D --> N[Field Components] - - E --> O[Frontend Rendering] - E --> P[Validation] - - F --> Q[Submission Handler] - F --> R[Email Notifications] - F --> S[Confirmation Handler] - - G --> T[Custom Post Types] - G --> U[Custom Database Tables] - G --> V[Metadata Storage] - - H --> W[AI Form Generator] - H --> X[AI Middleware] -``` - -## Core Components - -### Plugin Loader (`Plugin_Loader`) - -The central initialization class that bootstraps the plugin, registers hooks, and loads core components. It follows a singleton pattern to ensure only one instance exists. - -### Post Types (`Post_Types`) - -Registers and manages custom post types for forms and entries. Handles the admin UI customizations for these post types. - -### Form Submit (`Form_Submit`) - -Processes form submissions through REST API endpoints, validates input data, handles file uploads, processes anti-spam measures, and triggers email notifications. - -### AI Form Builder (`AI_Form_Builder`) - -Integrates with AI services to generate form structures based on user prompts. Communicates with middleware services to process AI requests. - -### Database Layer - -Custom database tables for storing form entries and related data, separate from the WordPress post tables for better performance and data organization. - -### Frontend Assets (`Frontend_Assets`) - -Manages the loading of CSS and JavaScript assets for the frontend rendering of forms. - -### Email Notifications - -Handles the sending of email notifications based on form submissions, with support for multiple notification templates and recipients. - -## Data Flow - -### Form Creation Flow - -```mermaid -sequenceDiagram - participant User - participant WordPress - participant FormBuilder - participant Database - - User->>WordPress: Access Form Builder - WordPress->>FormBuilder: Initialize Builder Interface - FormBuilder->>User: Display Form Builder UI - - alt Manual Form Creation - User->>FormBuilder: Add/Configure Form Fields - FormBuilder->>User: Update UI Preview - else AI-Assisted Creation - User->>FormBuilder: Enter Form Description - FormBuilder->>AI: Send Form Description - AI->>FormBuilder: Return Suggested Form Structure - FormBuilder->>User: Display AI-Generated Form - User->>FormBuilder: Modify/Approve Form - end - - User->>FormBuilder: Save Form - FormBuilder->>Database: Store Form Configuration - Database->>WordPress: Return Form ID - WordPress->>User: Confirm Form Creation -``` - -### Form Submission Flow - -```mermaid -sequenceDiagram - participant User - participant Frontend - participant REST_API - participant Validation - participant AntiSpam - participant Database - participant EmailSystem - - User->>Frontend: Fill Form - Frontend->>User: Validate Client-Side - User->>Frontend: Submit Form - Frontend->>REST_API: POST Form Data - - REST_API->>Validation: Validate Data - Validation->>REST_API: Validation Result - - REST_API->>AntiSpam: Check for Spam - AntiSpam->>REST_API: Spam Check Result - - alt Valid Submission - REST_API->>Database: Store Entry - Database->>REST_API: Entry ID - REST_API->>EmailSystem: Trigger Notifications - EmailSystem->>REST_API: Email Status - REST_API->>Frontend: Success Response - Frontend->>User: Show Confirmation - else Invalid Submission - REST_API->>Frontend: Error Response - Frontend->>User: Show Error Message - end -``` - -## Form Building Process - -SureForms uses WordPress's block editor as the foundation for its form builder. This approach provides several advantages: - -1. **Familiar Interface**: Users already familiar with WordPress's editor can easily adapt -2. **Native Integration**: Seamless integration with WordPress core functionality -3. **Extensibility**: Ability to leverage the block ecosystem - -### Block Structure - -Forms are composed of various block types: - -- **Container Blocks**: Group and organize form fields -- **Field Blocks**: Individual form inputs (text, email, checkbox, etc.) -- **Layout Blocks**: Control the visual arrangement of fields -- **Special Blocks**: Submit buttons, GDPR notices, etc. - -Each block has its own edit and save components, following the WordPress block API pattern. - -### Form Data Storage - -Form configurations are stored as custom post types with: - -- Post content: Stores the block structure (serialized blocks) -- Post meta: Stores form settings, styling options, and other configuration data - -## AI Integration - -SureForms features an AI-powered form generation system that allows users to create forms by describing them in natural language. - -```mermaid -graph TD - A[User Input] --> B[AI Middleware] - B --> C[AI Service] - C --> D[Form Structure Generation] - D --> E[Field Mapping] - E --> F[Block Generation] - F --> G[Form Preview] - G --> H[User Edits] - H --> I[Final Form] -``` - -### AI Form Generation Process - -1. User provides a description of the desired form -2. The description is sent to the AI middleware -3. AI analyzes the description and generates a structured form definition -4. The form definition is mapped to SureForms field types -5. Blocks are generated and inserted into the editor -6. User can review and modify the generated form - -## Form Submission and Processing - -### Submission Handling - -Form submissions are processed through a custom REST API endpoint that: - -1. Validates the form data against defined rules -2. Checks for spam using various methods (honeypot, reCAPTCHA, etc.) -3. Processes file uploads if present -4. Stores the submission in the database -5. Triggers email notifications -6. Returns appropriate success/error responses - -### Email Notifications - -The email notification system supports: - -- Multiple notification templates per form -- Dynamic content using smart tags -- HTML email formatting -- Conditional sending based on form data -- Custom headers (CC, BCC, Reply-To) - -```mermaid -graph TD - A[Form Submission] --> B[Email Notification System] - B --> C{Multiple Recipients?} - C -->|Yes| D[Process Each Recipient] - C -->|No| E[Single Email] - D --> F[Parse Email Template] - E --> F - F --> G[Replace Smart Tags] - G --> H[Apply Email Template] - H --> I[Send Email] - I --> J{Success?} - J -->|Yes| K[Log Success] - J -->|No| L[Log Failure] -``` - -## Security Features - -SureForms implements multiple security measures to protect against common vulnerabilities: - -### Anti-Spam Protection - -- **Honeypot Fields**: Hidden fields to catch automated submissions -- **reCAPTCHA Integration**: Multiple versions of Google reCAPTCHA -- **hCaptcha Support**: Alternative to reCAPTCHA -- **Cloudflare Turnstile**: Modern CAPTCHA alternative - -### Data Protection - -- **GDPR Compliance**: Options to enable GDPR-compliant data handling -- **Data Encryption**: Sensitive data can be encrypted in storage -- **Auto-Delete Entries**: Automatic deletion of entries after a specified period - -### Input Validation - -- **Client-Side Validation**: Immediate feedback to users -- **Server-Side Validation**: Thorough validation of all submitted data -- **File Upload Security**: Strict file type and size validation - -## Extension Points - -SureForms provides several extension points for developers: - -### WordPress Hooks - -```php -// Example of filter hook for email notification -add_filter('srfm_email_notification', function($parsed, $submission_data, $item, $form_data) { - // Modify email content or recipients - return $parsed; -}, 10, 4); - -// Example of action hook before form submission -add_action('srfm_before_submission', function($form_data) { - // Perform custom actions before form processing -}); -``` - -### JavaScript API - -```javascript -// Example of extending the form validation -window.sureFormsHooks.addFilter( - 'srfm.validation.rules', - 'my-plugin/custom-validation', - function(rules, fieldData) { - // Add custom validation rules - return rules; - } -); -``` - -## Technical Debt and Improvement Areas - -1. **Performance Optimization**: - - Lazy loading of form assets - - Improved caching strategies - -2. **Code Refactoring**: - - Further modularization of components - - Consistent naming conventions - -3. **Testing Coverage**: - - Increase unit and integration test coverage - - Automated UI testing - -## Development Guidelines - -### Coding Standards - -SureForms follows WordPress coding standards with some additional guidelines: - -- PHP: PSR-12 with WordPress-specific adaptations -- JavaScript: ESLint with WordPress configuration -- CSS: Follows WordPress CSS coding standards - -### Development Workflow - -1. **Feature Planning**: Document requirements and design -2. **Development**: Implement features with appropriate tests -3. **Code Review**: Peer review for quality assurance -4. **Testing**: Automated and manual testing -5. **Documentation**: Update technical and user documentation -6. **Release**: Version tagging and deployment - -### Version Control - -- Feature branches for new development -- Pull request workflow for code review -- Semantic versioning for releases - -## Conclusion - -SureForms represents a modern approach to WordPress form building, combining the power of the block editor with advanced features like AI form generation. Its modular architecture and extensive extension points make it both user-friendly and developer-friendly, while its focus on security and performance ensures a reliable experience for all users. - -The plugin continues to evolve with a focus on improving user experience, expanding integration capabilities, and optimizing performance across all devices and platforms. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 40ad8c0e7..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,165 +0,0 @@ -# SureForms - CLAUDE.md - -## Project Overview -SureForms is a WordPress form builder plugin (v2.5.1) by Brainstorm Force. AI-powered drag-and-drop form building using the Gutenberg block editor. - -- **Plugin slug:** `sureforms` | **Text domain:** `sureforms` -- **Prefix:** `SRFM_` (constants), `srfm` (slug), `sureforms_` (post types/DB) -- **Minimum PHP:** 7.4 | **Minimum WP:** 6.4 - -## Tech Stack -- **Backend:** PHP 7.4+ / WordPress Plugin API / Custom REST endpoints -- **Frontend:** React 18 + WordPress Gutenberg blocks -- **Styling:** TailwindCSS 3.4 + SASS + PostCSS -- **Build:** WordPress Scripts (Webpack 5) + Grunt -- **Testing:** PHPUnit 9 (unit) + Playwright (E2E) -- **Static Analysis:** PHPStan level 9 + PHPCS (WordPress-Extra) -- **Node:** 24.16.0 (Volta) | **UI Library:** @bsf/force-ui - -## Commands - -### Build -```bash -npm run start # Dev server with watch -npm run build # Full production build -npm run build:script # Webpack only -npm run build:sass # SASS only -``` - -### Lint & Format -```bash -npm run lint-js # ESLint -npm run lint-js:fix # ESLint auto-fix -npm run lint-css # Stylelint -composer lint # PHPCS -composer format # PHPCBF auto-fix -composer phpstan # PHPStan level 9 -composer insights # PHP Insights -``` - -### Test -```bash -composer test # PHPUnit -composer test:coverage # PHPUnit with coverage -npm run test:unit # JS unit tests -npm run play:up # Start WP test env -npm run play:run # Playwright E2E -npm run play:stop # Stop WP test env -``` - -### i18n -```bash -npm run makepot # Generate .pot -npm run i18n:po # Update .po from .pot -npm run i18n:mo # Compile .mo -npm run i18n:json # JSON translations -``` - -## Architecture - -### Directory Structure -``` -sureforms/ -├── sureforms.php # Entry point, constants -├── plugin-loader.php # Bootstrap -├── inc/ # PHP backend -│ ├── helper.php # Central utility (large — minimize changes) -│ ├── rest-api.php # REST endpoints -│ ├── form-submit.php # Submission handler -│ ├── entries.php # Entry management -│ ├── post-types.php # CPT registration -│ ├── blocks/ # PHP block rendering -│ ├── fields/ # Field type handlers -│ ├── database/ # Custom DB tables -│ ├── payments/ # Stripe integration -│ ├── ai-form-builder/ # AI form generation -│ ├── global-settings/ # Plugin settings -│ ├── page-builders/ # Elementor/Bricks compat -│ └── lib/ # Third-party (DO NOT modify) -├── src/ # React/JS source -│ ├── admin/ # Admin UI -│ ├── blocks/ # Gutenberg block JS -│ ├── components/ # Shared React components -│ ├── store/ # WordPress data store -│ ├── utils/ # JS utilities -│ └── srfm-controls/ # Custom block controls -├── assets/ # Compiled output (gitignored) -├── sass/ # SASS source -├── modules/ # Feature modules -├── templates/ # PHP templates -├── tests/ # PHPUnit + Playwright + Docker -└── languages/ # Translation files -``` - -### Webpack Aliases -``` -@Admin → src/admin/ @Blocks → src/blocks/ @Controls → src/srfm-controls/ -@Components → src/components/ @Utils → src/utils/ @Svg → assets/svg/ -@Attributes → src/blocks-attributes/ @Image → images/ @IncBlocks → inc/blocks/ -``` - -### Key Data Structures -- **Post Type:** `sureforms_form` — Forms as CPT with block content -- **Custom Tables:** `sureforms_entries`, `sureforms_payments` -- **REST Namespace:** Custom endpoints in `inc/rest-api.php` - -## Code Rules - -### PHP -- Follow WPCS. Use `sureforms` text domain for all translatable strings -- `@since x.x.x` on all new functions/classes (updated in release PRs) -- NEVER `echo` without escaping — use `esc_html()`, `esc_attr()`, `wp_kses_post()` -- NEVER use `$_GET/$_POST/$_REQUEST` directly — use `sanitize_text_field( wp_unslash( ... ) )` -- NEVER skip security verification on endpoints/AJAX handlers - - **Public form submission endpoints** use HMAC tokens via `Submit_Token::verify()` (`inc/submit-token.php`) — NOT nonces. These are cache-safe and session-independent. The `// phpcs:ignore WordPress.Security.NonceVerification.Missing` comment is expected on these handlers. - - **Admin/authenticated endpoints** still use traditional nonces (`wp_verify_nonce()` / `check_ajax_referer()`) -- NEVER skip capability checks — use `current_user_can()` before privileged operations -- NEVER use `wp_die()` in REST callbacks — use `WP_Error` with proper response codes -- NEVER hardcode table names — use `$wpdb->prefix . 'sureforms_entries'` -- ALWAYS use `wp_json_encode()`, `wp_remote_get/post()`, `gmdate()`, `wp_safe_redirect()` - -### JavaScript/React -- Follow WordPress Scripts ESLint config -- Use WordPress data stores (`useSelect`/`useDispatch`) — never raw fetch in blocks -- Import from `@wordpress/*` packages — never import React directly -- Use `__()` from `@wordpress/i18n` — never hardcode user-facing strings -- Use TailwindCSS utility classes; use `@bsf/force-ui` for admin UI -- NEVER use `dangerouslySetInnerHTML` — use `RawHTML` from `@wordpress/element` - -### General -- NEVER create files unless absolutely necessary — prefer editing existing files -- NEVER proactively create documentation files unless explicitly requested - -## Gotchas -- **Custom DB tables** introduced in v0.0.13 — migration is irreversible -- **PHPStan level 9** is strict; check baseline before adding new ignores -- **Grunt** still used alongside Webpack for CSS minification and release packaging -- **`inc/lib/`** is third-party code — do not lint or modify -- **Node 24.16.0** pinned via Volta — other versions may cause build issues - -## Verification Before Done -After code changes, verify before reporting done: - -**JS:** `npm run lint-js` → `npm run build:script` -**SASS:** `npm run build:sass` (if touched) - -Checklist: -- Re-read the diff for obvious issues -- No debug code left (`console.log`, `error_log`, `var_dump`) -- All new functions have `@since x.x.x` -- All user-facing strings use `__()` / `_e()` -- Security checks on new endpoints/handlers (HMAC token for public, nonce for admin) -- For significant changes, suggest `npm run play:run` - -## Self-Improvement Loop -- When corrected, add a rule to **Learned Rules** so the mistake never repeats -- When a pattern causes a bug or CI failure, document it immediately -- Periodically prune outdated rules - -## Learned Rules - -- NEVER flag `phpcs:ignore WordPress.Security.NonceVerification.Missing` as a security issue on public form submission handlers (form submit, field validation, Stripe/PayPal payment intents) — these use HMAC token verification (`Submit_Token::verify()`) instead of nonces for page-cache compatibility. The HMAC token is embedded in the form at render time and verified server-side without any session dependency. -- ALWAYS use `Submit_Token::generate($form_id)` / `Submit_Token::verify($token, $form_id)` for public-facing form security — nonces (`wp_verify_nonce`) are only for admin/authenticated endpoints. - -## Current Focus - diff --git a/COMPREHENSIVE_ANALYSIS.md b/COMPREHENSIVE_ANALYSIS.md deleted file mode 100644 index aed786d38..000000000 --- a/COMPREHENSIVE_ANALYSIS.md +++ /dev/null @@ -1,304 +0,0 @@ -# SureForms: Comprehensive Analysis - -This document provides a multi-perspective analysis of the SureForms WordPress plugin, examining it from software architecture, development, and product management viewpoints. - -## Table of Contents - -1. [Software Architecture Analysis](#software-architecture-analysis) -2. [Development Perspective](#development-perspective) -3. [Product Management Analysis](#product-management-analysis) -4. [Integration Capabilities](#integration-capabilities) -5. [Future Development Roadmap](#future-development-roadmap) - -## Software Architecture Analysis - -### Core Architecture - -SureForms follows a modular architecture pattern with clear separation of concerns. The plugin is built around several key components: - -```mermaid -graph TD - A[WordPress Core] --> B[SureForms Plugin] - B --> C[Plugin_Loader] - C --> D[Core Components] - D --> E[Post_Types] - D --> F[Form_Submit] - D --> G[Block_Patterns] - D --> H[Frontend_Assets] - D --> I[AI_Form_Builder] - D --> J[Rest_Api] - D --> K[Database] - K --> L[Custom Tables] - K --> M[Post Meta] -``` - -The architecture leverages WordPress's plugin system and follows object-oriented principles with extensive use of the singleton pattern for core components. - -### Data Flow Architecture - -The data flow within SureForms follows a clear pattern: - -```mermaid -sequenceDiagram - participant User - participant Frontend - participant BlockEditor - participant REST_API - participant Database - - User->>BlockEditor: Create Form - BlockEditor->>Database: Store Form Configuration - User->>Frontend: View & Submit Form - Frontend->>REST_API: Submit Form Data - REST_API->>Database: Store Submission - REST_API->>Frontend: Return Response -``` - -This separation ensures clean data handling and allows for extensibility at various points in the process. - -### Component Analysis - -#### Plugin Loader - -The `Plugin_Loader` class serves as the entry point for the plugin, handling initialization, autoloading, and hook registration. It follows the singleton pattern to ensure only one instance exists. - -#### Post Types - -The `Post_Types` class manages custom post types for forms and entries, including admin UI customizations, shortcode functionality, and metadata registration. - -#### Form Submit - -The `Form_Submit` class processes form submissions through REST API endpoints, handling validation, file uploads, anti-spam measures, and email notifications. - -#### Database Layer - -SureForms uses both WordPress custom post types and custom database tables: -- Custom post type `sureforms_form` for form configurations -- Custom table `{prefix}_srfm_entries` for form submissions - -This hybrid approach leverages WordPress's content management for forms while optimizing data storage for submissions. - -#### AI Integration - -The AI form builder component connects with external AI services through a middleware layer, translating natural language descriptions into structured form definitions. - -### Technical Debt Assessment - -Current areas of technical debt include: - -1. **Performance Optimization** - - Opportunity for more aggressive asset loading strategies - - Potential for improved caching mechanisms - -2. **Code Organization** - - Some components have grown large and could benefit from further modularization - - Inconsistent naming conventions in some areas - -3. **Testing Coverage** - - Limited automated testing for JavaScript components - - Opportunity for more comprehensive integration tests - -## Development Perspective - -### Code Quality - -The codebase demonstrates several positive qualities: - -- **Consistent Structure**: Well-organized directory structure -- **Modern Practices**: Use of namespaces, autoloading, and OOP principles -- **Documentation**: Comprehensive inline documentation -- **Separation of Concerns**: Clear boundaries between components - -Areas for improvement include: - -- **Testing Coverage**: Expanding unit and integration tests -- **Consistent Naming**: Standardizing naming conventions across all components -- **Performance Profiling**: Identifying and addressing performance bottlenecks - -### Development Workflow - -The development process appears to follow modern practices: - -- **Version Control**: Git-based workflow with feature branches -- **Code Review**: Evidence of peer review processes -- **Continuous Integration**: GitHub Actions for automated testing -- **Semantic Versioning**: Clear versioning strategy - -### Extension Points - -SureForms provides several extension mechanisms: - -1. **WordPress Hooks** - - Actions for process intervention - - Filters for data modification - -2. **JavaScript API** - - Client-side hooks for extending functionality - - Custom events for integration - -3. **Block API** - - Ability to register custom form field blocks - - Block filters for modifying existing blocks - -4. **Template System** - - Customizable templates for form rendering - - Theme override capabilities - -### Technology Stack - -The plugin leverages a modern technology stack: - -- **PHP**: Core server-side logic -- **JavaScript/React**: Block editor integration and frontend functionality -- **CSS/SCSS**: Styling with preprocessor support -- **MySQL**: Database storage -- **REST API**: Data communication -- **AI Services**: Form generation capabilities - -## Product Management Analysis - -### User Segments - -SureForms targets several distinct user segments: - -1. **Website Owners** - - Need: Simple form creation without technical knowledge - - Value: Intuitive interface, pre-designed templates - -2. **Designers** - - Need: Visually appealing forms that match site design - - Value: Design customization, aesthetic controls - -3. **Developers** - - Need: Extensible platform with clean APIs - - Value: Hooks, filters, and documentation - -4. **No-Code Professionals** - - Need: Advanced functionality without coding - - Value: Conditional logic, multi-step forms - -### Feature Analysis - -The feature set is well-aligned with user needs: - -| Feature | User Segment | Value Proposition | -|---------|--------------|-------------------| -| Block Editor Integration | All | Familiar, native WordPress experience | -| AI Form Generation | All | Rapid form creation with minimal effort | -| Mobile-First Design | All | Forms work well on all devices | -| Advanced Field Types | No-Code Professionals | Complex form capabilities without coding | -| Developer Hooks | Developers | Extensibility for custom needs | -| Visual Customization | Designers | Forms match site aesthetics | -| Anti-Spam Features | All | Protection from spam submissions | -| GDPR Compliance | All | Legal compliance and data protection | - -### Competitive Positioning - -SureForms differentiates itself through: - -1. **Native WordPress Integration**: Using the block editor rather than a custom interface -2. **AI-Powered Creation**: Faster form building through AI assistance -3. **Design Focus**: Emphasis on visual appeal and user experience -4. **Developer-Friendly**: Clean architecture and extension points - -### Market Fit - -The product demonstrates strong market fit by addressing several key pain points: - -1. **Complex Form Building**: Simplifying the process through AI and intuitive interface -2. **Design Limitations**: Providing modern styling out-of-the-box -3. **Mobile Responsiveness**: Ensuring forms work well on all devices -4. **Technical Barriers**: Enabling advanced features without coding requirements - -## Integration Capabilities - -### WordPress Ecosystem - -SureForms integrates well with the WordPress ecosystem: - -- **Themes**: Compatible with major themes including Astra, Kadence, GeneratePress -- **Page Builders**: Works with Elementor, Beaver Builder, Divi -- **Plugins**: Compatible with popular plugins like WooCommerce, Easy Digital Downloads - -### External Services - -The plugin offers integration with several external services: - -- **reCAPTCHA**: Anti-spam protection -- **hCaptcha**: Alternative CAPTCHA solution -- **Cloudflare Turnstile**: Modern CAPTCHA alternative -- **AI Services**: Form generation capabilities - -### API Architecture - -The API architecture facilitates integration: - -```mermaid -graph LR - A[WordPress] --> B[SureForms Core] - B --> C[REST API] - C --> D[External Integrations] - D --> E[CRM Systems] - D --> F[Email Marketing] - D --> G[Payment Processors] - D --> H[AI Services] -``` - -This architecture allows for flexible integration with various third-party services. - -## Future Development Roadmap - -Based on the analysis, several development priorities emerge: - -### Short-Term Priorities - -1. **Performance Optimization** - - Implement lazy loading for form assets - - Optimize database queries for large form submissions - -2. **Code Quality Improvements** - - Increase test coverage - - Standardize naming conventions - - Refactor larger components - -3. **Integration Expansion** - - Add more third-party service integrations - - Enhance existing integrations with deeper functionality - -### Medium-Term Priorities - -1. **Advanced AI Capabilities** - - Improve form generation accuracy - - Add form analytics and insights - -2. **Enhanced User Experience** - - Streamline the form building process - - Improve form submission handling - -3. **Developer Tools** - - Create comprehensive API documentation - - Develop example extensions - -### Long-Term Vision - -1. **Form Ecosystem** - - Develop a marketplace for extensions - - Create specialized form solutions for different industries - -2. **Enterprise Features** - - Advanced workflow automation - - Enhanced security and compliance features - -3. **SaaS Potential** - - Explore hosted form solution alongside WordPress plugin - - Develop cloud-based form management - -## Conclusion - -SureForms represents a modern approach to WordPress form building, combining native block editor integration with AI-powered form generation. Its architecture demonstrates good separation of concerns and extensibility, while its feature set addresses key user needs across multiple segments. - -The product is well-positioned in the competitive form builder market, with clear differentiation through its WordPress integration, AI capabilities, and design focus. Future development should focus on expanding integrations, enhancing AI capabilities, and improving performance to maintain this competitive advantage. - -From a technical perspective, the codebase is well-structured but would benefit from increased test coverage and some refactoring of larger components. The extension system provides good flexibility for developers, though more comprehensive documentation would enhance this further. - -Overall, SureForms shows strong potential for continued growth and adoption, particularly as the WordPress ecosystem continues to embrace the block editor and AI-powered tools become increasingly important in the website building process. diff --git a/PRODUCT_ANALYSIS.md b/PRODUCT_ANALYSIS.md deleted file mode 100644 index a6c4e595e..000000000 --- a/PRODUCT_ANALYSIS.md +++ /dev/null @@ -1,251 +0,0 @@ -# SureForms Product Analysis - -## Executive Summary - -SureForms is a modern WordPress form builder plugin that differentiates itself through its native WordPress integration, AI-powered form generation, and focus on user experience. This analysis examines the product from multiple perspectives to provide a comprehensive understanding of its strengths, challenges, and opportunities for growth. - -## Table of Contents - -1. [Product Overview](#product-overview) -2. [Market Analysis](#market-analysis) -3. [User Experience Analysis](#user-experience-analysis) -4. [Technical Analysis](#technical-analysis) -5. [Competitive Analysis](#competitive-analysis) -6. [SWOT Analysis](#swot-analysis) -7. [Growth Opportunities](#growth-opportunities) -8. [Recommendations](#recommendations) - -## Product Overview - -SureForms is a WordPress form builder plugin that leverages the native WordPress block editor (Gutenberg) to provide an intuitive form building experience. Key features include: - -- **Native WordPress Integration**: Built on top of the WordPress block editor -- **AI-Powered Form Generation**: Create forms quickly using AI suggestions -- **No-Code Flexibility**: Advanced features without requiring coding knowledge -- **Mobile-First Design**: Responsive forms that work across all devices -- **Modern UI/UX**: Clean, intuitive interface for both admins and users -- **Security Features**: Anti-spam protection, GDPR compliance, and data security -- **Extensibility**: Developer-friendly architecture with hooks and filters - -The plugin is currently at version 1.7.0 and requires WordPress 6.4+ and PHP 7.4+. - -## Market Analysis - -### Target Audience - -SureForms targets several key user segments: - -1. **Website Owners**: Individuals and businesses who manage their own WordPress websites and need to collect information from visitors -2. **Designers**: Professionals who value good design and want to create visually appealing forms -3. **Developers**: Technical users who need a developer-friendly solution with clean APIs and hooks -4. **No-Code Professionals**: Users who need advanced form features without coding skills - -### Market Positioning - -SureForms positions itself as a modern alternative to traditional form builders, emphasizing: - -1. **Seamless WordPress Integration**: Using the native block editor rather than a separate interface -2. **Design-First Approach**: Focusing on aesthetics and user experience -3. **AI-Powered Efficiency**: Leveraging AI to speed up form creation -4. **Simplicity Without Compromise**: Providing advanced features in an accessible way - -### Market Trends - -Several trends in the WordPress form builder market are relevant to SureForms: - -1. **Shift to Block Editor**: Growing adoption of Gutenberg and block-based themes -2. **AI Integration**: Increasing use of AI for content and design generation -3. **Mobile-First Design**: Continued emphasis on mobile responsiveness -4. **Privacy Compliance**: Growing importance of GDPR and other privacy regulations -5. **No-Code Movement**: Rising demand for powerful tools that don't require coding skills - -## User Experience Analysis - -### Form Creation Experience - -SureForms provides a streamlined form creation experience: - -- **Block-Based Building**: Drag-and-drop interface using familiar WordPress blocks -- **AI Form Generation**: Natural language prompts to quickly create form structures -- **Visual Editing**: Real-time preview of forms as they're built -- **Templates**: Pre-designed form templates for common use cases - -### Form Submission Experience - -The end-user experience for form submission is optimized for: - -- **Responsive Design**: Forms adapt to different screen sizes -- **Inline Validation**: Real-time feedback on form inputs -- **Accessibility**: Support for screen readers and keyboard navigation -- **Confirmation Messages**: Customizable success messages and redirects - -### Admin Experience - -The admin interface focuses on: - -- **Intuitive Management**: Easy access to forms, entries, and settings -- **Data Visualization**: Visual reporting of form submission data -- **Entry Management**: Tools for viewing, filtering, and exporting submissions -- **Email Notifications**: Customizable email alerts for new submissions - -## Technical Analysis - -### Architecture - -SureForms follows a modular architecture with clear separation of concerns: - -- **Core Plugin**: Handles initialization, hooks, and basic functionality -- **Block System**: Manages form field blocks and their behavior -- **Form Processing**: Handles form submissions and data storage -- **AI Integration**: Connects with AI services for form generation -- **Extension System**: Provides hooks and filters for customization - -### Performance - -The plugin is designed with performance in mind: - -- **Optimized Asset Loading**: CSS and JS loaded only when needed -- **Efficient Database Usage**: Custom tables with appropriate indexes -- **Caching Strategies**: Caching of frequently accessed data -- **Minimal Dependencies**: Limited reliance on external libraries - -### Security - -Security features include: - -- **Input Validation**: Thorough validation of all user inputs -- **Anti-Spam Measures**: Multiple options for spam prevention -- **Data Protection**: GDPR compliance features -- **File Upload Security**: Strict validation of uploaded files - -### Extensibility - -The plugin provides several extension points: - -- **WordPress Hooks**: Actions and filters for modifying behavior -- **JavaScript API**: Client-side hooks for extending functionality -- **Block API**: Ability to create custom form field blocks -- **Template System**: Customizable templates for form rendering - -## Competitive Analysis - -### Key Competitors - -1. **Gravity Forms**: Established premium form builder with extensive features -2. **WPForms**: User-friendly form builder with a focus on simplicity -3. **Formidable Forms**: Advanced form builder with data management features -4. **Ninja Forms**: Modular form builder with add-on marketplace -5. **Contact Form 7**: Simple, free form builder with large user base - -### Competitive Advantages - -SureForms differentiates itself through: - -1. **Native Block Editor Integration**: More seamless WordPress experience -2. **AI-Powered Form Creation**: Faster form building process -3. **Modern Design Focus**: Better out-of-the-box aesthetics -4. **Developer-Friendly Architecture**: Clean APIs and extension points -5. **Mobile-First Approach**: Better experience on mobile devices - -### Competitive Challenges - -Areas where competitors may have advantages: - -1. **Ecosystem Maturity**: Established competitors have larger add-on ecosystems -2. **Market Recognition**: Lower brand awareness as a newer entrant -3. **Feature Depth**: Some competitors have more specialized features -4. **Integration Breadth**: Fewer third-party service integrations - -## SWOT Analysis - -### Strengths - -- Native integration with WordPress block editor -- AI-powered form generation capabilities -- Modern, clean design aesthetic -- Mobile-first responsive approach -- Developer-friendly architecture -- Focus on user experience - -### Weaknesses - -- Newer entrant in a mature market -- Smaller ecosystem of extensions and add-ons -- Limited third-party integrations compared to competitors -- Requires newer versions of WordPress and PHP - -### Opportunities - -- Growing adoption of the WordPress block editor -- Increasing demand for AI-powered tools -- Rising importance of mobile-friendly forms -- Expanding market for no-code solutions -- Potential for vertical-specific form solutions - -### Threats - -- Established competitors with loyal user bases -- WordPress core changes affecting block editor functionality -- Potential AI regulation affecting AI form generation -- Market commoditization driving price competition -- Alternative form solutions outside WordPress ecosystem - -## Growth Opportunities - -### Product Development - -1. **Expanded AI Capabilities**: More sophisticated form generation and analysis -2. **Vertical-Specific Templates**: Forms tailored to specific industries or use cases -3. **Advanced Conditional Logic**: More powerful dynamic form behavior -4. **Enhanced Data Analysis**: Better visualization and reporting of form data -5. **Multi-Step Form Improvements**: More sophisticated multi-page form experiences - -### Market Expansion - -1. **Agency Partner Program**: Dedicated resources for agencies and developers -2. **Vertical Marketing**: Targeted solutions for specific industries -3. **Educational Content**: Tutorials and resources to drive adoption -4. **Integration Partnerships**: Collaborations with complementary services -5. **International Expansion**: Localization for key markets - -### Business Model - -1. **Freemium Model**: Free core plugin with premium add-ons or features -2. **Subscription Tiers**: Different service levels for different user needs -3. **White-Label Solutions**: Customizable versions for agencies and resellers -4. **Enterprise Offerings**: Custom solutions for larger organizations -5. **Marketplace**: Platform for third-party extensions - -## Recommendations - -### Short-Term (0-6 months) - -1. **Expand Integration Ecosystem**: Add connections to popular CRM and email marketing platforms -2. **Enhance AI Form Builder**: Improve accuracy and capabilities of AI form generation -3. **Develop Educational Content**: Create tutorials and documentation to drive adoption -4. **Optimize Performance**: Further improve loading speed and resource usage -5. **Implement User Feedback System**: Gather structured feedback from early adopters - -### Medium-Term (6-12 months) - -1. **Launch Partner Program**: Develop resources for agencies and developers -2. **Create Vertical Solutions**: Develop specialized templates for key industries -3. **Expand Block Library**: Add more specialized form field types -4. **Improve Analytics**: Enhance data visualization and reporting capabilities -5. **Develop Extension Marketplace**: Create platform for third-party add-ons - -### Long-Term (12+ months) - -1. **Develop Enterprise Features**: Add capabilities for larger organizations -2. **Explore SaaS Model**: Consider hosted form solution alongside WordPress plugin -3. **Implement Advanced AI**: Add predictive analytics and personalization -4. **Create Form Ecosystem**: Develop complementary products and services -5. **Explore International Markets**: Localize for key global markets - -## Conclusion - -SureForms represents a modern approach to WordPress form building, combining the power of the native block editor with AI-powered form generation. Its focus on user experience, design, and developer-friendly architecture positions it well in the competitive form builder market. - -By leveraging its strengths in WordPress integration and AI capabilities while addressing its challenges in market recognition and ecosystem development, SureForms has significant potential for growth. The recommended strategies focus on expanding integrations, enhancing AI capabilities, developing educational resources, and creating specialized solutions for key market segments. - -With continued development and strategic focus, SureForms can establish itself as a leading WordPress form solution for the modern web. diff --git a/TECHNICAL_OVERVIEW.md b/TECHNICAL_OVERVIEW.md deleted file mode 100644 index 9d2d44140..000000000 --- a/TECHNICAL_OVERVIEW.md +++ /dev/null @@ -1,393 +0,0 @@ -# SureForms Technical Overview - -## Introduction - -SureForms is a modern WordPress form builder plugin that leverages the native WordPress block editor (Gutenberg) to provide an intuitive, no-code form building experience. This document provides a comprehensive technical overview of the plugin's architecture, components, and functionality. - -## Table of Contents - -1. [Plugin Architecture](#plugin-architecture) -2. [Core Components](#core-components) -3. [Database Structure](#database-structure) -4. [Form Building System](#form-building-system) -5. [Form Submission Process](#form-submission-process) -6. [AI Integration](#ai-integration) -7. [Security Features](#security-features) -8. [Extension Points](#extension-points) -9. [Performance Considerations](#performance-considerations) -10. [Future Development](#future-development) - -## Plugin Architecture - -SureForms follows a modular architecture pattern with clear separation of concerns. The plugin is structured around several key components that interact through well-defined interfaces. - -### Directory Structure - -``` -sureforms/ -├── admin/ # Admin-specific functionality -├── api/ # REST API endpoints -├── assets/ # CSS, JS, and other assets -├── inc/ # Core functionality -│ ├── ai-form-builder/ # AI form generation -│ ├── blocks/ # Block registration -│ ├── compatibility/ # Theme/plugin compatibility -│ ├── database/ # Custom database tables -│ ├── email/ # Email notification system -│ ├── fields/ # Form field definitions -│ ├── global-settings/ # Global plugin settings -│ ├── lib/ # Third-party libraries -│ ├── page-builders/ # Page builder integrations -│ ├── single-form-settings/ # Form-specific settings -│ └── traits/ # Shared PHP traits -├── languages/ # Internationalization files -├── modules/ # Feature modules -│ ├── gutenberg/ # Gutenberg integration -│ └── quick-action-sidebar/ # Quick action UI -├── src/ # JavaScript source files -│ ├── admin/ # Admin JS -│ ├── blocks/ # Block JS -│ ├── components/ # Reusable React components -│ ├── lib/ # JS libraries -│ ├── store/ # Redux store -│ ├── styles/ # SCSS files -│ └── utils/ # Utility functions -└── templates/ # Template files -``` - -### Initialization Flow - -The plugin follows a structured initialization process: - -1. `sureforms.php` - Main plugin file that defines constants and includes the plugin loader -2. `plugin-loader.php` - Initializes the plugin and registers core components -3. Core components are loaded through the `Plugin_Loader` class using autoloading -4. WordPress hooks are registered to integrate with the WordPress lifecycle - -```mermaid -graph TD - A[sureforms.php] --> B[plugin-loader.php] - B --> C[Plugin_Loader class] - C --> D[Register hooks] - C --> E[Autoload classes] - C --> F[Load textdomain] - D --> G[Init core components] - G --> H[Post_Types] - G --> I[Form_Submit] - G --> J[Block_Patterns] - G --> K[Frontend_Assets] - G --> L[AI_Form_Builder] - G --> M[Rest_Api] -``` - -## Core Components - -### Plugin_Loader - -The central initialization class that bootstraps the plugin, registers hooks, and loads core components. It follows a singleton pattern to ensure only one instance exists. - -```php -class Plugin_Loader { - private static $instance = null; - - public static function get_instance() { - if (null === self::$instance) { - self::$instance = new self(); - do_action('srfm_core_loaded'); - } - return self::$instance; - } - - // Initialization methods... -} -``` - -### Post_Types - -Registers and manages custom post types for forms and entries. Handles the admin UI customizations for these post types. - -Key features: -- Registers `sureforms_form` post type for storing form configurations -- Adds custom columns to the admin list view -- Implements shortcode functionality -- Manages form metadata registration - -### Form_Submit - -Processes form submissions through REST API endpoints, validates input data, handles file uploads, processes anti-spam measures, and triggers email notifications. - -Key features: -- Registers `/sureforms/v1/submit-form` REST API endpoint -- Validates form submissions against defined rules -- Processes file uploads with security checks -- Handles anti-spam measures (honeypot, reCAPTCHA, hCaptcha, Cloudflare Turnstile) -- Stores submissions in the database -- Triggers email notifications - -### AI_Form_Builder - -Integrates with AI services to generate form structures based on user prompts. Communicates with middleware services to process AI requests. - -Key features: -- Processes natural language descriptions into form structures -- Maps AI-generated fields to SureForms field types -- Generates block structures for the editor - -## Database Structure - -SureForms uses both WordPress custom post types and custom database tables to store data efficiently. - -### Custom Post Types - -- **sureforms_form**: Stores form configurations - - Post content: Serialized blocks representing the form structure - - Post meta: Form settings, styling options, and other configuration data - -### Custom Database Tables - -- **{prefix}_srfm_entries**: Stores form submissions - - ID: Primary key - - form_id: Associated form ID - - user_id: Submitter's user ID (if logged in) - - form_data: JSON-encoded submission data - - submission_info: Browser, IP, and device information - - status: Entry status (read, unread, trash) - - logs: Activity logs for the entry - - created_at: Submission timestamp - -```mermaid -erDiagram - FORMS ||--o{ ENTRIES : "has" - FORMS { - int ID - string post_title - text post_content - string post_status - datetime post_date - } - ENTRIES { - int ID - int form_id - int user_id - json form_data - json submission_info - string status - json logs - datetime created_at - } - FORMS ||--o{ FORM_META : "has" - FORM_META { - int meta_id - int post_id - string meta_key - mixed meta_value - } -``` - -## Form Building System - -SureForms uses WordPress's block editor as the foundation for its form builder. - -### Block Structure - -Forms are composed of various block types: - -- **Container Blocks**: Group and organize form fields -- **Field Blocks**: Individual form inputs (text, email, checkbox, etc.) -- **Layout Blocks**: Control the visual arrangement of fields -- **Special Blocks**: Submit buttons, GDPR notices, etc. - -Each block has its own edit and save components, following the WordPress block API pattern. - -### Block Registration - -Blocks are registered using the WordPress block registration API: - -```javascript -registerBlockType('sureforms/input', { - title: __('Text Field', 'sureforms'), - icon: 'text', - category: 'sureforms', - attributes: { - // Block attributes - }, - edit: Edit, - save: Save -}); -``` - -### Form Rendering - -Forms are rendered on the frontend using a combination of server-side rendering and client-side JavaScript: - -1. The form shortcode or block triggers the `Generate_Form_Markup::get_form_markup()` method -2. The method retrieves the form configuration and generates the HTML markup -3. Frontend JavaScript initializes the form functionality (validation, submission, etc.) - -## Form Submission Process - -### Client-Side Flow - -1. User fills out the form -2. Client-side validation checks for errors -3. Form data is collected and serialized -4. AJAX request is sent to the REST API endpoint -5. Response is processed and appropriate feedback is shown to the user - -### Server-Side Flow - -1. REST API endpoint receives the form submission -2. Data is validated and sanitized -3. Anti-spam checks are performed -4. If GDPR compliance is enabled, appropriate data handling is applied -5. Submission is stored in the database (unless configured not to) -6. Email notifications are triggered -7. Response is sent back to the client - -```mermaid -sequenceDiagram - participant User - participant Browser - participant REST_API - participant Database - participant Email - - User->>Browser: Fill form - Browser->>Browser: Validate input - User->>Browser: Submit form - Browser->>REST_API: POST /sureforms/v1/submit-form - REST_API->>REST_API: Validate data - REST_API->>REST_API: Anti-spam check - REST_API->>Database: Store submission - REST_API->>Email: Send notifications - REST_API->>Browser: Return response - Browser->>User: Show confirmation -``` - -## AI Integration - -SureForms features an AI-powered form generation system that allows users to create forms by describing them in natural language. - -### AI Form Generation Process - -1. User provides a description of the desired form -2. The description is sent to the AI middleware -3. AI analyzes the description and generates a structured form definition -4. The form definition is mapped to SureForms field types -5. Blocks are generated and inserted into the editor -6. User can review and modify the generated form - -### AI Middleware - -The AI middleware acts as a bridge between SureForms and the AI service: - -1. Receives the form description from SureForms -2. Processes the description using AI models -3. Returns a structured form definition -4. Handles authentication and rate limiting - -## Security Features - -SureForms implements multiple security measures to protect against common vulnerabilities: - -### Anti-Spam Protection - -- **Honeypot Fields**: Hidden fields to catch automated submissions -- **reCAPTCHA Integration**: Multiple versions of Google reCAPTCHA -- **hCaptcha Support**: Alternative to reCAPTCHA -- **Cloudflare Turnstile**: Modern CAPTCHA alternative - -### Data Protection - -- **GDPR Compliance**: Options to enable GDPR-compliant data handling -- **Data Encryption**: Sensitive data can be encrypted in storage -- **Auto-Delete Entries**: Automatic deletion of entries after a specified period - -### Input Validation - -- **Client-Side Validation**: Immediate feedback to users -- **Server-Side Validation**: Thorough validation of all submitted data -- **File Upload Security**: Strict file type and size validation - -## Extension Points - -SureForms provides several extension points for developers: - -### WordPress Hooks - -```php -// Example of filter hook for email notification -add_filter('srfm_email_notification', function($parsed, $submission_data, $item, $form_data) { - // Modify email content or recipients - return $parsed; -}, 10, 4); - -// Example of action hook before form submission -add_action('srfm_before_submission', function($form_data) { - // Perform custom actions before form processing -}); -``` - -### JavaScript API - -```javascript -// Example of extending the form validation -window.sureFormsHooks.addFilter( - 'srfm.validation.rules', - 'my-plugin/custom-validation', - function(rules, fieldData) { - // Add custom validation rules - return rules; - } -); -``` - -## Performance Considerations - -SureForms is designed with performance in mind: - -### Asset Loading - -- CSS and JavaScript assets are loaded only when needed -- Assets are minified and optimized for production -- Critical CSS is inlined for faster rendering - -### Database Optimization - -- Custom database tables with appropriate indexes -- Efficient queries with proper WHERE clauses -- Caching of frequently accessed data - -### Form Rendering - -- Server-side rendering for initial form display -- Progressive enhancement for JavaScript features -- Lazy loading of heavy components - -## Future Development - -Planned enhancements for future versions: - -1. **Enhanced AI Capabilities** - - More sophisticated form generation - - AI-powered form analytics - -2. **Advanced Integrations** - - More third-party service integrations - - Improved CRM connections - -3. **Performance Optimizations** - - Further asset optimization - - Enhanced caching strategies - -4. **Accessibility Improvements** - - Better screen reader support - - Keyboard navigation enhancements - -5. **Developer Tools** - - More comprehensive API documentation - - Additional extension points - -## Conclusion - -SureForms represents a modern approach to WordPress form building, combining the power of the block editor with advanced features like AI form generation. Its modular architecture and extensive extension points make it both user-friendly and developer-friendly, while its focus on security and performance ensures a reliable experience for all users. diff --git a/bin/build-zip.sh b/bin/build-zip.sh deleted file mode 100644 index 0bf38a51d..000000000 --- a/bin/build-zip.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -# Exit if any command fails. -set -e - -# Enable nicer messaging for build status. -BLUE_BOLD='\033[1;34m'; -GREEN_BOLD='\033[1;32m'; -RED_BOLD='\033[1;31m'; -YELLOW_BOLD='\033[1;33m'; -COLOR_RESET='\033[0m'; -error () { - echo -e "\n${RED_BOLD}$1${COLOR_RESET}\n" -} -status () { - echo -e "\n${BLUE_BOLD}$1${COLOR_RESET}\n" -} -success () { - echo -e "\n${GREEN_BOLD}$1${COLOR_RESET}\n" -} -warning () { - echo -e "\n${YELLOW_BOLD}$1${COLOR_RESET}\n" -} - - -status "💃 Time to build the plugin ZIP file 🕺" - -if [ ! -d "artifact" ]; then - mkdir "artifact" -fi - - -# Copy files for zip. -rsync -rc --delete --exclude-from ".distignore" "./" "artifact/sureforms" - -# Go to directory -cd artifact - -# Create a zip copied files. -zip -r sureforms.zip "./sureforms" - -if [ "no-clean" != "$1" ]; then - # Removed copied files folder. - rm -rf sureforms -fi - -success "Done. Your sureforms zip is ready..! 🎉" diff --git a/bin/checkout-and-build b/bin/checkout-and-build deleted file mode 100755 index c7529e42e..000000000 --- a/bin/checkout-and-build +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# Script to checkout a branch and build the project - -# Check if branch name is provided as an argument -if [ -z "$1" ]; then - echo "Error: No branch name provided." - echo "Usage: ./checkout-and-build.sh " - exit 1 -fi - -# Assign the first argument to a variable -branch_name=$1 - -# Checkout the specified branch -git checkout "$branch_name" - -# pull the latest version -git pull origin "$branch_name" - -# Run the build command -npm run build diff --git a/bin/i18n.sh b/bin/i18n.sh deleted file mode 100644 index bd71a862a..000000000 --- a/bin/i18n.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash - -# Run release script -#bash bin/i18n.sh -# -# Generates/updates the .pot, .po, .mo and JS .json translation files for the -# BSF Top 20 languages standard. Run at release time. New locales are created -# automatically via msginit, so the .po files do not need to be committed. - -# Always run from the plugin root so relative paths resolve. -cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1 - -# Target locales (BSF Top 20). Format: : -LANGS=( - "de_DE:de_DE" "es_ES:es_ES" "fr_FR:fr_FR" "it_IT:it_IT" "nl_NL:nl_NL" - "pl_PL:pl_PL" "pt_PT:pt_PT" "id_ID:id_ID" "pt_BR:pt_BR" "ru_RU:ru_RU" - "tr_TR:tr_TR" "ja:ja_JP" "zh_CN:zh_CN" "ar:ar" "sv_SE:sv_SE" - "vi:vi_VN" "he_IL:he_IL" "th:th_TH" "el:el_GR" "cs_CZ:cs_CZ" -) - -# Run textdomain updates and generate the POT file -echo "Running textdomain update and POT file generation..." -npm run makepot - -# Create a .po for any target locale that does not exist yet. -echo "Ensuring .po files exist for all target locales..." -for entry in "${LANGS[@]}"; do - file="${entry%%:*}"; loc="${entry##*:}" - po="languages/sureforms-${file}.po" - if [ ! -f "$po" ]; then - echo " creating $po ($loc)" - msginit --input=languages/sureforms.pot --output="$po" --locale="$loc" --no-translator - fi -done - -# Update PO files -echo "Updating PO files..." -npm run i18n:po - -# Translate using GPT-PO (only empty msgstr entries are filled). -echo "Translating PO files using GPT-PO..." -npm run i18n:gptpo:nl -npm run i18n:gptpo:fr -npm run i18n:gptpo:de -npm run i18n:gptpo:es -npm run i18n:gptpo:it -npm run i18n:gptpo:pt -npm run i18n:gptpo:pl -npm run i18n:gptpo:id -npm run i18n:gptpo:ptbr -npm run i18n:gptpo:ru -npm run i18n:gptpo:tr -npm run i18n:gptpo:ja -npm run i18n:gptpo:zh -npm run i18n:gptpo:ar -npm run i18n:gptpo:sv -npm run i18n:gptpo:vi -npm run i18n:gptpo:he -npm run i18n:gptpo:th -npm run i18n:gptpo:el -npm run i18n:gptpo:cs - -# Update PO files again after translation -echo "Updating PO files again..." -npm run i18n:po - -# Generate MO files -echo "Generating MO files..." -npm run i18n:mo - -# Generate JSON translation files -echo "Generating JSON translation files..." -npm run i18n:json - -echo "All commands executed successfully." diff --git a/docs/01-getting-started/GETTING-STARTED.md b/docs/01-getting-started/GETTING-STARTED.md deleted file mode 100644 index a9c0b0c34..000000000 --- a/docs/01-getting-started/GETTING-STARTED.md +++ /dev/null @@ -1,647 +0,0 @@ -# Getting Started with SureForms Development - -> **Goal:** Get productive in less than 30 minutes -> **Audience:** New developers, contractors, open-source contributors -> **Prerequisites:** Basic WordPress, PHP, and React knowledge - -## Prerequisites - -Before you begin, ensure you have: - -- **PHP 7.4+** with extensions: mysqli, mbstring, curl, json -- **Composer 2.3+** for PHP dependencies -- **Node.js 18.15+** (check with `node --version`, use nvm: `nvm use`) -- **npm** (comes with Node.js) -- **Git** for version control -- **Code editor** (VS Code recommended with extensions: PHP Intelephense, ESLint, Prettier) -- **Docker** (optional, for wp-env local environment) - ---- - -## Quick Start (5 Minutes) - -### 1. Clone Repository -```bash -git clone https://github.com/brainstormforce/sureforms.git -cd sureforms -``` - -### 2. Install Dependencies -```bash -# PHP dependencies -composer install - -# JavaScript dependencies -npm install -``` - -This installs: -- **PHP:** Action Scheduler, NPS Survey, Analytics libraries -- **JavaScript:** React 18, WordPress scripts, TailwindCSS, Webpack, testing tools - -### 3. Build Assets -```bash -npm run build -``` - -This compiles: -- JavaScript bundles (Webpack → assets/build/) -- CSS from SASS (sass/ → assets/css/) -- Minified production files - -**Time:** ~2-3 minutes - -### 4. Start Local Environment - -**Option A: Using wp-env (Recommended)** -```bash -npm run play:up -``` - -This creates a Docker-based WordPress installation: -- **URL:** http://localhost:8888 -- **Admin:** http://localhost:8888/wp-admin -- **Username:** admin -- **Password:** password - -**Option B: Manual WordPress Setup** -1. Install WordPress locally (MAMP, Local by Flywheel, or XAMPP) -2. Symlink or copy plugin to `wp-content/plugins/sureforms` -3. Activate plugin in WordPress admin - -### 5. Activate Plugin - -Visit http://localhost:8888/wp-admin/plugins.php and activate "SureForms". - -**You're now ready to develop!** 🎉 - ---- - -## Project Structure Overview - -``` -sureforms/ -├── sureforms.php # Main plugin file (entry point) -├── plugin-loader.php # Bootstrap and autoloader -│ -├── inc/ # PHP backend logic (176+ files) -│ ├── form-submit.php # Form submission handler -│ ├── post-types.php # Custom post type registration -│ ├── rest-api.php # REST API endpoints -│ ├── helper.php # Utility functions -│ ├── blocks/ # Gutenberg block PHP classes -│ │ ├── base.php # Abstract base for blocks -│ │ ├── register.php # Block registration -│ │ ├── input/ # Text input block -│ │ └── [14+ more blocks] -│ ├── database/ # Custom database layer -│ │ ├── base.php # Abstract CRUD operations -│ │ ├── register.php # Table initialization -│ │ └── tables/ -│ │ ├── entries.php # Form submissions table -│ │ └── payments.php # Payment transactions table -│ └── [many more directories] -│ -├── src/ # JavaScript/React source -│ ├── blocks/ # Gutenberg block components -│ │ ├── blocks.js # Block registration entry -│ │ ├── input/ # Input block React component -│ │ │ ├── edit.js # Editor interface -│ │ │ ├── save.js # Frontend save (usually null) -│ │ │ └── block.json # Block metadata -│ │ └── [14+ more blocks] -│ ├── admin/ # Admin dashboard React apps -│ │ ├── dashboard/ # Forms listing -│ │ ├── settings/ # Settings page -│ │ ├── entries/ # Entry management -│ │ └── single-form-settings/ # Form editor -│ ├── components/ # Reusable React components -│ ├── store/ # WordPress Data Store (Redux) -│ └── utils/ # Helper functions -│ -├── assets/ # Compiled build output -│ ├── build/ # Webpack bundles -│ ├── css/ # Compiled CSS -│ └── js/ # Compiled JavaScript -│ -├── sass/ # SCSS source files -├── tests/ # PHPUnit and Playwright tests -├── webpack.config.js # Build configuration -├── package.json # npm dependencies and scripts -└── composer.json # PHP dependencies -``` - -### Key Entry Points - -**PHP Initialization:** -``` -sureforms.php (defines constants) - → plugin-loader.php (Plugin_Loader class) - → Autoloader registration - → WordPress hooks: plugins_loaded, init, admin_init -``` - -**JavaScript Entry:** -- **Blocks:** [src/blocks/index.js](../../src/blocks/index.js) -- **Admin Pages:** [src/admin/{page}/index.js](../../src/admin/) - ---- - -## Development Workflow - -### Daily Development - -**1. Start Development Server** -```bash -npm start -``` - -This starts Webpack in watch mode: -- Automatically rebuilds on file changes -- Hot reload for React components -- Source maps for debugging - -**2. Make Changes** -- Edit PHP files in `inc/` or `admin/` -- Edit React components in `src/` -- Edit styles in `sass/` - -**3. See Changes** -- **Admin:** Refresh browser (Cmd/Ctrl + R) -- **Frontend:** Hard refresh (Cmd/Ctrl + Shift + R) to clear cache - -### Before Committing - -**CRITICAL: Always run these commands before committing** - -```bash -# 1. Fix JavaScript linting issues -npm run lint-js:fix - -# 2. Fix CSS linting issues -npm run lint-css:fix - -# 3. Production build (REQUIRED) -npm run build - -# 4. Check PHP coding standards -composer phpcs - -# 5. Run static analysis -composer phpstan - -# 6. Run tests (optional but recommended) -composer test -npm run test:e2e -``` - -**If all pass, commit:** -```bash -git add . -git commit -m "feat: your change description" -git push -``` - -### Commit Message Format - -Follow [Conventional Commits](https://www.conventionalcommits.org/): - -``` -feat: add date picker field block -fix: resolve form submission nonce error -docs: update API endpoint documentation -style: format code with PHPCS -refactor: simplify validation logic -test: add unit tests for Helper class -chore: update dependencies -``` - ---- - -## Common Development Tasks - -### Task 1: Create a Test Form - -**Time:** 5 minutes - -1. Navigate to **SureForms → Add New** in WordPress admin -2. Block editor opens with empty form -3. Add fields by clicking "+" button: - - Add "Input" block → Configure as "Name" - - Add "Email" block → Configure as "Email Address" - - Add "Textarea" block → Configure as "Message" -4. Click "Publish" -5. View form on frontend or use shortcode `[sureforms id="{ID}"]` - -### Task 2: Submit a Test Form - -**Time:** 2 minutes - -1. Visit form on frontend -2. Fill out fields -3. Click "Submit" -4. Check **SureForms → Entries** to see submission - -### Task 3: Add a New Setting to Existing Block - -**Time:** 30-45 minutes -**Difficulty:** Easy -**Files Modified:** 2 - -**Example: Add "Max Length" setting to Input block** - -**Step 1:** Update [src/blocks/input/edit.js](../../src/blocks/input/edit.js) - -Find the `` section and add: -```javascript -import { TextControl } from '@wordpress/components'; - -// Inside InspectorControls > PanelBody - setAttributes({ maxLength: parseInt(value) })} - help={__('Maximum number of characters allowed', 'sureforms')} -/> -``` - -**Step 2:** Update [src/blocks/input/block.json](../../src/blocks/input/block.json) - -Add to `attributes` object: -```json -"maxLength": { - "type": "number", - "default": 0 -} -``` - -**Step 3:** Build and test -```bash -npm run build -# Refresh block editor -# Test setting in Inspector panel -``` - -**Step 4:** Update PHP rendering (if needed) - -Edit [inc/blocks/input/class-input.php](../../inc/blocks/input/class-input.php) to use the attribute: -```php -$max_length = $attributes['maxLength'] ?? 0; - -// In HTML output - 0): ?> - maxlength="" - -/> -``` - -### Task 4: Debug a Form Submission Issue - -**Time:** 10-20 minutes - -**Symptoms:** Form submission fails silently, no error message - -**Debug Steps:** - -1. **Check Browser Console** (F12 → Console tab) - - Look for JavaScript errors - - Check Network tab for failed AJAX requests - - Look for 403 (nonce error) or 500 (server error) - -2. **Check PHP Error Logs** -```bash -# If using wp-env -npm run play:logs - -# Or tail WordPress debug log -tail -f wp-content/debug.log -``` - -3. **Enable WordPress Debug Mode** (wp-config.php) -```php -define('WP_DEBUG', true); -define('WP_DEBUG_LOG', true); -define('WP_DEBUG_DISPLAY', false); -define('SCRIPT_DEBUG', true); -``` - -4. **Test REST API Directly** -```bash -# Get nonce from browser (inspect form, look for data-nonce attribute) -curl -X POST "http://localhost:8888/wp-json/sureforms/v1/submit-form" \ - -H "X-WP-Nonce: YOUR_NONCE_HERE" \ - -H "Content-Type: application/json" \ - -d '{"form_id":123,"field_data":{"email":{"value":"test@example.com"}}}' -``` - -5. **Common Fixes:** - - **Nonce expired:** Refresh page - - **Spam protection:** Disable reCAPTCHA temporarily - - **PHP error:** Check debug.log for syntax errors - - **REST API disabled:** Check .htaccess or security plugins - ---- - -## Testing Your Changes - -### Unit Tests (PHP) - -```bash -# Run all PHPUnit tests -composer test - -# Run specific test file -./vendor/bin/phpunit tests/unit/test-helper.php - -# Generate coverage report -composer test:coverage -``` - -### E2E Tests (Playwright) - -```bash -# Run all E2E tests -npm run test:e2e - -# Run specific test -npx playwright test tests/play/specs/basic-form-test-submit-text-field.spec.js - -# Run in headed mode (see browser) -npx playwright test --headed - -# Debug mode -npx playwright test --debug -``` - -### Manual Testing Checklist - -After making changes, test: -- [ ] Form appears in block editor -- [ ] Settings panel works -- [ ] Form renders on frontend -- [ ] Form submission works -- [ ] Entry appears in admin -- [ ] Email notification sent (if configured) -- [ ] No console errors -- [ ] No PHP errors in debug.log - ---- - -## Debugging Tips - -### Enable Debugging - -**WordPress Debug Mode** (wp-config.php): -```php -define('WP_DEBUG', true); // Enable debug mode -define('WP_DEBUG_LOG', true); // Log to wp-content/debug.log -define('WP_DEBUG_DISPLAY', false); // Don't display errors on page -define('SCRIPT_DEBUG', true); // Load unminified JS/CSS -define('SAVEQUERIES', true); // Log database queries -``` - -**Plugin Debug Mode** (wp-config.php): -```php -define('SRFM_DEBUG', true); // Load unminified assets -``` - -### View Logs - -**PHP Errors:** -```bash -tail -f wp-content/debug.log -``` - -**JavaScript Console:** -- Open browser DevTools (F12) -- Console tab shows errors and logs -- Network tab shows AJAX requests - -### Common Issues - -#### Issue: "Class not found" Error -**Cause:** Namespace doesn't match file path -**Fix:** Ensure `SRFM\Inc\Blocks\Input` maps to `inc/blocks/input/class-input.php` - -#### Issue: Blocks Don't Appear in Editor -**Cause:** JavaScript not rebuilt -**Fix:** -```bash -npm run build -# Hard refresh browser (Cmd/Ctrl + Shift + R) -``` - -#### Issue: Form Submission Fails -**Cause:** Nonce expired or invalid -**Fix:** Refresh page to get new nonce - -#### Issue: Styling Not Applied -**Cause:** CSS not compiled or cached -**Fix:** -```bash -npm run build:sass -# Hard refresh browser -``` - -#### Issue: PHP Syntax Error After Edit -**Cause:** Syntax mistake in PHP file -**Fix:** Check debug.log, fix syntax, refresh - ---- - -## Next Steps - -Now that you're set up, here's how to continue learning: - -### 1. Read Core Documentation (2-3 hours) - -**Essential:** -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide with constraints and patterns -- [ARCHITECTURE.md](../02-architecture/ARCHITECTURE.md) - System design overview - -**Important:** -- [CODING-STANDARDS.md](../03-development/CODING-STANDARDS.md) - Code style rules -- [DATABASE-SCHEMA.md](../04-backend/DATABASE-SCHEMA.md) - Data model -- [FRONTEND-GUIDE.md](../05-frontend/FRONTEND-GUIDE.md) - React/Gutenberg patterns - -### 2. Explore the Codebase (2-3 hours) - -**Follow the Request Flow:** - -**Admin Page Load:** -``` -1. WordPress loads: admin.php?page=sureforms_forms -2. admin/admin.php → render_forms_page() -3. Outputs:

-4. React app mounts from assets/build/forms.js -5. Fetches data from REST API -``` - -**Form Submission:** -``` -1. User fills form → formSubmit.js validates -2. AJAX POST → /wp-json/sureforms/v1/submit-form -3. inc/form-submit.php → handle_form_submission() -4. Validates → Saves to wp_srfm_entries table -5. Sends email notifications -6. Returns success response -``` - -### 3. Study Example Implementations - -**Reference Block:** [inc/blocks/input/](../../inc/blocks/input/) -- See complete block implementation -- PHP class extends Base -- React component structure -- Attribute schema in block.json - -**Reference React App:** [src/admin/dashboard/](../../src/admin/dashboard/) -- React Query for data fetching -- React Router for navigation -- Component structure - -### 4. Make Your First Contribution - -**Choose from "Good First Issue" labels:** -- Add documentation -- Fix typos -- Add unit tests -- Improve error messages -- Add new field setting - -**Contribution Process:** -1. Fork repository -2. Create feature branch: `git checkout -b feat/your-feature` -3. Make changes -4. Run tests and linters -5. Commit with conventional commit message -6. Push and create pull request -7. Address review feedback - ---- - -## Getting Help - -### Documentation -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide -- [docs/](../) - Full documentation -- [README.md](../../README.md) - Plugin overview - -### Community -- **GitHub Issues:** https://github.com/brainstormforce/sureforms/issues -- **Pull Requests:** https://github.com/brainstormforce/sureforms/pulls - -### Code Questions - -**When asking for help, include:** -1. What you're trying to do -2. What you've tried -3. Error messages (browser console + PHP debug.log) -4. Code snippets -5. Environment (WordPress version, PHP version, plugin version) - ---- - -## Development Tools - -### Recommended VS Code Extensions - -- **PHP Intelephense** - PHP intelligence and autocomplete -- **ESLint** - JavaScript linting -- **Prettier** - Code formatting -- **EditorConfig** - Consistent editor config -- **WordPress Snippets** - WordPress function snippets -- **GitLens** - Git integration -- **Auto Rename Tag** - HTML tag renaming -- **Path Intellisense** - File path autocomplete - -### Browser Extensions - -- **React Developer Tools** - Inspect React components -- **Redux DevTools** - Inspect WordPress Data Store state - -### Command Line Tools - -```bash -# Install WP-CLI globally (optional but helpful) -curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -chmod +x wp-cli.phar -sudo mv wp-cli.phar /usr/local/bin/wp - -# Useful WP-CLI commands -wp plugin list -wp post list --post_type=sureforms_form -wp cache flush -``` - ---- - -## Keyboard Shortcuts (Block Editor) - -| Action | Mac | Windows | -|--------|-----|---------| -| Save | Cmd + S | Ctrl + S | -| Add block | / | / | -| Duplicate block | Cmd + Shift + D | Ctrl + Shift + D | -| Delete block | Shift + Alt + Z | Shift + Alt + Z | -| Block settings | Cmd + Shift + , | Ctrl + Shift + , | -| Undo | Cmd + Z | Ctrl + Z | -| Redo | Cmd + Shift + Z | Ctrl + Shift + Z | - ---- - -## Quick Reference - -### Important npm Scripts - -```bash -npm start # Watch mode with hot reload -npm run build # Production build -npm run build:script # JavaScript only -npm run build:sass # CSS only -npm run lint-js # Check JavaScript -npm run lint-js:fix # Fix JavaScript -npm run lint-css # Check CSS -npm run lint-css:fix # Fix CSS -npm test:unit # JavaScript unit tests -npm run test:e2e # Playwright E2E tests -npm run play:up # Start wp-env -npm run play:stop # Stop wp-env -npm run play:down # Destroy wp-env -npm run makepot # Generate translation template -``` - -### Important Composer Scripts - -```bash -composer install # Install dependencies -composer test # Run PHPUnit tests -composer test:coverage # Generate coverage report -composer phpcs # Check coding standards -composer phpcbf # Fix coding standards -composer phpstan # Static analysis -composer phpinsights # Code quality metrics -``` - -### Important File Paths - -| Purpose | Path | -|---------|------| -| Main plugin file | sureforms.php | -| Plugin loader | plugin-loader.php | -| Form submission | inc/form-submit.php | -| REST API | inc/rest-api.php | -| Helper functions | inc/helper.php | -| Block base class | inc/blocks/base.php | -| Database entries | inc/database/tables/entries.php | -| Webpack config | webpack.config.js | -| TailwindCSS config | tailwind.config.js | - ---- - -**Congratulations!** You're now ready to contribute to SureForms. Happy coding! 🚀 - -**Questions?** Check [CLAUDE.md](../../CLAUDE.md) or create an issue on GitHub. diff --git a/docs/04-backend/DATABASE-SCHEMA.md b/docs/04-backend/DATABASE-SCHEMA.md deleted file mode 100644 index 68f5de98f..000000000 --- a/docs/04-backend/DATABASE-SCHEMA.md +++ /dev/null @@ -1,962 +0,0 @@ -# SureForms Database Schema - -> **Purpose:** Document custom database tables, WordPress post types, and data migration strategies -> **Audience:** Backend developers, database administrators -> **Last Updated:** 2026-02-06 -> **Plugin Version:** 2.5.0 - -## Overview - -SureForms uses a **hybrid data storage approach** combining WordPress native tables with custom optimized tables: - -1. **WordPress Custom Post Type (`sureforms_form`)** - Form configurations and structure -2. **Custom Table (`wp_srfm_entries`)** - Form submission data (performance-optimized) -3. **Custom Table (`wp_srfm_payments`)** - Payment transaction tracking - -**Why Custom Tables?** -- **Performance:** Separate from WordPress posts/postmeta tables (millions of rows) -- **Query Optimization:** Custom indexes for form-specific queries -- **Data Integrity:** Structured schema with foreign keys -- **Scalability:** Handle large volumes of form submissions efficiently - ---- - -## Custom Tables - -### Table: `wp_srfm_entries` - -**Purpose:** Store form submission data separate from WordPress posts for optimized query performance. - -**Schema Definition:** [inc/database/tables/entries.php](../../inc/database/tables/entries.php) -**Table Version:** 1 -**Character Set:** utf8mb4_unicode_ci - -#### SQL Schema - -```sql -CREATE TABLE wp_srfm_entries ( - ID BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY, - form_id BIGINT(20) UNSIGNED, - user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, - form_data LONGTEXT, - logs LONGTEXT, - notes LONGTEXT, - submission_info LONGTEXT, - status VARCHAR(10), - type VARCHAR(20), - extras LONGTEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - INDEX idx_form_id (form_id), - INDEX idx_user_id (user_id), - INDEX idx_form_id_created_at_status (form_id, created_at, status) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -#### Column Descriptions - -| Column | Type | Nullable | Default | Description | -|--------|------|----------|---------|-------------| -| `ID` | BIGINT(20) UNSIGNED | No | AUTO_INCREMENT | Primary key, unique entry identifier | -| `form_id` | BIGINT(20) UNSIGNED | No | - | Foreign key to wp_posts.ID (sureforms_form post type) | -| `user_id` | BIGINT(20) UNSIGNED | No | 0 | Foreign key to wp_users.ID (0 for guest submissions) | -| `form_data` | LONGTEXT | Yes | NULL | JSON-encoded submission data (field values) | -| `logs` | LONGTEXT | Yes | NULL | JSON-encoded activity logs array | -| `notes` | LONGTEXT | Yes | NULL | JSON-encoded admin notes array | -| `submission_info` | LONGTEXT | Yes | NULL | JSON-encoded metadata (IP, browser, device, referrer) | -| `status` | VARCHAR(10) | Yes | 'unread' | Entry status: 'unread', 'read', 'trash' | -| `type` | VARCHAR(20) | Yes | NULL | Form type: 'quiz', 'standard', NULL (standard default) | -| `extras` | LONGTEXT | Yes | NULL | JSON-encoded additional data | -| `created_at` | TIMESTAMP | No | CURRENT_TIMESTAMP | Entry creation timestamp | -| `updated_at` | TIMESTAMP | No | CURRENT_TIMESTAMP ON UPDATE | Last modification timestamp | - -#### Indexes - -**Purpose:** Optimize query performance for common access patterns - -| Index Name | Columns | Type | Purpose | -|------------|---------|------|---------| -| PRIMARY | ID | PRIMARY KEY | Unique entry lookup | -| idx_form_id | form_id | INDEX | Filter entries by form | -| idx_user_id | user_id | INDEX | Filter entries by user | -| idx_form_id_created_at_status | form_id, created_at, status | COMPOSITE INDEX | Optimize form dashboard queries with date/status filters | - -**Index Usage Examples:** -```sql --- Uses idx_form_id index -SELECT * FROM wp_srfm_entries WHERE form_id = 123; - --- Uses idx_form_id_created_at_status composite index -SELECT * FROM wp_srfm_entries -WHERE form_id = 123 - AND status = 'unread' - AND created_at > '2026-01-01' -ORDER BY created_at DESC; -``` - -#### JSON Column Structures - -**form_data** (LONGTEXT, JSON): -```json -{ - "field_slug": { - "value": "user input", - "label": "Field Label", - "type": "text" - }, - "email": { - "value": "user@example.com", - "label": "Email Address", - "type": "email" - }, - "phone": { - "value": "+1234567890", - "label": "Phone Number", - "type": "phone" - } -} -``` - -**submission_info** (LONGTEXT, JSON): -```json -{ - "ip_address": "192.168.1.1", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...", - "browser": "Chrome", - "browser_version": "120.0.0.0", - "device": "Desktop", - "os": "Mac OS X", - "referrer": "https://example.com/landing-page", - "timestamp": 1706800000 -} -``` - -**logs** (LONGTEXT, JSON): -```json -[ - { - "title": "Entry Created", - "messages": ["Entry submitted successfully"], - "timestamp": "2026-02-06 10:30:00" - }, - { - "title": "Email Sent", - "messages": [ - "Notification sent to admin@example.com", - "User confirmation sent to user@example.com" - ], - "timestamp": "2026-02-06 10:30:05" - }, - { - "title": "Integration Triggered", - "messages": ["Data sent to MailChimp", "Subscriber added successfully"], - "timestamp": "2026-02-06 10:30:10" - } -] -``` - -**notes** (LONGTEXT, JSON): -```json -[ - { - "note": "Follow up required", - "author": "admin", - "timestamp": "2026-02-06 12:00:00" - } -] -``` - -**extras** (LONGTEXT, JSON): -```json -{ - "quiz_score": 85, - "quiz_result": "passed", - "conversational_form": true, - "utm_source": "google", - "utm_campaign": "summer_2026" -} -``` - -#### Status Values - -| Status | Description | -|--------|-------------| -| `unread` | New submission, not yet viewed by admin (default) | -| `read` | Admin has viewed the submission | -| `trash` | Soft-deleted, can be restored or permanently deleted | - -#### Type Values - -| Type | Description | -|------|-------------| -| NULL or `standard` | Regular form submission (default) | -| `quiz` | Quiz form submission with score/results | -| Other custom types | Extensible for future form types | - -#### Migration History - -**Version 1:** -- **Renamed Column:** `user_data` → `form_data` (v0.0.13) - - Reason: More descriptive name - - Migration: ALTER TABLE RENAME COLUMN - - Backward compatibility: Old queries updated - -- **Added Columns:** - - `type` VARCHAR(20) (v0.0.13) - - `extras` LONGTEXT (v0.0.13) - - `user_id` BIGINT(20) (v0.0.13) - ---- - -### Table: `wp_srfm_payments` - -**Purpose:** Store payment transaction records for Stripe integration and payment tracking. - -**Schema Definition:** [inc/database/tables/payments.php](../../inc/database/tables/payments.php) -**Table Version:** 1 -**Character Set:** utf8mb4_unicode_ci - -#### SQL Schema - -```sql -CREATE TABLE wp_srfm_payments ( - id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY, - form_id BIGINT(20) UNSIGNED, - block_id VARCHAR(255) NOT NULL, - status VARCHAR(50) NOT NULL, - total_amount DECIMAL(26,8) NOT NULL, - refunded_amount DECIMAL(26,8) NOT NULL, - currency VARCHAR(10) NOT NULL, - entry_id BIGINT(20) UNSIGNED NOT NULL, - gateway VARCHAR(20) NOT NULL, - type VARCHAR(30) NOT NULL, - mode VARCHAR(20) NOT NULL, - transaction_id VARCHAR(50) NOT NULL, - customer_id VARCHAR(50) NOT NULL, - subscription_id VARCHAR(50) NOT NULL, - subscription_status VARCHAR(20) NOT NULL, - parent_subscription_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, - payment_data LONGTEXT, - extra LONGTEXT, - log LONGTEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - srfm_txn_id VARCHAR(100) NOT NULL, - customer_email VARCHAR(255) NOT NULL, - customer_name VARCHAR(255) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -#### Column Descriptions - -| Column | Type | Nullable | Default | Description | -|--------|------|----------|---------|-------------| -| `id` | BIGINT(20) UNSIGNED | No | AUTO_INCREMENT | Primary key, unique payment identifier | -| `form_id` | BIGINT(20) UNSIGNED | No | - | Foreign key to wp_posts.ID (form) | -| `block_id` | VARCHAR(255) | No | '' | Block client ID (unique identifier for payment field) | -| `status` | VARCHAR(50) | No | 'pending' | Payment status (Stripe-specific) | -| `total_amount` | DECIMAL(26,8) | No | 0.00000000 | Total amount after discount (8 decimal precision) | -| `refunded_amount` | DECIMAL(26,8) | No | 0.00000000 | Total refunded amount | -| `currency` | VARCHAR(10) | No | '' | ISO 4217 currency code (USD, EUR, GBP, etc.) | -| `entry_id` | BIGINT(20) UNSIGNED | No | 0 | Foreign key to wp_srfm_entries.ID | -| `gateway` | VARCHAR(20) | No | '' | Payment gateway ('stripe') | -| `type` | VARCHAR(30) | No | '' | Payment type: 'payment', 'subscription', 'renewal' | -| `mode` | VARCHAR(20) | No | '' | Payment mode: 'test' or 'live' | -| `transaction_id` | VARCHAR(50) | No | '' | Gateway transaction ID (e.g., Stripe pi_xxx) | -| `customer_id` | VARCHAR(50) | No | '' | Gateway customer ID (e.g., Stripe cus_xxx) | -| `subscription_id` | VARCHAR(50) | No | '' | Gateway subscription ID (e.g., Stripe sub_xxx) | -| `subscription_status` | VARCHAR(20) | No | '' | Subscription status ('active', 'canceled', etc.) | -| `parent_subscription_id` | BIGINT(20) UNSIGNED | No | 0 | Parent subscription payment ID (for renewal payments) | -| `payment_data` | LONGTEXT | Yes | NULL | JSON-encoded gateway-specific data | -| `extra` | LONGTEXT | Yes | NULL | JSON-encoded additional metadata | -| `log` | LONGTEXT | Yes | NULL | JSON-encoded payment activity logs | -| `created_at` | TIMESTAMP | No | CURRENT_TIMESTAMP | Payment creation timestamp | -| `updated_at` | TIMESTAMP | No | CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| `srfm_txn_id` | VARCHAR(100) | No | '' | SureForms transaction ID (custom format) | -| `customer_email` | VARCHAR(255) | No | '' | Customer email address | -| `customer_name` | VARCHAR(255) | No | '' | Customer full name | - -#### Valid Enum Values - -**Payment Status** (Stripe-specific): -- `pending` - Payment initiated but not completed -- `succeeded` - Payment completed successfully -- `failed` - Payment failed -- `canceled` - Payment canceled by user or admin -- `requires_action` - Requires additional authentication (3D Secure) -- `requires_payment_method` - Payment method needs to be provided -- `processing` - Payment is being processed -- `refunded` - Full refund processed -- `partially_refunded` - Partial refund processed - -**Currency Codes** (ISO 4217): -``` -USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, SEK, NZD, -MXN, SGD, HKD, NOK, TRY, RUB, INR, BRL, ZAR, KRW -``` - -**Payment Gateway:** -- `stripe` (currently only Stripe supported) - -**Payment Mode:** -- `test` - Test/sandbox mode -- `live` - Production mode - -**Subscription Status** (Stripe-specific): -- `active` - Subscription is active -- `canceled` - Subscription canceled -- `past_due` - Payment failed, retrying -- `unpaid` - Payment failed, no retry -- `trialing` - In trial period -- `incomplete` - Initial payment not completed -- `incomplete_expired` - Initial payment window expired -- `paused` - Subscription paused - -#### JSON Column Structures - -**payment_data** (LONGTEXT, JSON): -```json -{ - "stripe_payment_intent": { - "id": "pi_1234567890", - "amount": 4999, - "currency": "usd", - "status": "succeeded", - "created": 1706800000 - }, - "refunds": { - "re_1234567890": { - "refund_id": "re_1234567890", - "amount": 1000, - "status": "succeeded", - "created": 1706900000, - "reason": "requested_by_customer" - } - } -} -``` - -**extra** (LONGTEXT, JSON): -```json -{ - "discount_code": "SUMMER2026", - "discount_amount": 10.00, - "original_amount": 59.99, - "tax_amount": 5.00, - "metadata": { - "utm_source": "google", - "utm_campaign": "summer_sale" - } -} -``` - -**log** (LONGTEXT, JSON): -```json -[ - { - "title": "Payment Initiated", - "message": "Stripe Payment Intent created", - "timestamp": "2026-02-06 10:30:00" - }, - { - "title": "Payment Succeeded", - "message": "Payment completed successfully", - "timestamp": "2026-02-06 10:30:15" - } -] -``` - ---- - -## WordPress Custom Post Type - -### Post Type: `sureforms_form` - -**Purpose:** Store form configurations and structure using WordPress's content management system. - -**Registration:** [inc/post-types.php](../../inc/post-types.php) -**Supports:** title, editor, custom-fields -**Public:** false (admin-only) -**Has Archive:** false -**Rewrite:** false - -#### Post Fields - -| Field | Type | Description | -|-------|------|-------------| -| `post_title` | VARCHAR(200) | Form name (user-editable) | -| `post_content` | LONGTEXT | Serialized Gutenberg blocks (form structure) | -| `post_status` | VARCHAR(20) | 'draft', 'publish', 'trash' | -| `post_type` | VARCHAR(20) | 'sureforms_form' (constant) | -| `post_author` | BIGINT(20) | WordPress user ID who created the form | -| `post_date` | DATETIME | Form creation date | -| `post_modified` | DATETIME | Last modification date | - -#### Post Meta Keys - -Form-specific settings stored in `wp_postmeta`: - -| Meta Key | Description | Data Type | Example | -|----------|-------------|-----------|---------| -| `_srfm_form_settings` | General form settings | Serialized array | [complex settings object] | -| `_srfm_email_notifications` | Email notification rules | Serialized array | Multiple notification configs | -| `_srfm_confirmation_settings` | Post-submission behavior | Serialized array | Message or redirect URL | -| `_srfm_spam_protection` | Anti-spam settings | Serialized array | reCAPTCHA, honeypot config | -| `_srfm_gdpr_settings` | GDPR compliance options | Serialized array | Consent, auto-delete rules | -| `_srfm_styling_settings` | Form visual customization | Serialized array | Colors, fonts, spacing | -| `_srfm_conditional_logic` | Conditional field display | Serialized array | Field visibility rules | -| `_srfm_integrations_webhooks` | Third-party integrations | Serialized array | MailChimp, webhooks | -| `_srfm_instant_form_settings` | Instant form configuration | Serialized array | AI-generated form data | -| `_srfm_conversational_form` | Conversational mode settings | Serialized array | Step-by-step form config | -| `_srfm_premium_common` | Pro features settings | Serialized array | Pro-only configurations | -| `_srfm_user_registration_settings` | User registration integration | Serialized array | Create user on submission | - -#### Block Structure (post_content) - -Forms stored as serialized Gutenberg blocks (HTML comments): - -```html - - - - - - - - - -``` - ---- - -## Database Schema Migrations - -### Version Control System - -**Implementation:** Each table class maintains a `$table_version` property. - -**Version Storage:** WordPress options table -``` -Option Name: srfm_entries_version -Option Value: 1 (integer) - -Option Name: srfm_payments_version -Option Value: 1 (integer) -``` - -### Migration Flow - -``` -1. Plugin loads → Database\Register::init() -2. For each table: - a. Get stored version from wp_options - b. Compare with code version ($table_version) - c. If mismatch: run upgrade() method - d. Execute schema changes - e. Update version in wp_options -``` - -### Migration Methods - -**Base Class Methods** ([inc/database/base.php](../../inc/database/base.php)): - -| Method | Purpose | -|--------|---------| -| `maybe_create_table()` | Create table if not exists | -| `maybe_add_new_columns()` | Add new columns to existing table | -| `maybe_rename_columns()` | Rename columns (e.g., user_data → form_data) | -| `upgrade($old_version)` | Custom migration logic per version | - -### Adding a New Column (Example) - -**Step 1:** Increment table version in table class -```php -// In inc/database/tables/entries.php -protected $table_version = 2; // Increment from 1 to 2 -``` - -**Step 2:** Add column to schema -```php -public function get_schema() { - return [ - // ... existing columns - 'new_column' => [ - 'type' => 'string', - 'default' => '', - ], - ]; -} -``` - -**Step 3:** Add column definition -```php -public function get_new_columns_definition() { - return [ - 'new_column VARCHAR(255) DEFAULT "" AFTER status', - ]; -} -``` - -**Step 4:** Implement upgrade logic (if needed) -```php -public function upgrade($old_version) { - global $wpdb; - - if ($old_version < 2) { - // Additional migration logic beyond column addition - $wpdb->query("UPDATE {$this->table_name} SET new_column = 'default_value'"); - } -} -``` - -### Migration Best Practices - -1. **Never Delete Columns** - Mark as deprecated instead - ```php - // DON'T: ALTER TABLE DROP COLUMN old_column - // DO: Leave column, document as deprecated - ``` - -2. **Always Provide Defaults** - Avoid NULL for new columns - ```php - 'new_column VARCHAR(255) DEFAULT ""' // Good - 'new_column VARCHAR(255)' // Bad (no default) - ``` - -3. **Test with Production Data** - Use staging environment first -4. **Log All Migrations** - Use error_log() for debugging -5. **Backwards Compatible** - Old code must work during migration -6. **Atomic Operations** - Use transactions where possible - ---- - -## Common Query Patterns - -### Get All Entries for a Form - -```php -use SRFM\Inc\Database\Tables\Entries; - -$entries = Entries::get_all([ - 'where' => [ - [ - ['key' => 'form_id', 'compare' => '=', 'value' => 123], - ['key' => 'status', 'compare' => '!=', 'value' => 'trash'], - ] - ], - 'orderby' => 'created_at', - 'order' => 'DESC', - 'limit' => 20, - 'offset' => 0 -]); -``` - -**Generated SQL:** -```sql -SELECT * FROM wp_srfm_entries -WHERE form_id = 123 - AND status != 'trash' -ORDER BY created_at DESC -LIMIT 0, 20 -``` - -### Get Entry Count by Status - -```php -$unread_count = Entries::get_total_entries_by_status('unread', $form_id); -$all_count = Entries::get_total_entries_by_status('all', $form_id); -``` - -**Generated SQL:** -```sql --- Unread count -SELECT COUNT(*) FROM wp_srfm_entries -WHERE status = 'unread' AND form_id = 123 - --- All count (excluding trash) -SELECT COUNT(*) FROM wp_srfm_entries -WHERE status != 'trash' AND form_id = 123 -``` - -### Get Entries with Payments - -```php -global $wpdb; - -$entries_with_payments = $wpdb->get_results($wpdb->prepare( - "SELECT e.*, p.total_amount, p.status as payment_status, p.currency - FROM {$wpdb->prefix}srfm_entries e - INNER JOIN {$wpdb->prefix}srfm_payments p ON e.ID = p.entry_id - WHERE e.form_id = %d - AND p.status = 'succeeded' - ORDER BY e.created_at DESC", - $form_id -)); -``` - -### Get Payments by Status - -```php -use SRFM\Inc\Database\Tables\Payments; - -$succeeded_payments = Payments::get_all([ - 'where' => [ - [ - ['key' => 'form_id', 'compare' => '=', 'value' => 123], - ['key' => 'status', 'compare' => '=', 'value' => 'succeeded'], - ] - ], - 'orderby' => 'created_at', - 'order' => 'DESC' -]); -``` - ---- - -## Performance Optimization - -### Indexing Strategy - -**Why Indexes Matter:** -- 10,000+ entries: ~50ms query time with indexes vs ~500ms without -- Composite indexes optimize multi-column WHERE clauses -- Index order matters: (form_id, created_at, status) ≠ (status, form_id, created_at) - -**Index Usage Guidelines:** - -1. **Use Indexed Columns in WHERE Clauses** -```php -// GOOD - Uses idx_form_id index -$entries = Entries::get_all(['where' => [[['key' => 'form_id', 'compare' => '=', 'value' => 123]]]]); - -// BAD - No index on 'type' column (full table scan) -$entries = Entries::get_all(['where' => [[['key' => 'type', 'compare' => '=', 'value' => 'quiz']]]]); -``` - -2. **Leverage Composite Indexes** -```php -// GOOD - Uses idx_form_id_created_at_status composite index -$entries = Entries::get_all([ - 'where' => [ - [ - ['key' => 'form_id', 'compare' => '=', 'value' => 123], - ['key' => 'created_at', 'compare' => '>', 'value' => '2026-01-01'], - ['key' => 'status', 'compare' => '=', 'value' => 'unread'], - ] - ] -]); -``` - -3. **Avoid SELECT *** -```php -// GOOD - Specify columns -$entries = Entries::get_instance()->get_results( - ['form_id' => 123], - 'ID, form_data, created_at' -); - -// BAD - Selects all columns including large LONGTEXT -$entries = Entries::get_all(['where' => [[['key' => 'form_id', 'compare' => '=', 'value' => 123]]]]); -``` - -### Pagination Best Practices - -```php -// Paginate large result sets -$per_page = 20; -$page = 2; -$offset = ($page - 1) * $per_page; - -$entries = Entries::get_all([ - 'where' => [[['key' => 'form_id', 'compare' => '=', 'value' => 123]]], - 'limit' => $per_page, - 'offset' => $offset, - 'orderby' => 'created_at', - 'order' => 'DESC' -]); - -// Cache total count separately -$total = Entries::get_total_entries_by_status('all', 123); -$total_pages = ceil($total / $per_page); -``` - -### JSON Column Optimization - -**MySQL 5.7+ JSON Functions:** -```sql --- Extract specific field from form_data JSON -SELECT - ID, - JSON_EXTRACT(form_data, '$.email.value') as email -FROM wp_srfm_entries -WHERE form_id = 123; - --- Search within JSON -SELECT * FROM wp_srfm_entries -WHERE JSON_EXTRACT(form_data, '$.email.value') = 'user@example.com'; -``` - -**Performance Warning:** -- JSON queries are slower than indexed columns -- Avoid wildcard searches in JSON (`LIKE '%value%'`) -- Consider extracting frequently-queried JSON fields to dedicated columns - ---- - -## Data Retention & GDPR - -### Auto-Delete Configuration - -**Setting Location:** [inc/single-form-settings/compliance-settings.php](../../inc/single-form-settings/compliance-settings.php) - -**Per-Form Settings:** -```php -$retention_days = get_post_meta($form_id, '_srfm_entry_retention_days', true); -// Values: 0 (never delete), 7, 30, 60, 90, 365 (days) -``` - -**Implementation:** WordPress Cron (Action Scheduler) - -```php -// Scheduled task runs daily at 2 AM -add_action('srfm_daily_scheduled_action', 'srfm_auto_delete_entries'); - -function srfm_auto_delete_entries() { - global $wpdb; - - // Get forms with auto-delete enabled - $forms = $wpdb->get_results(" - SELECT post_id, meta_value as retention_days - FROM {$wpdb->postmeta} - WHERE meta_key = '_srfm_entry_retention_days' - AND meta_value > 0 - "); - - foreach ($forms as $form) { - $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$form->retention_days} days")); - - // Delete entries older than cutoff - $wpdb->delete( - $wpdb->prefix . 'srfm_entries', - [ - 'form_id' => $form->post_id, - 'created_at < ' => $cutoff_date - ] - ); - } -} -``` - -### Manual Entry Management - -**Soft Delete (Trash):** -```php -Entries::update($entry_id, ['status' => 'trash']); -``` - -**Permanent Delete:** -```php -// Fires srfm_before_delete_entry hook -Entries::delete($entry_id); -``` - -**Restore from Trash:** -```php -Entries::update($entry_id, ['status' => 'unread']); -``` - ---- - -## Backup & Recovery - -### Backup Recommendations - -**1. Full Database Backup** (before plugin updates) -```bash -mysqldump -u username -p database_name > sureforms_backup.sql -``` - -**2. Table-Specific Dumps** -```bash -# Entries table -mysqldump -u username -p database_name wp_srfm_entries > entries_backup.sql - -# Payments table -mysqldump -u username -p database_name wp_srfm_payments > payments_backup.sql - -# Form configurations (WordPress tables) -mysqldump -u username -p database_name wp_posts wp_postmeta \ - --where="post_type='sureforms_form'" > forms_backup.sql -``` - -**3. Export Entries via Admin UI** -- Navigate to SureForms → Entries -- Select entries → Export CSV -- Includes all form data and metadata - -### Restore Process - -```bash -# Restore from backup -mysql -u username -p database_name < entries_backup.sql - -# Restore specific table -mysql -u username -p database_name < wp_srfm_entries.sql -``` - ---- - -## Troubleshooting - -### Issue: Missing Tables - -**Symptom:** "Table doesn't exist" errors in PHP logs -**Cause:** Tables not created during activation -**Fix:** -1. Deactivate plugin -2. Delete option: `srfm_entries_version`, `srfm_payments_version` -3. Reactivate plugin (triggers table creation) - -### Issue: Charset/Collation Errors - -**Symptom:** Garbled text in submissions (emoji, special characters) -**Cause:** Incorrect charset (not utf8mb4) -**Fix:** -```sql -ALTER TABLE wp_srfm_entries -CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE wp_srfm_payments -CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -``` - -### Issue: JSON Parsing Errors - -**Symptom:** "Invalid JSON" errors when retrieving form_data -**Cause:** Unescaped quotes in field values -**Fix:** -```php -// Always use wp_json_encode() for JSON storage -$form_data_json = wp_json_encode($form_data); - -// Always use json_decode() with error handling -$form_data = json_decode($form_data_json, true); -if (json_last_error() !== JSON_ERROR_NONE) { - error_log('JSON decode error: ' . json_last_error_msg()); -} -``` - -### Issue: Slow Queries - -**Symptom:** Dashboard loads slowly with many entries -**Cause:** Missing indexes or inefficient queries -**Debug:** -```sql --- Check query execution plan -EXPLAIN SELECT * FROM wp_srfm_entries -WHERE form_id = 123 - AND status = 'unread' -ORDER BY created_at DESC -LIMIT 20; - --- Should show "Using index" in Extra column -``` - -**Fix:** -1. Ensure indexes exist (check `SHOW INDEX FROM wp_srfm_entries`) -2. Use indexed columns in WHERE clauses -3. Add pagination (LIMIT/OFFSET) - ---- - -## Entity Relationship Diagram - -```mermaid -erDiagram - wp_posts ||--o{ wp_srfm_entries : "has many" - wp_posts ||--o{ wp_srfm_payments : "has many" - wp_srfm_entries ||--o| wp_srfm_payments : "may have" - wp_users ||--o{ wp_srfm_entries : "submits" - - wp_posts { - bigint ID PK - varchar post_type "sureforms_form" - longtext post_content "Gutenberg blocks" - varchar post_status "draft|publish|trash" - } - - wp_srfm_entries { - bigint ID PK - bigint form_id FK - bigint user_id FK - longtext form_data "JSON" - varchar status "unread|read|trash" - timestamp created_at - } - - wp_srfm_payments { - bigint id PK - bigint form_id FK - bigint entry_id FK - varchar status "pending|succeeded|failed" - decimal total_amount - varchar currency - varchar transaction_id - timestamp created_at - } - - wp_users { - bigint ID PK - varchar user_email - } -``` - ---- - -## Summary - -**Storage Strategy:** -- **Forms:** WordPress custom post type (native CMS benefits) -- **Entries:** Custom table (performance optimization) -- **Payments:** Custom table (financial data tracking) - -**Key Design Decisions:** -- Custom tables for scalability (handle millions of entries) -- JSON columns for flexible schema (form fields vary per form) -- Composite indexes for query optimization -- Version-controlled migrations for safe schema updates - -**Maintenance:** -- Regular backups before plugin updates -- Monitor table sizes (OPTIMIZE TABLE if needed) -- Review slow query logs -- Test migrations on staging first - ---- - -**Related Documentation:** -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide -- [ARCHITECTURE.md](../02-architecture/ARCHITECTURE.md) - System design -- [CODING-STANDARDS.md](../03-development/CODING-STANDARDS.md) - Code style -- [SECURITY.md](../07-security/SECURITY.md) - Security practices diff --git a/docs/04-backend/HOOKS-REFERENCE.md b/docs/04-backend/HOOKS-REFERENCE.md deleted file mode 100644 index 35b5c4c12..000000000 --- a/docs/04-backend/HOOKS-REFERENCE.md +++ /dev/null @@ -1,1055 +0,0 @@ -# SureForms Hooks Reference - -> **Purpose:** Complete reference of all WordPress actions and filters for extending SureForms -> **Audience:** Plugin developers, theme developers, integrators -> **Last Updated:** 2026-02-06 -> **Plugin Version:** 2.5.0 - -## Overview - -SureForms provides **450+ hooks** (actions and filters) for extending functionality without modifying core files. This document catalogs the most commonly used hooks with usage examples. - -**Hook Naming Convention:** -- **Prefix:** All hooks start with `srfm_` -- **Format:** `srfm_{context}_{action}` or `srfm_{data_type}_{modification}` -- **Examples:** `srfm_before_submission`, `srfm_field_validation`, `srfm_email_content` - ---- - -## Table of Contents - -- [Plugin Lifecycle](#plugin-lifecycle) -- [Form Submission](#form-submission) -- [Field Validation](#field-validation) -- [Email Notifications](#email-notifications) -- [Form Rendering](#form-rendering) -- [Data Modification](#data-modification) -- [Database Operations](#database-operations) -- [Payment Processing](#payment-processing) -- [Block Registration](#block-registration) -- [Admin Operations](#admin-operations) -- [Integration Points](#integration-points) - ---- - -## Plugin Lifecycle - -### `srfm_core_loaded` - -Fires after the plugin core is fully loaded and initialized. - -**Location:** [plugin-loader.php](../../plugin-loader.php) -**Since:** 0.0.1 -**Parameters:** None - -**Usage:** -```php -add_action('srfm_core_loaded', function() { - // Plugin is ready, initialize custom extensions - error_log('SureForms plugin loaded'); - - // Register custom integrations - my_custom_sureforms_integration(); -}); -``` - -### `srfm_init` - -Fires during WordPress `init` hook after SureForms initialization. - -**Location:** [plugin-loader.php](../../plugin-loader.php) -**Since:** 0.0.1 -**Parameters:** None - -**Usage:** -```php -add_action('srfm_init', function() { - // Register custom post types, taxonomies, or other WordPress entities - register_post_type('custom_form_data', [ - 'public' => false, - 'supports' => ['title'] - ]); -}); -``` - ---- - -## Form Submission - -### `srfm_before_submission` - -Fires immediately before form data is processed and saved to database. Use to validate or modify data before processing. - -**Location:** [inc/form-submit.php:525](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $form_data` - Submitted form data including form_id, fields, and metadata - -**Usage:** -```php -add_action('srfm_before_submission', function($form_data) { - $form_id = $form_data['form_id'] ?? 0; - - // Log submission attempt - error_log(sprintf('Form %d submission started', $form_id)); - - // Perform custom validation - if ($form_id === 123) { - $email = $form_data['email']['value'] ?? ''; - if (strpos($email, '@company.com') === false) { - // Throw exception to halt submission - throw new Exception('Only company emails allowed'); - } - } -}, 10, 1); -``` - -### `srfm_after_submission_process` - -Fires after form entry is saved to database but before email notifications are sent. - -**Location:** [inc/form-submit.php:556](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $form_data` - Complete form data including entry_id - -**Usage:** -```php -add_action('srfm_after_submission_process', function($form_data) { - $entry_id = $form_data['entry_id'] ?? 0; - $form_id = $form_data['form_id'] ?? 0; - - // Trigger third-party integration - if ($form_id === 123) { - $email = $form_data['email']['value'] ?? ''; - $name = $form_data['name']['value'] ?? ''; - - // Add to MailChimp - mailchimp_add_subscriber($email, $name); - - // Send to CRM - crm_create_lead([ - 'email' => $email, - 'name' => $name, - 'source' => 'sureforms' - ]); - } -}, 10, 1); -``` - -### `srfm_form_submit` - -Fires after complete form submission process (entry saved, emails sent). - -**Location:** [inc/form-submit.php:548,644](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $response` - Submission response with entry_id, success status, message - -**Usage:** -```php -add_action('srfm_form_submit', function($response) { - if ($response['success']) { - $entry_id = $response['entry_id']; - - // Custom logging - error_log(sprintf('Form submission %d completed', $entry_id)); - - // Update custom database - global $wpdb; - $wpdb->insert('wp_custom_leads', [ - 'sureforms_entry_id' => $entry_id, - 'created_at' => current_time('mysql') - ]); - } -}, 10, 1); -``` - ---- - -## Field Validation - -### `srfm_field_validation` - -Filters field validation result. Return `WP_Error` to fail validation. - -**Location:** [inc/field-validation.php](../../inc/field-validation.php) -**Since:** 0.0.1 -**Parameters:** -- `bool|WP_Error $is_valid` - Current validation status -- `mixed $field_value` - Field value to validate -- `array $field_data` - Field metadata (type, label, required, etc.) -- `int $form_id` - Form post ID - -**Return:** `bool|WP_Error` - True if valid, WP_Error if invalid - -**Usage:** -```php -add_filter('srfm_field_validation', function($is_valid, $field_value, $field_data, $form_id) { - // Custom validation for specific field type - if ($field_data['type'] === 'text' && $field_data['slug'] === 'phone') { - // Validate phone format - if (!preg_match('/^\+?[1-9]\d{1,14}$/', $field_value)) { - return new WP_Error( - 'invalid_phone', - __('Please enter a valid phone number', 'your-plugin') - ); - } - } - - // Custom validation for specific form - if ($form_id === 123 && $field_data['slug'] === 'age') { - if (intval($field_value) < 18) { - return new WP_Error( - 'age_restriction', - __('You must be 18 or older', 'your-plugin') - ); - } - } - - return $is_valid; -}, 10, 4); -``` - -### `srfm_validate_{field_type}` - -Filters validation for specific field type. - -**Location:** [inc/field-validation.php](../../inc/field-validation.php) -**Since:** Various -**Available Types:** `email`, `phone`, `url`, `number`, `text`, etc. - -**Usage:** -```php -// Custom email validation -add_filter('srfm_validate_email', function($is_valid, $value, $field_data) { - // Block disposable email domains - $disposable_domains = ['tempmail.com', 'throwaway.email']; - $domain = substr(strrchr($value, '@'), 1); - - if (in_array($domain, $disposable_domains)) { - return new WP_Error( - 'disposable_email', - __('Disposable email addresses are not allowed', 'your-plugin') - ); - } - - return $is_valid; -}, 10, 3); -``` - ---- - -## Email Notifications - -### `srfm_email_notification_should_send` - -Filters whether email notification should be sent. - -**Location:** [inc/form-submit.php:796](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `array $email_notification` - Email configuration -- `array $submission_data` - Submitted form data -- `array $form_data` - Complete form metadata - -**Return:** `array|false` - Email config to send, false to skip - -**Usage:** -```php -add_filter('srfm_email_notification_should_send', function($email_notification, $submission_data, $form_data) { - $form_id = $form_data['form_id'] ?? 0; - - // Only send email for specific form if user opted in - if ($form_id === 123) { - $opted_in = $submission_data['email_optin']['value'] ?? false; - if (!$opted_in) { - return false; // Skip email - } - } - - return $email_notification; -}, 10, 3); -``` - -### `srfm_email_notification` - -Filters parsed email data before sending. - -**Location:** [inc/form-submit.php:809](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `array $parsed` - Parsed email data (to, subject, message, headers) -- `array $submission_data` - Submitted form data -- `array $item` - Email notification configuration -- `array $form_data` - Form metadata - -**Return:** `array` - Modified email data - -**Usage:** -```php -add_filter('srfm_email_notification', function($parsed, $submission_data, $item, $form_data) { - // Add custom header - $parsed['headers'][] = 'X-Form-ID: ' . ($form_data['form_id'] ?? 0); - - // Modify subject based on submission data - if (isset($submission_data['priority']['value']) && $submission_data['priority']['value'] === 'high') { - $parsed['subject'] = '[URGENT] ' . $parsed['subject']; - } - - // Add custom footer to message - $parsed['message'] .= "\n\n---\nSubmitted via SureForms"; - - return $parsed; -}, 10, 4); -``` - -### `srfm_before_email_send` - -Action fired immediately before email is sent via `wp_mail()`. - -**Location:** [inc/form-submit.php:812](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `array $parsed` - Email data (to, subject, message, headers) -- `array $submission_data` - Submitted form data -- `array $item` - Email notification configuration -- `array $form_data` - Form metadata - -**Usage:** -```php -add_action('srfm_before_email_send', function($parsed, $submission_data, $item, $form_data) { - // Log email send attempt - error_log(sprintf( - 'Sending email to %s for form %d', - $parsed['to'], - $form_data['form_id'] ?? 0 - )); - - // Save email to custom table for tracking - global $wpdb; - $wpdb->insert('wp_email_log', [ - 'recipient' => $parsed['to'], - 'subject' => $parsed['subject'], - 'sent_at' => current_time('mysql') - ]); -}, 10, 4); -``` - -### `srfm_after_email_send` - -Action fired after email is sent. - -**Location:** [inc/form-submit.php:904-905](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `array $parsed` - Email data -- `array $submission_data` - Form data -- `array $item` - Email config -- `array $form_data` - Form metadata -- `bool $sent` - Whether email was sent successfully - -**Usage:** -```php -add_action('srfm_after_email_send', function($parsed, $submission_data, $item, $form_data, $sent) { - if (!$sent) { - // Email failed to send - error_log(sprintf( - 'Email failed for form %d to %s', - $form_data['form_id'] ?? 0, - $parsed['to'] - )); - - // Send alert to admin - wp_mail( - get_option('admin_email'), - 'Form email notification failed', - sprintf('Failed to send notification for form %d', $form_data['form_id']) - ); - } -}, 10, 5); -``` - ---- - -## Data Modification - -### `srfm_form_submit_data` - -Filters complete form submission data before processing. - -**Location:** [inc/form-submit.php:474](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $form_data` - Complete form data - -**Return:** `array` - Modified form data - -**Usage:** -```php -add_filter('srfm_form_submit_data', function($form_data) { - // Add custom calculated field - if (isset($form_data['price'], $form_data['quantity'])) { - $form_data['total'] = [ - 'value' => $form_data['price']['value'] * $form_data['quantity']['value'], - 'label' => 'Total Amount', - 'type' => 'calculated' - ]; - } - - // Add timestamp - $form_data['submitted_timestamp'] = [ - 'value' => current_time('mysql'), - 'label' => 'Submission Time', - 'type' => 'hidden' - ]; - - return $form_data; -}, 10, 1); -``` - -### `srfm_before_entry_data` - -Filters entry data before saving to database. - -**Location:** [inc/form-submit.php:605-606](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $entries_data` - Entry data to be saved -- `int $form_id` - Form post ID - -**Return:** `array` - Modified entry data - -**Usage:** -```php -add_filter('srfm_before_entry_data', function($entries_data, $form_id) { - // Add custom extras data - $entries_data['extras'] = [ - 'ip_country' => get_user_ip_country(), - 'utm_source' => $_GET['utm_source'] ?? '', - 'utm_campaign' => $_GET['utm_campaign'] ?? '', - 'referrer' => wp_get_referer() - ]; - - // Modify status based on conditions - if ($form_id === 123) { - $entries_data['status'] = 'pending_review'; // Custom status - } - - return $entries_data; -}, 10, 2); -``` - -### `srfm_form_submission_response` - -Filters final submission response sent to frontend. - -**Location:** [inc/form-submit.php:660](../../inc/form-submit.php) -**Since:** 2.4.0 -**Parameters:** -- `array $response` - Response data (success, message, entry_id) -- `array $form_data` - Form data -- `array $submission_data` - Submission data - -**Return:** `array` - Modified response - -**Usage:** -```php -add_filter('srfm_form_submission_response', function($response, $form_data, $submission_data) { - // Add custom data to response - if ($response['success']) { - $response['custom_redirect_url'] = 'https://example.com/thank-you'; - $response['tracking_code'] = 'FB-' . $response['entry_id']; - - // Add custom message - $response['custom_message'] = sprintf( - 'Thank you! Your reference number is %d', - $response['entry_id'] - ); - } - - return $response; -}, 10, 3); -``` - -### `srfm_process_field_value` - -Filters individual field value during processing. - -**Location:** [inc/form-submit.php:1211-1212](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `mixed $value` - Field value -- `array $field` - Field data -- `array $form_data` - Complete form data - -**Return:** `mixed` - Modified field value - -**Usage:** -```php -add_filter('srfm_process_field_value', function($value, $field, $form_data) { - // Normalize phone numbers - if ($field['type'] === 'phone') { - $value = preg_replace('/[^0-9+]/', '', $value); - } - - // Convert text to uppercase for specific field - if ($field['slug'] === 'reference_code') { - $value = strtoupper($value); - } - - // Encrypt sensitive data - if ($field['slug'] === 'ssn') { - $value = encrypt_data($value); - } - - return $value; -}, 10, 3); -``` - ---- - -## Form Rendering - -### `srfm_form_markup` - -Filters complete form HTML before rendering. - -**Location:** [inc/generate-form-markup.php](../../inc/generate-form-markup.php) -**Since:** 0.0.1 -**Parameters:** -- `string $markup` - Generated HTML -- `int $form_id` - Form post ID -- `array $blocks` - Gutenberg blocks array - -**Return:** `string` - Modified HTML - -**Usage:** -```php -add_filter('srfm_form_markup', function($markup, $form_id, $blocks) { - // Add custom wrapper - $markup = '
' . $markup . '
'; - - // Add custom analytics tracking - $markup .= sprintf( - '', - $form_id - ); - - return $markup; -}, 10, 3); -``` - -### `srfm_field_markup` - -Filters individual field HTML. - -**Location:** [inc/fields/base.php](../../inc/fields/base.php) -**Since:** 0.0.1 -**Parameters:** -- `string $markup` - Field HTML -- `array $attributes` - Block attributes -- `string $field_type` - Field type (input, email, etc.) - -**Return:** `string` - Modified HTML - -**Usage:** -```php -add_filter('srfm_field_markup', function($markup, $attributes, $field_type) { - // Add custom class to all email fields - if ($field_type === 'email') { - $markup = str_replace( - 'class="srfm-field', - 'class="srfm-field custom-email-field', - $markup - ); - } - - // Add tooltips - if (!empty($attributes['help_text'])) { - $markup .= sprintf( - '%s', - esc_html($attributes['help_text']) - ); - } - - return $markup; -}, 10, 3); -``` - -### `srfm_form_class` - -Filters CSS classes applied to form element. - -**Location:** [inc/generate-form-markup.php](../../inc/generate-form-markup.php) -**Since:** 0.0.1 -**Parameters:** -- `array $classes` - CSS class array -- `int $form_id` - Form post ID - -**Return:** `array` - Modified classes - -**Usage:** -```php -add_filter('srfm_form_class', function($classes, $form_id) { - // Add custom class - $classes[] = 'custom-form'; - - // Add class based on form ID - $classes[] = 'form-' . $form_id; - - // Add class based on form type - $form_type = get_post_meta($form_id, '_srfm_form_type', true); - if ($form_type === 'contact') { - $classes[] = 'contact-form'; - } - - return $classes; -}, 10, 2); -``` - ---- - -## Database Operations - -### `srfm_before_delete_entry` - -Action fired before entry is permanently deleted from database. - -**Location:** [inc/database/tables/entries.php:297](../../inc/database/tables/entries.php) -**Since:** 0.0.13 -**Parameters:** -- `int $entry_id` - Entry ID being deleted - -**Usage:** -```php -add_action('srfm_before_delete_entry', function($entry_id) { - // Log deletion - error_log(sprintf('Deleting entry %d', $entry_id)); - - // Clean up related data - global $wpdb; - $wpdb->delete('wp_custom_entry_meta', ['entry_id' => $entry_id]); - - // Archive entry before deletion - $entry = SRFM\Inc\Database\Tables\Entries::get($entry_id); - if ($entry) { - $wpdb->insert('wp_deleted_entries_archive', [ - 'entry_id' => $entry_id, - 'form_data' => wp_json_encode($entry), - 'deleted_at' => current_time('mysql') - ]); - } -}, 10, 1); -``` - ---- - -## Payment Processing - -### `srfm_before_payment_process` - -Action fired before payment processing starts. - -**Location:** [inc/payments/front-end.php](../../inc/payments/front-end.php) -**Since:** 2.0.0 -**Parameters:** -- `array $payment_data` - Payment data -- `int $form_id` - Form post ID - -**Usage:** -```php -add_action('srfm_before_payment_process', function($payment_data, $form_id) { - // Log payment attempt - error_log(sprintf( - 'Processing payment of %s %s for form %d', - $payment_data['amount'], - $payment_data['currency'], - $form_id - )); - - // Verify inventory before charging - if ($form_id === 123) { - $product_id = $payment_data['product_id'] ?? 0; - if (!check_inventory($product_id)) { - throw new Exception('Product out of stock'); - } - } -}, 10, 2); -``` - -### `srfm_after_payment_success` - -Action fired after successful payment. - -**Location:** [inc/payments/front-end.php](../../inc/payments/front-end.php) -**Since:** 2.0.0 -**Parameters:** -- `int $payment_id` - Payment record ID -- `array $payment_data` - Payment data -- `int $entry_id` - Entry ID - -**Usage:** -```php -add_action('srfm_after_payment_success', function($payment_id, $payment_data, $entry_id) { - // Send receipt - $customer_email = $payment_data['customer_email']; - wp_mail( - $customer_email, - 'Payment Receipt', - sprintf('Payment of %s received', $payment_data['total_amount']) - ); - - // Update inventory - update_inventory($payment_data['product_id'], -1); - - // Trigger fulfillment - trigger_order_fulfillment($payment_id); -}, 10, 3); -``` - ---- - -## Block Registration - -### `srfm_block_registration_args` - -Filters block registration arguments. - -**Location:** [inc/blocks/register.php](../../inc/blocks/register.php) -**Since:** 0.0.1 -**Parameters:** -- `array $args` - Block registration args -- `string $block_name` - Block name (sureforms/input, etc.) - -**Return:** `array` - Modified args - -**Usage:** -```php -add_filter('srfm_block_registration_args', function($args, $block_name) { - // Modify render callback - if ($block_name === 'sureforms/input') { - $original_callback = $args['render_callback']; - $args['render_callback'] = function($attributes, $content) use ($original_callback) { - // Wrap output in custom div - $output = call_user_func($original_callback, $attributes, $content); - return '
' . $output . '
'; - }; - } - - return $args; -}, 10, 2); -``` - ---- - -## Admin Operations - -### `srfm_after_form_created` - -Action fired after new form is created. - -**Location:** [inc/create-new-form.php](../../inc/create-new-form.php) -**Since:** 0.0.1 -**Parameters:** -- `int $form_id` - Created form post ID - -**Usage:** -```php -add_action('srfm_after_form_created', function($form_id) { - // Set default form settings - update_post_meta($form_id, '_srfm_custom_setting', 'default_value'); - - // Log form creation - error_log(sprintf('New form created: %d', $form_id)); - - // Notify admin - wp_mail( - get_option('admin_email'), - 'New form created', - sprintf('Form ID %d was created', $form_id) - ); -}, 10, 1); -``` - ---- - -## Form Restrictions - -### `srfm_form_restriction_message` - -Filters restriction message shown to users. - -**Location:** [inc/form-submit.php:267](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `string $message` - Default restriction message -- `int $form_id` - Form post ID -- `array $form_restriction` - Restriction settings - -**Return:** `string` - Modified message - -**Usage:** -```php -add_filter('srfm_form_restriction_message', function($message, $form_id, $form_restriction) { - // Custom message based on restriction type - if ($form_restriction['type'] === 'schedule' && $form_restriction['state'] === 'not_started') { - $message = sprintf( - 'This form will open on %s', - date('F j, Y', strtotime($form_restriction['start_date'])) - ); - } - - return $message; -}, 10, 3); -``` - -### `srfm_additional_restriction_check` - -Filters whether form has additional restrictions beyond scheduling. - -**Location:** [inc/form-submit.php:276](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `bool $is_restricted` - Whether form is restricted -- `int $form_id` - Form post ID -- `array $form_data` - Form data - -**Return:** `bool` - True to restrict, false to allow - -**Usage:** -```php -add_filter('srfm_additional_restriction_check', function($is_restricted, $form_id, $form_data) { - // Restrict based on user role - if ($form_id === 123 && !current_user_can('subscriber')) { - return true; // Restrict - } - - // Restrict based on IP - $user_ip = $_SERVER['REMOTE_ADDR']; - if (is_ip_blocked($user_ip)) { - return true; - } - - // Restrict based on previous submissions - $user_email = $form_data['email']['value'] ?? ''; - if ($user_email && has_recent_submission($form_id, $user_email)) { - return true; // Already submitted recently - } - - return $is_restricted; -}, 10, 3); -``` - ---- - -## Integration Points - -### Third-Party Service Integration Example - -```php -/** - * Complete integration example: Send form data to CRM after submission - */ -class SureForms_CRM_Integration { - public function __construct() { - add_action('srfm_after_submission_process', [$this, 'send_to_crm'], 10, 1); - add_action('srfm_before_delete_entry', [$this, 'delete_from_crm'], 10, 1); - } - - public function send_to_crm($form_data) { - // Only for contact forms - if ($form_data['form_id'] !== 123) { - return; - } - - $crm_data = [ - 'email' => $form_data['email']['value'] ?? '', - 'name' => $form_data['name']['value'] ?? '', - 'phone' => $form_data['phone']['value'] ?? '', - 'company' => $form_data['company']['value'] ?? '', - 'source' => 'sureforms', - 'entry_id' => $form_data['entry_id'] ?? 0 - ]; - - $response = wp_remote_post('https://crm.example.com/api/leads', [ - 'body' => json_encode($crm_data), - 'headers' => [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer ' . get_option('crm_api_key') - ] - ]); - - if (is_wp_error($response)) { - error_log('CRM sync failed: ' . $response->get_error_message()); - } else { - $crm_id = json_decode(wp_remote_retrieve_body($response))->id; - // Store CRM ID with entry - update_post_meta($form_data['entry_id'], '_crm_lead_id', $crm_id); - } - } - - public function delete_from_crm($entry_id) { - $crm_id = get_post_meta($entry_id, '_crm_lead_id', true); - if ($crm_id) { - wp_remote_request('https://crm.example.com/api/leads/' . $crm_id, [ - 'method' => 'DELETE', - 'headers' => [ - 'Authorization' => 'Bearer ' . get_option('crm_api_key') - ] - ]); - } - } -} - -new SureForms_CRM_Integration(); -``` - ---- - -## Hook Priority Guidelines - -**Priority Values:** -- **5:** Execute before default behavior -- **10:** Default priority (most hooks) -- **15:** Execute after default behavior -- **20+:** Late execution -- **100+:** Very late (cleanup, logging) - -**Example:** -```php -// Execute before SureForms processes submission -add_action('srfm_before_submission', 'my_function', 5); - -// Execute at default time -add_action('srfm_before_submission', 'my_function', 10); - -// Execute after SureForms processes submission -add_action('srfm_before_submission', 'my_function', 15); -``` - ---- - -## Debugging Hooks - -### Enable Hook Debugging - -```php -// Add to wp-config.php -define('SRFM_DEBUG_HOOKS', true); - -// Log all SureForms hooks -add_action('all', function($hook) { - if (strpos($hook, 'srfm_') === 0) { - error_log('Hook fired: ' . $hook); - } -}); -``` - -### View Hook Execution Order - -```php -// Log hook priority and callback -add_action('all', function($hook) { - if (strpos($hook, 'srfm_') === 0) { - global $wp_filter; - if (isset($wp_filter[$hook])) { - foreach ($wp_filter[$hook]->callbacks as $priority => $callbacks) { - foreach ($callbacks as $callback) { - error_log(sprintf( - 'Hook: %s, Priority: %d, Callback: %s', - $hook, - $priority, - is_array($callback['function']) ? - get_class($callback['function'][0]) . '::' . $callback['function'][1] : - $callback['function'] - )); - } - } - } - } -}); -``` - ---- - -## Best Practices - -### 1. Always Check Hook Existence - -```php -if (function_exists('add_action')) { - add_action('srfm_after_submission_process', 'my_function'); -} -``` - -### 2. Use Proper Nonces for Custom Forms - -```php -add_filter('srfm_before_entry_data', function($data, $form_id) { - // Verify custom nonce if adding custom fields - if (isset($_POST['custom_nonce'])) { - if (!wp_verify_nonce($_POST['custom_nonce'], 'custom_action')) { - return new WP_Error('nonce_failed', 'Security check failed'); - } - } - return $data; -}, 10, 2); -``` - -### 3. Return Correct Data Types - -```php -// Filter must return array -add_filter('srfm_form_submit_data', function($data) { - // ✅ GOOD - returns array - return array_merge($data, ['custom' => 'value']); - - // ❌ BAD - returns nothing - // (void return breaks functionality) -}, 10, 1); -``` - -### 4. Handle Errors Gracefully - -```php -add_action('srfm_after_submission_process', function($form_data) { - try { - // Risky operation - send_to_external_api($form_data); - } catch (Exception $e) { - error_log('External API failed: ' . $e->getMessage()); - // Don't throw - allow submission to continue - } -}, 10, 1); -``` - ---- - -## Summary - -**Hook Categories:** -- **Lifecycle:** 2 hooks (core_loaded, init) -- **Submission:** 10+ hooks (before, during, after) -- **Validation:** 5+ hooks (field-specific and general) -- **Email:** 5+ hooks (before, during, after send) -- **Rendering:** 10+ hooks (markup, classes, attributes) -- **Data:** 8+ hooks (modify, filter, transform) -- **Database:** 3+ hooks (before/after operations) -- **Payment:** 6+ hooks (process, success, failure) -- **Admin:** 5+ hooks (form creation, updates) - -**Total:** 450+ hooks across the plugin - -**Auto-Generation Note:** This document contains the most commonly used hooks. For a complete list, run the hook extraction script: -```bash -php scripts/extract-hooks.php > docs/COMPLETE-HOOKS-LIST.md -``` - ---- - -**Related Documentation:** -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide -- [ARCHITECTURE.md](../02-architecture/ARCHITECTURE.md) - System design -- [REST-API.md](REST-API.md) - REST API endpoints -- [DATABASE-SCHEMA.md](DATABASE-SCHEMA.md) - Database tables diff --git a/docs/05-frontend/FRONTEND-GUIDE.md b/docs/05-frontend/FRONTEND-GUIDE.md deleted file mode 100644 index d26f09ee8..000000000 --- a/docs/05-frontend/FRONTEND-GUIDE.md +++ /dev/null @@ -1,1197 +0,0 @@ -# SureForms Frontend Development Guide - -> **Purpose:** Comprehensive guide for React/Gutenberg block development -> **Audience:** Frontend developers working with React, Gutenberg, and WordPress -> **Last Updated:** 2026-02-06 -> **Plugin Version:** 2.5.0 - -## Overview - -SureForms frontend is built with modern JavaScript technologies integrated deeply with WordPress's Gutenberg block editor: - -- **React 18.2:** UI components and interactive features -- **Gutenberg Blocks:** WordPress block editor integration for form building -- **TailwindCSS 3.4:** Utility-first CSS framework -- **React Query 5.x:** Server state management and caching -- **@wordpress/scripts 26.2:** Build tooling (Webpack 5 wrapper) - ---- - -## Table of Contents - -- [Architecture Overview](#architecture-overview) -- [Directory Structure](#directory-structure) -- [Creating Gutenberg Blocks](#creating-gutenberg-blocks) -- [State Management](#state-management) -- [Component Patterns](#component-patterns) -- [Styling](#styling) -- [Form Validation](#form-validation) -- [Internationalization](#internationalization) -- [Performance Optimization](#performance-optimization) -- [Testing](#testing) - ---- - -## Architecture Overview - -### Technology Stack - -``` -┌─────────────────────────────────────────┐ -│ Admin UI (React Apps) │ -│ Dashboard, Settings, Entries, Editor │ -└──────────────┬──────────────────────────┘ - │ - ┌──────────┴──────────┐ - │ │ -┌───▼────────┐ ┌──────▼──────┐ -│ Gutenberg │ │ Admin │ -│ Blocks │ │ Pages │ -│ (Editor) │ │ (Router) │ -└───┬────────┘ └──────┬──────┘ - │ │ - └──────────┬─────────┘ - │ - ┌──────────▼──────────┐ - │ State Management │ - │ Data Store + Hooks │ - └──────────┬──────────┘ - │ - ┌──────────▼──────────┐ - │ REST API │ - │ /sureforms/v1/* │ - └─────────────────────┘ -``` - -### Component Layers - -1. **Gutenberg Block Layer** - Form builder blocks (src/blocks/) -2. **Admin UI Layer** - React SPA pages (src/admin/) -3. **Shared Components** - Reusable UI (src/components/) -4. **State Layer** - Data stores (src/store/) -5. **API Layer** - WordPress REST API - ---- - -## Directory Structure - -``` -src/ -├── blocks/ # Gutenberg block components -│ ├── blocks.js # Block registration entry -│ ├── register-block.js # Block utilities -│ │ -│ ├── sform/ # Form container block -│ │ ├── index.js # Block registration -│ │ ├── edit.js # Editor component -│ │ ├── save.js # Frontend save (returns null) -│ │ ├── block.json # Block metadata -│ │ ├── components/ # Block-specific components -│ │ └── style.scss # Block styles -│ │ -│ ├── input/ # Text input field block -│ │ ├── edit.js # Editor with InspectorControls -│ │ ├── save.js # Server-side rendering -│ │ └── block.json # Attributes schema -│ │ -│ └── [14+ more blocks] # Email, textarea, checkbox, etc. -│ -├── admin/ # Admin dashboard React apps -│ ├── dashboard/ # Forms listing page -│ │ ├── index.js # Entry point -│ │ ├── FormsTable.js # Table component -│ │ ├── Analytics.js # Analytics widgets -│ │ └── styles.scss -│ │ -│ ├── single-form-settings/ # Form editor -│ │ ├── Editor.js # Main editor wrapper -│ │ ├── InstantForm.js # Live preview -│ │ ├── tabs/ # Settings tabs -│ │ │ ├── GeneralSettings.js -│ │ │ └── StyleSettings.js -│ │ └── components/ # Editor components -│ │ -│ ├── entries/ # Entry management -│ ├── settings/ # Global settings -│ ├── payment/ # Payment settings -│ └── onboarding/ # Setup wizard -│ -├── components/ # Shared React components -│ ├── ui/ # UI primitives -│ │ ├── Button/ -│ │ ├── Input/ -│ │ ├── Modal/ -│ │ └── Tooltip/ -│ │ -│ ├── form/ # Form-specific -│ │ ├── FieldWrapper/ -│ │ ├── ValidationMessage/ -│ │ └── FormContainer/ -│ │ -│ ├── inspector-tabs/ # Block editor tabs -│ ├── color-switch-control/ # Color picker -│ ├── spacing-control/ # Responsive spacing -│ └── [40+ component dirs] -│ -├── srfm-controls/ # Gutenberg block controls -│ ├── index.js # Control registry -│ ├── getPreviewType.js # Responsive preview -│ └── [18+ control definitions] -│ -├── store/ # WordPress Data Store (Redux) -│ ├── store.js # Store creation -│ ├── actions.js # Action creators -│ ├── reducer.js # Reducer function -│ ├── selectors.js # Selectors -│ └── constants.js # Action types -│ -├── utils/ # Utility functions -│ ├── Helpers.js # Common helpers -│ ├── datePickerHelper.js -│ └── addressFieldHelper.js -│ -└── styles/ # Global styles - ├── typography/ - └── breakpoints/ -``` - -**Build Output:** -``` -assets/build/ -├── formEditor.js # Form editor bundle (~1.9MB) -├── blocks.js # Gutenberg blocks (~1.3MB) -├── dashboard.js # Dashboard page (~1.3MB) -├── entries.js # Entries page (~728KB) -├── forms.js # Forms listing (~727KB) -├── settings.js # Settings page (~1.1MB) -├── formSubmit.js # Frontend submission (~60KB) -└── *.asset.php # Webpack dependency manifests -``` - ---- - -## Creating Gutenberg Blocks - -### Block Anatomy - -Every form field block consists of: -1. **block.json** - Metadata and attributes schema -2. **edit.js** - Editor component (React) -3. **save.js** - Frontend rendering (usually null, PHP renders) -4. **style.scss** - Block-specific styles -5. **PHP class** - Server-side registration and rendering - -### Step-by-Step: Create a New Block - -#### Step 1: Create Block Directory - -```bash -mkdir -p src/blocks/custom-field -cd src/blocks/custom-field -``` - -#### Step 2: Create block.json - -**File:** [src/blocks/custom-field/block.json](../../src/blocks/custom-field/block.json) - -```json -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, - "name": "sureforms/custom-field", - "title": "Custom Field", - "category": "sureforms", - "icon": "text", - "description": "A custom form field", - "keywords": ["form", "custom", "field"], - "supports": { - "html": false, - "anchor": false, - "customClassName": false - }, - "attributes": { - "label": { - "type": "string", - "default": "Custom Field" - }, - "slug": { - "type": "string", - "default": "custom_field" - }, - "placeholder": { - "type": "string", - "default": "" - }, - "required": { - "type": "boolean", - "default": false - }, - "helpText": { - "type": "string", - "default": "" - } - }, - "editorScript": "file:./index.js", - "editorStyle": "file:./editor.scss", - "style": "file:./style.scss" -} -``` - -#### Step 3: Create edit.js (Editor Component) - -**File:** [src/blocks/custom-field/edit.js](../../src/blocks/custom-field/edit.js) - -```javascript -import { __ } from '@wordpress/i18n'; -import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; -import { PanelBody, TextControl, ToggleControl, TextareaControl } from '@wordpress/components'; - -export default function Edit({ attributes, setAttributes, clientId }) { - const { label, slug, placeholder, required, helpText } = attributes; - - const blockProps = useBlockProps({ - className: 'srfm-block srfm-custom-field-block', - }); - - return ( - <> - {/* Inspector Panel (Right Sidebar) */} - - - setAttributes({ label: value })} - help={__('Field label shown to users', 'sureforms')} - /> - - setAttributes({ slug: value })} - help={__('Unique identifier for this field', 'sureforms')} - /> - - setAttributes({ placeholder: value })} - /> - - setAttributes({ required: value })} - /> - - setAttributes({ helpText: value })} - help={__('Additional instructions for users', 'sureforms')} - /> - - - - {/* Block Preview in Editor */} -
-
- - - - - {helpText && ( - - {helpText} - - )} -
-
- - ); -} -``` - -#### Step 4: Create save.js - -**File:** [src/blocks/custom-field/save.js](../../src/blocks/custom-field/save.js) - -```javascript -/** - * Save function - return null for dynamic blocks (rendered server-side). - * - * SureForms blocks are rendered server-side via PHP for: - * - Better security (no client-side data exposure) - * - Consistent rendering across contexts - * - Easier form submission handling - */ -export default function save() { - return null; -} -``` - -#### Step 5: Create index.js (Registration) - -**File:** [src/blocks/custom-field/index.js](../../src/blocks/custom-field/index.js) - -```javascript -import { registerBlockType } from '@wordpress/blocks'; -import Edit from './edit'; -import save from './save'; -import metadata from './block.json'; - -// Register block -registerBlockType(metadata.name, { - ...metadata, - edit: Edit, - save, -}); -``` - -#### Step 6: Add to blocks.js - -**File:** [src/blocks/blocks.js](../../src/blocks/blocks.js) - -```javascript -// ... other imports -import './custom-field'; -``` - -#### Step 7: Create PHP Server-Side Rendering - -**File:** [inc/blocks/custom-field/class-custom-field.php](../../inc/blocks/custom-field/class-custom-field.php) - -```php - -
- - - - - /> - - - - - - -
- { - setValue(e.target.value); - - // Validate - if (e.target.value.length < 3) { - setError('Minimum 3 characters'); - } else { - setError(''); - } - }; - - return ( -
- - {error && {error}} -
- ); -} -``` - -### 2. WordPress Data Store (Redux-based) - -For block editor state and form metadata. - -**Define Store** ([src/store/store.js](../../src/store/store.js)): - -```javascript -import { createReduxStore, register } from '@wordpress/data'; -import apiFetch from '@wordpress/api-fetch'; - -const DEFAULT_STATE = { - formSettings: {}, - blockAttributes: {}, - isLoading: false, -}; - -const actions = { - setFormSettings(settings) { - return { - type: 'SET_FORM_SETTINGS', - settings, - }; - }, - - updateBlockAttribute(blockId, attribute, value) { - return { - type: 'UPDATE_BLOCK_ATTRIBUTE', - blockId, - attribute, - value, - }; - }, - - *fetchFormSettings(formId) { - yield { type: 'SET_LOADING', isLoading: true }; - - try { - const settings = yield apiFetch({ - path: `/sureforms/v1/forms/${formId}/settings`, - }); - - return actions.setFormSettings(settings); - } catch (error) { - console.error('Failed to fetch settings:', error); - return { type: 'FETCH_ERROR', error }; - } - }, -}; - -const selectors = { - getFormSettings(state) { - return state.formSettings; - }, - - getBlockAttribute(state, blockId, attribute) { - return state.blockAttributes[blockId]?.[attribute]; - }, - - isLoading(state) { - return state.isLoading; - }, -}; - -const reducer = (state = DEFAULT_STATE, action) => { - switch (action.type) { - case 'SET_FORM_SETTINGS': - return { - ...state, - formSettings: action.settings, - isLoading: false, - }; - - case 'UPDATE_BLOCK_ATTRIBUTE': - return { - ...state, - blockAttributes: { - ...state.blockAttributes, - [action.blockId]: { - ...state.blockAttributes[action.blockId], - [action.attribute]: action.value, - }, - }, - }; - - case 'SET_LOADING': - return { - ...state, - isLoading: action.isLoading, - }; - - default: - return state; - } -}; - -export const store = createReduxStore('srfm/forms', { - reducer, - actions, - selectors, -}); - -register(store); -``` - -**Use Store in Components:** - -```javascript -import { useSelect, useDispatch } from '@wordpress/data'; - -function FormEditor({ formId }) { - // Select data from store - const { formSettings, isLoading } = useSelect((select) => ({ - formSettings: select('srfm/forms').getFormSettings(), - isLoading: select('srfm/forms').isLoading(), - })); - - // Dispatch actions to store - const { fetchFormSettings, setFormSettings } = useDispatch('srfm/forms'); - - useEffect(() => { - fetchFormSettings(formId); - }, [formId]); - - if (isLoading) { - return ; - } - - return ( -
-

{formSettings.title}

- {/* Editor UI */} -
- ); -} -``` - -### 3. React Query (Server State) - -For API responses and caching. - -**Setup Query Client** ([src/admin/dashboard/index.js](../../src/admin/dashboard/index.js)): - -```javascript -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 60 * 1000, // 5 minutes - retry: 1, - }, - }, -}); - -function App() { - return ( - - - - ); -} -``` - -**Create Custom Hooks** ([src/hooks/useFormData.js](../../src/hooks/)): - -```javascript -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import apiFetch from '@wordpress/api-fetch'; - -// Fetch form data -export function useFormData(formId) { - return useQuery({ - queryKey: ['form', formId], - queryFn: async () => { - const response = await apiFetch({ - path: `/sureforms/v1/forms/${formId}`, - }); - return response; - }, - enabled: !!formId, - }); -} - -// Update form data -export function useUpdateForm() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ formId, data }) => { - return await apiFetch({ - path: `/sureforms/v1/forms/${formId}`, - method: 'PUT', - data, - }); - }, - onSuccess: (data, variables) => { - // Invalidate and refetch - queryClient.invalidateQueries(['form', variables.formId]); - }, - }); -} - -// Fetch entries -export function useEntries(formId, page = 1) { - return useQuery({ - queryKey: ['entries', formId, page], - queryFn: async () => { - return await apiFetch({ - path: `/sureforms/v1/forms/${formId}/entries?page=${page}`, - }); - }, - keepPreviousData: true, // For pagination - }); -} -``` - -**Use in Components:** - -```javascript -function FormEditor({ formId }) { - const { data: form, isLoading, error } = useFormData(formId); - const updateForm = useUpdateForm(); - - const handleSave = async () => { - try { - await updateForm.mutateAsync({ - formId, - data: { title: 'Updated Title' }, - }); - // Success toast - toast.success('Form saved!'); - } catch (error) { - // Error toast - toast.error('Failed to save form'); - } - }; - - if (isLoading) return ; - if (error) return
Error: {error.message}
; - - return ( -
-

{form.title}

- -
- ); -} -``` - ---- - -## Component Patterns - -### Functional Components with Hooks - -**Always use functional components** (not class components): - -```javascript -// ✅ GOOD - Functional component -function MyComponent({ prop1, prop2 }) { - const [state, setState] = useState(null); - - useEffect(() => { - // Side effects - }, []); - - return
{/* JSX */}
; -} - -// ❌ BAD - Class component (deprecated pattern) -class MyComponent extends React.Component { - // Don't use class components in SureForms -} -``` - -### Props Destructuring - -```javascript -// ✅ GOOD - Destructure in signature -function Button({ label, onClick, disabled = false, variant = 'primary' }) { - return ( - - ); -} - -// ❌ BAD - Access via props object -function Button(props) { - return ; -} -``` - -### Conditional Rendering - -```javascript -function FormField({ label, required, error }) { - return ( -
- - - {/* Ternary for if/else */} - {error ? ( - {error} - ) : ( - Optional - )} - - {/* Conditional with && */} - {!error && Valid} -
- ); -} -``` - -### Event Handlers - -```javascript -function FormContainer() { - // useCallback for stable reference - const handleSubmit = useCallback((event) => { - event.preventDefault(); - // Handle submit - }, []); - - // Arrow function for inline with parameters - const handleFieldChange = useCallback((fieldId, value) => { - setFormData((prev) => ({ - ...prev, - [fieldId]: value, - })); - }, []); - - return ( -
- handleFieldChange('name', e.target.value)} - /> -
- ); -} -``` - ---- - -## Styling - -### TailwindCSS (Preferred) - -Use Tailwind utility classes for rapid development: - -```javascript -function Card({ title, children }) { - return ( -
-

- {title} -

-
- {children} -
-
- ); -} -``` - -**Responsive Design:** - -```javascript -
- {/* Full width mobile, half on tablet, third on desktop */} -
- -
- {/* Responsive text size */} -
-``` - -### Custom SCSS (When Needed) - -**Use BEM methodology** with `.srfm-` prefix: - -```scss -// style.scss -.srfm-custom-field { - // Block - &__label { - // Element - @apply block text-sm font-medium text-gray-700 mb-2; - - .required { - // Nested element - @apply text-red-500 ml-1; - } - } - - &__input { - // Element - @apply w-full px-4 py-2 border border-gray-300 rounded-md; - @apply focus:outline-none focus:ring-2 focus:ring-blue-500; - - &:disabled { - // Modifier via pseudo-class - @apply bg-gray-100 cursor-not-allowed; - } - } - - &--error { - // Block modifier - .srfm-custom-field__input { - @apply border-red-500; - } - } -} -``` - -### Conditional Classes (classnames) - -```javascript -import classnames from 'classnames'; - -function Button({ variant, size, disabled, className }) { - const classes = classnames( - 'srfm-button', - `srfm-button--${variant}`, - `srfm-button--${size}`, - { - 'srfm-button--disabled': disabled, - }, - className // Allow override - ); - - return ; -} -``` - ---- - -## Form Validation - -### Client-Side Validation (Frontend) - -**Inline validation:** - -```javascript -import { useState } from 'react'; - -function EmailField({ value, onChange, required }) { - const [error, setError] = useState(''); - - const validateEmail = (email) => { - const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return regex.test(email); - }; - - const handleBlur = () => { - if (required && !value) { - setError(__('Email is required', 'sureforms')); - } else if (value && !validateEmail(value)) { - setError(__('Please enter a valid email', 'sureforms')); - } else { - setError(''); - } - }; - - return ( -
- - {error && {error}} -
- ); -} -``` - -**Form-level validation:** - -```javascript -function FormContainer() { - const [formData, setFormData] = useState({}); - const [errors, setErrors] = useState({}); - - const validate = () => { - const newErrors = {}; - - if (!formData.name) { - newErrors.name = 'Name is required'; - } - - if (!formData.email || !isValidEmail(formData.email)) { - newErrors.email = 'Valid email is required'; - } - - if (formData.age && formData.age < 18) { - newErrors.age = 'Must be 18 or older'; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - if (!validate()) { - return; - } - - // Submit form - try { - await submitForm(formData); - toast.success('Form submitted!'); - } catch (error) { - toast.error('Submission failed'); - } - }; - - return ( -
- {/* Form fields */} - -
- ); -} -``` - ---- - -## Internationalization (i18n) - -### Translation Functions - -```javascript -import { __, _n, sprintf } from '@wordpress/i18n'; - -// Simple translation -const label = __('Submit Form', 'sureforms'); - -// Plural forms -const message = _n( - 'One field is required', - '%d fields are required', - count, - 'sureforms' -); - -// String interpolation -const greeting = sprintf( - __('Hello, %s!', 'sureforms'), - userName -); -``` - -### Best Practices - -**✅ DO:** -```javascript -// Use sprintf for variables -const msg = sprintf(__('Form %s saved', 'sureforms'), formName); - -// Separate strings for different contexts -const saveButton = __('Save', 'sureforms'); -const saveForm = __('Save Form', 'sureforms'); -``` - -**❌ DON'T:** -```javascript -// Never use template literals in translations -const msg = __(`Form ${formName} saved`, 'sureforms'); // ❌ WRONG - -// Never concatenate -const msg = __('Form ', 'sureforms') + formName + __(' saved', 'sureforms'); // ❌ WRONG -``` - ---- - -## Performance Optimization - -### Code Splitting - -```javascript -import { lazy, Suspense } from 'react'; - -const HeavyComponent = lazy(() => import('./HeavyComponent')); - -function App() { - return ( - }> - - - ); -} -``` - -### Memoization - -```javascript -import { useMemo, useCallback } from 'react'; - -function DataTable({ data, onRowClick }) { - // Memoize expensive calculations - const sortedData = useMemo(() => { - console.log('Sorting data...'); - return data.sort((a, b) => a.name.localeCompare(b.name)); - }, [data]); // Only recompute when data changes - - // Memoize callbacks - const handleRowClick = useCallback( - (row) => { - onRowClick(row); - }, - [onRowClick] - ); - - return {/* Render sortedData */}
; -} -``` - -### React.memo - -```javascript -import { memo } from 'react'; - -// Prevent re-renders if props haven't changed -const ExpensiveComponent = memo(function ExpensiveComponent({ data }) { - console.log('Rendering...'); - return
{/* Complex rendering */}
; -}); -``` - ---- - -## Testing - -### Component Testing (Jest + React Testing Library) - -```javascript -import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import FormField from '../FormField'; - -describe('FormField', () => { - it('renders with label', () => { - render(); - expect(screen.getByText('Name')).toBeInTheDocument(); - }); - - it('shows required indicator', () => { - render(); - expect(screen.getByText('*')).toBeInTheDocument(); - }); - - it('calls onChange when value changes', () => { - const handleChange = jest.fn(); - render(); - - const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: 'test' } }); - - expect(handleChange).toHaveBeenCalledWith('test'); - }); - - it('displays error message', () => { - render(); - expect(screen.getByText('This field is required')).toBeInTheDocument(); - }); -}); -``` - ---- - -## Summary - -**Key Takeaways:** - -1. **Use React 18 functional components** with hooks -2. **Gutenberg blocks** are registered in JavaScript, rendered in PHP -3. **State management** uses three layers: local, WordPress Data Store, React Query -4. **Styling** prefers TailwindCSS utilities, custom SCSS for complex needs -5. **Validation** happens client-side (UX) and server-side (security) -6. **i18n** uses `@wordpress/i18n` functions, no template literals -7. **Performance** optimized with memoization, code splitting, virtualization - ---- - -**Related Documentation:** -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide -- [GETTING-STARTED.md](../01-getting-started/GETTING-STARTED.md) - Setup -- [CODING-STANDARDS.md](../03-development/CODING-STANDARDS.md) - Code style -- [TESTING.md](../08-testing/TESTING.md) - Testing guide diff --git a/docs/FORM_MIGRATION_OVERVIEW.md b/docs/FORM_MIGRATION_OVERVIEW.md deleted file mode 100644 index dd7fac4d6..000000000 --- a/docs/FORM_MIGRATION_OVERVIEW.md +++ /dev/null @@ -1,77 +0,0 @@ -# SureForms — Form Migration (Overview) - -> Looking for the full technical plan? See `FORM_MIGRATION_PLAN.md` in this folder. - -## What this feature does - -Lets users move their existing forms into SureForms from the other popular form-builder plugins, in one click — no copy-pasting, no rebuilding. - -## Which plugins we'll support - -| Source plugin | Approx. user base | Status | -|---|---|---| -| Contact Form 7 | 10M+ sites | **Available in v1** | -| WPForms | 6M+ sites | Coming next | -| Gravity Forms | ~650K sites | Coming next | -| Ninja Forms | 600K+ sites | Later release | -| Caldera Forms | ~50K sites (unmaintained plugin) | Later release | - -Combined audience: roughly **17 million sites** that could move to SureForms. - -## What an import gives you - -When a user clicks "Import", we bring across: - -1. **The form's fields** — text, email, phone, dropdown, checkbox, radio, file URL, number, textarea, consent checkboxes, etc. They show up as native SureForms blocks, ready to edit in the Gutenberg editor. -2. **The form's title** — same as the original. -3. **A working admin notification email** — recipient is the site admin, subject mentions the form name, body uses `{all_data}` so submissions go straight to your inbox. -4. **A success message** — a clean "Thank you" confirmation shown after submission. - -The imported form works on day one. Users can polish it in SureForms' editor. - -## What doesn't transfer yet (and why) - -| Thing | Why not | -|---|---| -| File-upload fields | SureForms doesn't have a file-upload block yet — coming separately. | -| Quiz / signature / rating fields | No SureForms equivalent yet. | -| The exact email-body text from CF7 | CF7 uses a different placeholder syntax (`[your-name]` vs SureForms' smart tags). Trying to translate it gets unreliable, so we ship a clean default instead. WPForms and Gravity emails will come through fully when those are added. | -| Multi-step / page breaks | Waiting on a SureForms multi-step block. | - -When a field can't be migrated, the import screen lists it under **"Some fields were skipped"** so the user knows what to add by hand. - -## How users will find it - -A new **Migration** tab inside **SureForms → Settings**, at the bottom of the sidebar. The workflow is four short steps: - -1. **Choose a source** — tiles show each detected plugin and how many forms it has. -2. **Pick which forms** — checkbox list; "Select all" if you want everything. -3. **Preview** — see what the migrated form will look like before anything is saved. -4. **Confirm & import** — done. The result screen has direct "Edit in SureForms" links. - -If a user re-runs the import for the same source form, we **update** the existing SureForms version instead of creating a duplicate. - -## What's already built - -- The Migration tab is live in **SureForms → Settings → Migration** -- Contact Form 7 imports work end to end (fields + notification + confirmation) -- "Preview before importing" works — nothing gets saved until the user confirms -- Skipped fields are surfaced clearly to the user - -## What's next - -1. **WPForms importer** — including its full email-notification and confirmation rules -2. **Gravity Forms importer** — including notifications, scheduling, entry limits -3. **Ninja Forms + Caldera Forms importers** -4. **Entry migration** — bringing across historical form submissions (capped at 1,000 per form to keep imports fast) -5. **Filling in field gaps** — once SureForms ships file-upload, multi-step, and signature blocks, the migrator will start carrying those over too - -## Why we chose this approach - -We modelled the migrator on **Fluent Forms**' approach, which has been running in production on 700K+ sites for years. Same flow (source → forms → preview → import), same safety net (re-imports update rather than duplicate), and the same honest stance on edge cases like CF7 email templates. - -## Where to find things (for engineers) - -- Full technical plan with code structure, REST endpoints, field-mapping tables: `FORM_MIGRATION_PLAN.md` -- PHP code: `inc/migrator/` -- Admin tab UI: `src/admin/settings/migration/` diff --git a/docs/FORM_MIGRATION_PLAN.md b/docs/FORM_MIGRATION_PLAN.md deleted file mode 100644 index 9d7c2a9a8..000000000 --- a/docs/FORM_MIGRATION_PLAN.md +++ /dev/null @@ -1,458 +0,0 @@ -# SureForms — Form Migration Implementation Plan - -## 1. Goal - -Build a one-click migrator that imports forms (and optionally entries) from the 5 major WordPress form plugins into SureForms, modelled on Fluent Forms' production-tested migrator architecture (~5,400 LOC across 5 sources, deployed on 700K+ sites). - -**Sources to support in v1**: Contact Form 7, WPForms (Lite + Pro), Gravity Forms, Ninja Forms, Caldera Forms. - -## 2. Why Fluent Forms' pattern - -Fluent's full migrator source was read end-to-end (`fluentform/app/Services/Migrator/`). Their architecture is: - -- **One abstract base class** with the import loop, idempotency map, and a ~1,200-line canonical field dictionary. -- **Thin per-source subclasses** (500–900 LOC each) that only do (a) `source_type → fluent_id` mapping and (b) source-specific args formatting. -- **4 AJAX endpoints** for: list sources, list forms in source, import forms, import entries. -- **Template Method pattern** — base class drives the flow, subclasses fill in 5 abstract methods (`exist`, `get_source_forms`, `get_source_form_id`, `get_source_form_name`, `build_form_content`, `get_form_metas`). - -It's clean, proven, and easy to extend. SureForms will adopt the same shape, adapted for: - -1. **REST instead of AJAX** — SureForms already uses `/sureforms/v1/` namespace (`inc/rest-api.php`); AJAX would be inconsistent. -2. **Block markup output instead of JSON** — SureForms forms are Gutenberg block strings in `post_content` of the `sureforms_form` CPT, not JSON in a custom table. So the importer's final stage is to serialize block markup, not assemble a flat field array. -3. **15 SureForms blocks as targets** — instead of Fluent's ~25 field types. - -## 3. Sources to import — confirmed field type maps - -These are lifted directly from Fluent's `fieldTypes()` methods in each migrator class. Total source field types we need to handle: **110** (with overlap). - -### 3.1 Contact Form 7 (13 form-tag types → SureForms blocks) -Source: `wpcf7_contact_form` post type, `_form` meta (shortcode template). - -| CF7 form-tag | SureForms block | Notes | -|---|---|---| -| `text`, `text*` | `srfm/input` | type=text | -| `email`, `email*` | `srfm/email` | | -| `url`, `url*` | `srfm/url` | | -| `tel`, `tel*` | `srfm/phone` | | -| `number`, `number*` | `srfm/number` | | -| `range` | `srfm/number` | slider mode | -| `date` | `srfm/input` | type=date (no native date block) | -| `textarea`, `textarea*` | `srfm/textarea` | | -| `select`, `select*` | `srfm/dropdown` | `multiple` → multi-select | -| `checkbox`, `checkbox*` | `srfm/checkbox` | | -| `radio` | `srfm/multi-choice` | | -| `acceptance` | `srfm/gdpr` | tnc_html → content | -| `file`, `file*` | unsupported in v1 | no native `srfm/file` — flag | -| `quiz`, `captchar`, `hidden` | skipped | legacy / not in v1 scope | - -Detection: `defined('WPCF7_PLUGIN')`. Entries: requires Flamingo plugin. - -### 3.2 WPForms (22 field types → SureForms blocks) -Source: CPT `wpforms`, field data as JSON in `post_content`. - -| WPForms type | SureForms block | Notes | -|---|---|---| -| `text` | `srfm/input` | | -| `email` | `srfm/email` | | -| `url` | `srfm/url` | | -| `phone` | `srfm/phone` | | -| `number`, `number-slider` | `srfm/number` | | -| `textarea`, `richtext` | `srfm/textarea` | rich → fallback | -| `select`, `multi_select` | `srfm/dropdown` | | -| `radio` | `srfm/multi-choice` | | -| `checkbox` | `srfm/checkbox` | | -| `name` | `srfm/input × 3` | split first/middle/last | -| `address` | `srfm/address` | structured fields | -| `date-time` | `srfm/input` | type=date until native block | -| `password` | `srfm/input` | type=password | -| `hidden` | `srfm/input` | type=hidden | -| `html`, `divider` | structural — core/html or section break | | -| `pagebreak` | unsupported in v1 | no native multi-step yet | -| `file-upload`, `rating`, `layout` | unsupported in v1 | flag | - -Detection: `function_exists('wpforms')`. Entries: WPForms Pro only. - -### 3.3 Gravity Forms (22 field types → SureForms blocks) -Source: custom tables `gf_form` + `gf_form_meta` (serialized PHP array). - -| Gravity type | SureForms block | Notes | -|---|---|---| -| `email` | `srfm/email` | | -| `text` | `srfm/input` | | -| `name` | `srfm/input × 3` | first/middle/last sub-inputs | -| `hidden` | `srfm/input` (type=hidden) | | -| `textarea` | `srfm/textarea` | | -| `website` | `srfm/url` | | -| `phone` | `srfm/phone` | | -| `select`, `multiselect` | `srfm/dropdown` | | -| `checkbox` | `srfm/checkbox` | | -| `radio` | `srfm/multi-choice` | | -| `date`, `time` | `srfm/input` (type=date/time) | | -| `number` | `srfm/number` | | -| `address` | `srfm/address` | | -| `consent` | `srfm/gdpr` | | -| `captcha` | unsupported | external service | -| `html` | core/html (or skip) | | -| `section` | section break / divider | | -| `page` | unsupported in v1 | multi-step | -| `list` | unsupported in v1 | repeater not in srfm | -| `fileupload` | unsupported in v1 | no srfm/file | - -Detection: `class_exists('GFForms')`. Entries: `gf_entry`, `gf_entry_meta` tables. - -### 3.4 Ninja Forms (27 field type aliases → SureForms blocks) -Source: custom tables `nf3_forms`, `nf3_fields`, `nf3_field_meta` (JOIN-based). - -| Ninja type | SureForms block | Notes | -|---|---|---| -| `email` | `srfm/email` | | -| `textbox`, `firstname`, `lastname`, `address`, `city`, `zip` | `srfm/input` | (Ninja splits address into individual text fields) | -| `liststate`, `listcountry` | `srfm/dropdown` | | -| `textarea` | `srfm/textarea` | | -| `phone` | `srfm/phone` | | -| `select`, `listselect` | `srfm/dropdown` | | -| `listmultiselect` | `srfm/dropdown` (multi) | | -| `radio`, `listradio`, `listimage`, `toggle_switch` | `srfm/multi-choice` | | -| `checkbox`, `listcheckbox` | `srfm/checkbox` | | -| `date` | `srfm/input` (type=date) | | -| `number` | `srfm/number` | | -| `hidden` | `srfm/input` (type=hidden) | | -| `html` | core/html | | -| `hr` | section divider | | -| `starrating` | unsupported in v1 | no srfm/rating | -| `repeater` | unsupported in v1 | | -| `recaptcha` | spam protection — emit as form-level setting | | -| `submit` | handled by `srfm/inline-button` | | - -Detection: `defined('NF_SERVER_URL')`. Entries: `nf_sub` CPT. - -### 3.5 Caldera Forms (26 field types → SureForms blocks) -Source: `Caldera_Forms_Forms::get_forms()` static API. Caldera is unmaintained — migration is a one-shot rescue. - -| Caldera type | SureForms block | Notes | -|---|---|---| -| `email` | `srfm/email` | | -| `text` | `srfm/input` | | -| `hidden` | `srfm/input` (type=hidden) | | -| `textarea`, `paragraph`, `wysiwyg` | `srfm/textarea` | | -| `url` | `srfm/url` | | -| `phone`, `phone_better` | `srfm/phone` | | -| `select`, `dropdown` | `srfm/dropdown` | | -| `filtered_select2` | `srfm/dropdown` (multi) | | -| `radio`, `toggle_switch` | `srfm/multi-choice` | | -| `checkbox` | `srfm/checkbox` | | -| `date_picker`, `date` | `srfm/input` (type=date) | | -| `number`, `range`, `calculation` | `srfm/number` | | -| `range_slider` | `srfm/number` (slider) | | -| `gdpr` | `srfm/gdpr` | | -| `section_break`, `html` | structural | | -| `color_picker`, `star_rating`, `file`, `cf2_file`, `advanced_file` | unsupported in v1 | flag | -| `button` | handled by `srfm/inline-button` | | - -Detection: `defined('CFCORE_VER')`. Caldera abandoned in 2022 — entries critical. - -## 4. SureForms target blocks (the destination schema) - -From `find sureforms/inc/blocks/*/block.json` — **15 blocks** total: - -| Block | Type | Used for | -|---|---|---| -| `srfm/form` | wrapper | Outer form container | -| `srfm/input` | field | Text/email/url/phone/password/date/hidden (HTML type via attr) | -| `srfm/email` | field | Email input | -| `srfm/textarea` | field | Paragraph text | -| `srfm/url` | field | URL input | -| `srfm/number` | field | Numeric input + slider mode | -| `srfm/phone` | field | International phone | -| `srfm/dropdown` | field | Select / multi-select | -| `srfm/multi-choice` | field | Radio / multiple choice | -| `srfm/checkbox` | field | Checkbox group | -| `srfm/address` | field | Structured address (line1/2/city/state/zip/country) | -| `srfm/gdpr` | field | Consent checkbox | -| `srfm/payment` | field | Stripe payment | -| `srfm/payment-history` | display | Past payments table | -| `srfm/inline-button` | structural | Submit / action button | - -**Gaps acknowledged**: no native rich-text, file upload, signature, rating, date-picker, multi-step (page break), or repeater blocks. Fields mapped to these in source plugins are flagged as `unsupported_fields[]` in the v1 response and surfaced in the import UI as warnings. They will be added in subsequent releases. - -## 5. Code layout (mirrors Fluent's, adapted to SureForms conventions) - -``` -sureforms/inc/migrator/ -├── class-migrator-bootstrap.php # ~200 LOC — REST routes, capability checks, factory -├── class-base-migrator.php # ~800 LOC — abstract import loop + idempotency + helpers -├── class-block-templates.php # ~600 LOC — canonical srfm/* block markup templates -├── importers/ -│ ├── class-cf7-importer.php # ~600 LOC — shortcode regex parser -│ ├── class-wpforms-importer.php # ~700 LOC — JSON decode -│ ├── class-gravity-importer.php # ~800 LOC — serialized array parser -│ ├── class-ninja-importer.php # ~600 LOC — JOIN across nf3_* tables -│ └── class-caldera-importer.php # ~500 LOC — Caldera static API -└── views/admin-page.php # ~150 LOC — admin page mount point - -sureforms/src/admin/migrator/ # React UI (~800 LOC) -├── MigratorApp.jsx # Top-level component -├── SourceList.jsx # Tile per detected source -├── FormSelector.jsx # Pick forms to import -├── ImportPreview.jsx # Dry-run rendered output -├── ImportResult.jsx # Success + unsupported warnings -└── EntryImporter.jsx # Per-form entry import -``` - -**Total estimate: ~5,750 LOC** for all 5 sources (close to Fluent's ~5,400 LOC, with the extra slack accounting for SureForms' block-markup serialization). - -## 6. REST API design - -Namespace: `/wp-json/sureforms/v1/migrator/` (extending the existing `/sureforms/v1/` namespace from `inc/rest-api.php`). - -| Method | Route | Body / Query | Returns | -|---|---|---|---| -| GET | `/migrator/sources` | — | `[{key, title, installed, form_count, supports_entries}]` | -| GET | `/migrator/sources/{key}/forms` | — | `[{id, name, field_count, imported_srfm_id, last_imported_at}]` | -| POST | `/migrator/sources/{key}/import` | `{form_ids: [], dry_run: bool, behavior: {}, post_status?: 'draft'\|'publish'}` | `{imported: [{srfm_id, edit_url, source_id}], failed: [], skipped: [], unsupported_fields: [], preview?: {}}` | -| POST | `/migrator/sources/{key}/forms/{id}/entries` _(planned — P3, not yet registered)_ | `{limit?: 1000}` | `{count_imported, count_skipped, count_total}` | - -**Security model** (per `sureforms/CLAUDE.md`): -- Every route's `permission_callback` requires `current_user_can('manage_options')`. -- REST cookie nonce (`X-WP-Nonce`) for all routes — these are admin-only, no public access. -- Sanitize `form_ids` via `array_map('absint', ...)`, `key` via allowlist `['cf7','wpforms','gravity','ninja','caldera']`. - -## 7. Canonical block template dictionary - -Analogue of Fluent's 1,200-line `defaultFieldConfig()` array, but emitting Gutenberg block markup strings instead of field-config arrays. - -One method per target block in `class-block-templates.php`: - -```php -final class SRFM_Block_Templates { - - public static function input( array $args ): string { - $defaults = [ - 'label' => '', - 'placeholder' => '', - 'required' => false, - 'field_type' => 'text', // text|email|number|password|date|hidden - 'help_text' => '', - 'default_val' => '', - 'min_length' => '', - 'max_length' => '', - 'css_class' => '', - ]; - $attrs = wp_parse_args( $args, $defaults ); - // Match the schema in src/blocks/input/block.json - $json = wp_json_encode( self::strip_empty( [ - 'label' => $attrs['label'], - 'placeholder' => $attrs['placeholder'], - 'required' => $attrs['required'], - 'fieldType' => $attrs['field_type'], - 'help' => $attrs['help_text'], - 'defaultValue' => $attrs['default_val'], - 'minLength' => $attrs['min_length'], - 'maxLength' => $attrs['max_length'], - 'className' => $attrs['css_class'], - ] ) ); - return "\n"; - } - - public static function email( array $args ): string { /* ... */ } - public static function dropdown( array $args ): string { /* ... */ } - public static function checkbox( array $args ): string { /* ... */ } - // ... one per srfm/* block - - public static function form_wrapper( string $inner_content ): string { - return "\n{$inner_content}\n"; - } -} -``` - -The base migrator's import loop concatenates the per-field strings, wraps them in `form_wrapper()`, and persists via `wp_insert_post(['post_type' => 'sureforms_form', 'post_content' => $markup])`. - -## 8. Base migrator contract (abstract methods) - -```php -abstract class SRFM_Base_Migrator { - - protected string $key; // 'cf7' | 'wpforms' | 'gravity' | 'ninja' | 'caldera' - protected string $title; // Display name - protected array $unsupported_fields = []; - - // Per-source plumbing (abstract — subclasses implement) - abstract public function exist(): bool; - abstract protected function get_source_forms(): array; - abstract protected function get_source_form_id( array $form ); - abstract protected function get_source_form_name( array $form ): string; - abstract protected function build_form_content( array $form ): string; // concatenated block markup - abstract protected function get_form_metas( array $form ): array; // notifications, confirmations - // NOTE: get_entries() is planned for the entries-migration phase (P3); not shipped yet. - - // Shared (in base): - public function list_forms(): array; - public function count_source_forms(): int; - public function import_forms( array $selected_ids = [], bool $dry_run = false, array $behavior = [], string $post_status = 'publish' ): array; - protected function find_existing_srfm_id( $source_id ): int; - protected function record_import_mapping( int $srfm_id, $source_id ): void; -} -``` - -## 9. Idempotent re-imports - -Same pattern as Fluent: a single `wp_option('srfm_imported_forms_map')` storing: - -```php -[ - 123 => [ 'source_id' => 'cf7-45', 'source_key' => 'cf7', 'last_imported' => '2026-05-22 10:00:00' ], - 124 => [ 'source_id' => '8', 'source_key' => 'wpforms', 'last_imported' => '2026-05-22 10:01:00' ], -] -``` - -`is_already_imported()`: -1. Look up `source_key` + `source_id` in the map. -2. If found and the matching `sureforms_form` post still exists → **update** (not insert). -3. If the SureForms form was deleted → prune the stale entry, fresh insert. - -## 10. Unsupported field tracking - -Same pattern as Fluent: -- `$this->unsupported_fields[]` is pushed each time a source field's type isn't in the source's `field_type_map`, capturing the field's user-facing label (not internal id). -- Returned in the REST response as `unsupported_fields: []`. -- Admin UI renders these as a yellow callout: "These fields couldn't be migrated to SureForms — add them manually after import: Field A, Field B, Field C." - -## 11. Entry migration - -- **Form-by-form, not bulk** — same as Fluent. -- **Hard cap of 1,000 entries per form**, override via the `srfm/entry_migration_max_limit` filter. -- **Idempotent**: import deletes existing SureForms entries for that form first (via the same `resetEntries()` pattern Fluent uses) before inserting. -- **Files**: copy via `WP_Filesystem_Direct::copy()` into `wp-content/uploads/sureforms/migrated/`. - -## 12. Admin UI flow - -Lives at **WordPress → SureForms → Tools → Migrator** (new sub-tab next to existing export/import). - -``` -[ Step 1: Select source ] Detected plugins shown as tiles with logo + install count - ▼ click "Migrate from Contact Form 7" - -[ Step 2: Select forms ] Table of forms in source (name | fields | already-imported badge) - ▼ bulk select + "Preview" - -[ Step 3: Dry-run preview ] Server returns block markup; UI renders in a code pane - + warning callout for unsupported fields - ▼ "Confirm & Import" - -[ Step 4: Result ] "Imported X forms." Links: [Edit in SureForms] [Import entries] - Unsupported-fields list (if any) -``` - -The dry-run preview is the **one improvement over Fluent's UX** — it answers the user's "is this going to break anything?" anxiety before they commit. - -## 13. Implementation phasing - -Four mergeable PRs, each ~7–10 working days: - -| Phase | Deliverable | LOC | -|---|---|---| -| **P1: Foundation** | `Base_Migrator`, `Block_Templates`, REST routes, admin page shell, CF7 importer end-to-end | ~2,400 | -| **P2: WPForms + Gravity** | Two more importers + dry-run preview UI | ~1,700 | -| **P3: Ninja + Caldera** | Final two importers + entry import flow | ~1,300 | -| **P4: Polish** | Field-mapping overrides, entry import UI, docs, E2E tests | ~350 | - -Total: ~5,750 LOC delivered across ~4–5 weeks. - -## 14. Testing approach - -- **Unit tests** (PHPUnit, per `composer test`): per importer, fixture-driven — store sample source-form payloads in `tests/fixtures/migrator/{source}/*.json` and assert the produced block markup string matches a golden snapshot. -- **Integration tests**: a real CF7 / WPForms / Gravity form created via their PHP APIs in a `tests/Integration/Migrator/` test, imported via `Base_Migrator::import_forms()`, then assert a `sureforms_form` post exists with the right block content. -- **Playwright E2E** (per `npm run play:run`): full admin flow — open Migrator, pick CF7, select a form, dry-run, confirm import, verify the new form renders correctly on the front-end. -- **Manual exploratory**: all 5 source plugins are already installed locally, so the test environment is real, not mocked. - -## 15. Verification (when is this done) - -1. All 5 importers exist under `sureforms/inc/migrator/importers/` and pass their PHPUnit fixtures. -2. REST endpoints respond correctly: `GET /sureforms/v1/migrator/sources` lists the 5 sources with `installed: true` for whichever plugins are active. -3. Admin page at SureForms → Tools → Migrator renders the tile-based source picker. -4. Dry-run for a non-trivial CF7 form (text + email + textarea + radio + acceptance) produces valid Gutenberg block markup that renders correctly in the SureForms editor. -5. Re-importing the same source form updates the existing SureForms form (not a duplicate). -6. Entry migration imports up to 1,000 entries for a WPForms Pro form and the entries appear in SureForms' Entries admin. -7. Unsupported-field warnings appear in the UI for source fields without a SureForms target (e.g. CF7 `[file]`, WPForms `rating`). -8. PHPStan level 9 passes (`composer phpstan`) and PHPCS passes (`composer lint`). - -## 16. Reference source files (Fluent Forms — production reference) - -| Fluent file | Our equivalent | Why useful | -|---|---|---| -| `app/Services/Migrator/Bootstrap.php` | `class-migrator-bootstrap.php` | AJAX → REST route mapping | -| `app/Services/Migrator/Classes/BaseMigrator.php` | `class-base-migrator.php` | Import loop, idempotency, file migration helpers | -| `app/Services/Migrator/Classes/ContactForm7Migrator.php` | `class-cf7-importer.php` | Battle-tested shortcode regex parser (`formatAsFluentField()`) | -| `app/Services/Migrator/Classes/WpFormsMigrator.php` | `class-wpforms-importer.php` | JSON-decode + 22-entry field map | -| `app/Services/Migrator/Classes/GravityFormsMigrator.php` | `class-gravity-importer.php` | Serialized-array parsing + Name/Address sub-input handling | -| `app/Services/Migrator/Classes/NinjaFormsMigrator.php` | `class-ninja-importer.php` | JOIN across `nf3_*` tables via Ninja's PHP API | -| `app/Services/Migrator/Classes/CalderaMigrator.php` | `class-caldera-importer.php` | `Caldera_Forms_Forms` static API | - -Fluent Forms is GPL-2.0+ — we can lift regexes and parsing patterns directly with attribution comments. The `defaultFieldConfig()` array structure should be re-written for SureForms blocks, not copied (different output format). - -## 16a. Metadata migration - -Form metadata (email notifications, confirmation messages, compliance flags, integrations) lives **outside** the block-markup pipeline — it's persisted as `sureforms_form` post meta. The migrator handles it via the abstract `Base_Migrator::get_form_metas()` method which returns a `meta_key => meta_value` array passed to `wp_insert_post()` / `wp_update_post()` via `meta_input`. - -### What every importer must produce - -A SureForms-shaped payload. The two keys every importer should populate are: - -| SureForms meta key | Schema (registered in `inc/post-types.php`) | -|---|---| -| `_srfm_email_notification` | Array of objects: `{id, status, is_raw_format, name, email_to, email_reply_to, from_name, from_email, email_cc, email_bcc, subject, email_body}` | -| `_srfm_form_confirmation` | Array of objects: `{id, confirmation_type, page_url, custom_url, message, submission_action, enable_query_params, query_params}` | - -Other meta keys exist (`_srfm_compliance`, `_srfm_conditional_logic`, `_srfm_forms_styling`, `_srfm_integrations_webhooks`) but are not required for a usable imported form — they default to SureForms' built-in `register_post_meta` defaults if not provided. - -### What each source can give us (and what we ship in v1) - -| Source | Notifications | Confirmations | Other meta | Status in v1 | -|---|---|---|---|---| -| **CF7** | `_mail`, `_mail_2` use `[field-name]` shortcodes — incompatible with SureForms smart tags | `_messages` array (success/error text) | None | **Stub only** — admin notification to `{admin_email}` with `{all_data}` body, generic success message. Form is usable; users can refine in Single Form Settings. (Matches Fluent's CF7 approach for the same reason.) | -| **WPForms** | `settings.notifications[]` — fully structured (subject/body/recipient/CC/BCC/replyTo/fromName) | `settings.confirmations[]` — multi-confirmation with conditional logic | Webhooks | **Full migration planned in P2** | -| **Gravity** | Notifications, conditional notifications, routing | Confirmations, scheduling, login-required, entry limits | Advanced validation | **Full migration planned in P2** | -| **Ninja** | `Actions` API — type `email`, `successmessage`, `redirect`, `save` | Same Actions API | — | **Full migration planned in P3** | -| **Caldera** | `mailer` config in form settings | `successMessage` field | — | **Full migration planned in P3** | - -### Why CF7 is special - -CF7's email body uses `[your-name]`, `[your-email]` style shortcodes that reference the form-tag `name` attribute. Translating these to SureForms' `{slug_field_id}` smart-tag system requires either (a) maintaining a per-form name→slug mapping during import, or (b) rewriting the body string with regex substitutions that match the field tokens we emitted. Both approaches add ~400 LOC and break easily on edge cases (multi-line bodies, mixed shortcodes/text, CF7 special mail tags like `[_remote_ip]`). - -Fluent Forms — with 700K+ installs and 5+ years of migrator iteration — landed on the same stub-only conclusion for CF7. v1 matches that decision; if user demand surfaces, CF7 mail-template translation is a separate scoped feature. - -### Code pointers (live) - -- **Abstract method**: `Base_Migrator::get_form_metas()` — `inc/migrator/base-migrator.php` -- **Wire-up**: `Base_Migrator::insert_form_post()` / `update_form_post()` both accept a `$metas` arg and pass it to `wp_insert_post()` via `meta_input` -- **CF7 implementation**: `Cf7_Importer::get_form_metas()` — `inc/migrator/importers/cf7-importer.php` -- **SureForms meta schemas** (the contract we have to match): `inc/post-types.php` lines 911-1010 (notification), 1086-1182 (confirmation) - -### Verification - -After importing a CF7 form via the Migration tab, open the resulting SureForms form's settings: -- **Email Notifications** tab → should show one enabled notification named "Admin Notification Email" with subject "New submission: {form title}" and body `{all_data}`. -- **Form Confirmation** tab → should show a centered "Thank you" success message, "Same page" confirmation type. - -If those panels are empty, the meta payload didn't persist — check `wp_options.srfm_imported_forms_map` and `wp_postmeta` for the form id. - -## 17. Out of scope for v1 - -- Polls / Quizzes (Forminator, Gravity surveys) — no SureForms equivalent. -- Formidable Views — front-end data display is its own feature. -- WS Form — niche, defer. -- Multi-step / page break preservation — wait for native `srfm/page-break` block. -- File-upload, signature, rating, repeater fields — wait for native blocks (flagged as unsupported until then). -- Bulk source-selection ("import everything from everywhere") — one source at a time in v1. - -## 18. Sources verified live (May 2026) - -All wordpress.org plugin pages were fetched live during research: - -- [Contact Form 7](https://wordpress.org/plugins/contact-form-7/) — 10M+ installs, v6.1.6 -- [WPForms Lite](https://wordpress.org/plugins/wpforms-lite/) — 6M+ installs, v1.10.0.5 -- [Gravity Forms docs](https://docs.gravityforms.com/form-fields/) — ~655K live sites, commercial -- [Ninja Forms](https://wordpress.org/plugins/ninja-forms/) — 600K+ installs, v3.14.4 -- Caldera Forms — unmaintained since 2022, ~50K legacy sites - -**Combined migration pool: ~17.3M active sites** — the addressable target for v1. diff --git a/docs/wiki/AI-Form-Builder.md b/docs/wiki/AI-Form-Builder.md deleted file mode 100644 index a35a46a4e..000000000 --- a/docs/wiki/AI-Form-Builder.md +++ /dev/null @@ -1,103 +0,0 @@ -# AI Form Builder - -SureForms includes an AI-powered form generator that creates complete forms from natural language descriptions. - -## Architecture - -``` -inc/ai-form-builder/ -|-- ai-form-builder.php # Main AI builder class -|-- ai-auth.php # Authentication and billing -|-- ai-helper.php # AI utility functions -|-- field-mapping.php # Maps AI response to Gutenberg blocks -``` - -## Authentication Flow - -The `AI_Auth` class handles authentication with the SureForms middleware API: - -1. User initiates authentication from admin settings -2. `GET /initiate-auth` REST endpoint generates an auth URL -3. User is redirected to the billing portal (`SRFM_BILLING_PORTAL`) -4. After authentication, an encrypted access key is returned -5. `POST /handle-access-key` decrypts and stores the key in WordPress options -6. Subsequent AI requests use this key for authorization - -### Constants - -| Constant | URL | Purpose | -|----------|-----|---------| -| `SRFM_AI_MIDDLEWARE` | `https://credits.startertemplates.com/sureforms/` | AI credits API | -| `SRFM_MIDDLEWARE_BASE_URL` | `https://api.sureforms.com/` | API middleware | -| `SRFM_BILLING_PORTAL` | `https://billing.sureforms.com/` | Billing portal | - -## Form Generation Process - -### Step 1: User Input - -The user provides a natural language description of the form they want (e.g., "Create a contact form with name, email, phone, and message fields"). - -### Step 2: AI Generation - -`POST /generate-form` sends the description to the AI middleware: - -``` -User prompt -> REST API -> AI_Form_Builder::generate_ai_form() - -> External AI middleware API call - -> AI generates form structure as JSON - -> Returns field definitions -``` - -### Step 3: Field Mapping - -`POST /map-fields` converts the AI response to SureForms Gutenberg blocks: - -``` -AI JSON response -> Field_Mapping::generate_gutenberg_fields_from_questions() - -> Maps each AI field to a SureForms block type - -> Generates block attributes - -> Creates Gutenberg block markup - -> Returns blocks ready for the editor -``` - -### Field Type Mapping - -The `Field_Mapping` class maps AI-suggested field types to SureForms blocks: - -| AI Field Type | SureForms Block | -|---------------|----------------| -| text / name | `srfm/input` | -| email | `srfm/email` | -| phone / tel | `srfm/phone` | -| textarea / message | `srfm/textarea` | -| number | `srfm/number` | -| url / website | `srfm/url` | -| select / dropdown | `srfm/dropdown` | -| checkbox | `srfm/checkbox` | -| radio / multiple choice | `srfm/multichoice` | -| address | `srfm/address` | - -## REST API Endpoints - -| Method | Endpoint | Handler | Description | -|--------|----------|---------|-------------| -| POST | `/generate-form` | `AI_Form_Builder::generate_ai_form` | Send prompt to AI, get form structure | -| POST | `/map-fields` | `Field_Mapping::generate_gutenberg_fields_from_questions` | Convert AI fields to blocks | -| GET | `/initiate-auth` | `AI_Auth::get_auth_url` | Get billing portal auth URL | -| POST | `/handle-access-key` | `AI_Auth::handle_access_key` | Save decrypted access key | - -All endpoints require `manage_options` capability. - -## AI Helper - -The `AI_Helper` class provides utility functions for: -- API key management -- Credit balance checking -- Request formatting -- Error handling for AI middleware responses - -## Related Pages - -- [REST API Reference](REST-API-Reference) -- All API endpoints -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block types the AI maps to -- [Architecture Overview](Architecture-Overview) -- AI classes in bootstrap flow diff --git a/docs/wiki/Admin-Dashboard.md b/docs/wiki/Admin-Dashboard.md deleted file mode 100644 index 9c16aea85..000000000 --- a/docs/wiki/Admin-Dashboard.md +++ /dev/null @@ -1,121 +0,0 @@ -# Admin Dashboard - -The SureForms admin dashboard is a React single-page application (SPA) integrated into the WordPress admin area. - -## Architecture - -### Entry Point - -The admin React app is bootstrapped in `src/admin/editor-scripts.js`, which serves as the Webpack entry point. It mounts the React app to a DOM container registered by the PHP `Admin` class. - -### Tech Stack - -| Technology | Purpose | -|------------|---------| -| React 18 | UI rendering | -| react-router-dom 6 | Client-side routing | -| @tanstack/react-query 5 | Server state management and data fetching | -| @wordpress/api-fetch | REST API client with nonce handling | -| @wordpress/data | WordPress data store integration | -| @bsf/force-ui | BSF component library (buttons, modals, inputs, etc.) | -| Tailwind CSS 3 | Utility-first styling | -| react-hot-toast | Toast notifications | - -### Admin Pages - -The React app includes the following pages/routes: - -| Route | Component | Description | -|-------|-----------|-------------| -| Dashboard | `src/admin/dashboard/` | Overview with charts and stats | -| Forms | `src/admin/forms/` | Forms listing with search, filter, bulk actions | -| Entries | `src/admin/entries/` | Entry listing and management | -| Settings | `src/admin/settings/` | Global plugin settings | -| Payments | `src/admin/payment/` | Payment transactions listing | -| Single Form Settings | `src/admin/single-form-settings/` | Per-form configuration | - -### Shared Components - -Common admin components are in `src/admin/components/`: - -- `PageHeader` -- Consistent page header with title and actions -- `TemplatePicker` -- Form template selection UI -- Loading skeletons (via `react-loading-skeleton`) -- Modal dialogs and confirmation prompts - -## WordPress Integration - -### Menu Registration - -The PHP `Admin` class (`admin/admin.php`) registers the WordPress admin menu: - -```php -add_menu_page( - 'SureForms', - 'SureForms', - 'manage_options', - 'sureforms_menu', - [ $this, 'render_admin_page' ], - 'dashicons-feedback', - 30 -); -``` - -### Asset Enqueuing - -Admin assets are enqueued only on SureForms admin pages: - -- React app bundle (from `assets/build/`) -- Tailwind CSS styles -- Localized data via `wp_localize_script()` providing: - - REST API URL and nonce - - Current user data - - Plugin settings - - Form data - -## Data Fetching - -### @tanstack/react-query - -Used for server state management with automatic caching, refetching, and invalidation: - -```javascript -const { data, isLoading } = useQuery({ - queryKey: ['entries', filters], - queryFn: () => apiFetch({ path: '/sureforms/v1/entries/list', ... }), -}); -``` - -### @wordpress/api-fetch - -WordPress API client with automatic nonce injection: - -```javascript -import apiFetch from '@wordpress/api-fetch'; - -apiFetch({ path: '/sureforms/v1/form-data' }) - .then(data => { /* handle response */ }); -``` - -## Force UI Component Library - -`@bsf/force-ui` is BSF's internal component library providing: - -- Buttons, inputs, selects, toggles -- Modals and dialogs -- Tables and data display -- Navigation components -- Consistent design system - -Components are imported directly: - -```javascript -import { Button, Modal, Input } from '@bsf/force-ui'; -``` - -## Related Pages - -- [State Management](State-Management) -- WordPress data store -- [Frontend Assets](Frontend-Assets) -- Asset loading pipeline -- [REST API Reference](REST-API-Reference) -- API endpoints used by dashboard -- [Environment Configuration](Environment-Configuration) -- Build setup diff --git a/docs/wiki/Architecture-Overview.md b/docs/wiki/Architecture-Overview.md deleted file mode 100644 index 523f18cc8..000000000 --- a/docs/wiki/Architecture-Overview.md +++ /dev/null @@ -1,294 +0,0 @@ -# Architecture Overview - -This page describes the high-level architecture of the SureForms WordPress plugin (v2.5.1), covering the bootstrap flow, namespace conventions, directory layout, design patterns, and data flow. - -## High-Level Architecture Diagram - -``` -+------------------------------------------------------+ -| WordPress Core | -| (hooks: plugins_loaded, init, rest_api_init, etc.) | -+---------------------------+--------------------------+ - | - +--------v---------+ - | sureforms.php | - | (entry point, | - | constants) | - +--------+---------+ - | - +--------v---------+ - | plugin-loader.php| - | (Plugin_Loader) | - | - autoloader | - | - hook setup | - | - class init | - +--------+---------+ - | - +------------------+------------------+ - | | | - plugins_loaded(99) init admin_init - load_plugin() load_classes() activation_redirect() - | | - +------v------+ +------v------+ - | Core Classes| | Admin/Block | - | Post_Types | | Register | - | Form_Submit | | Admin | - | Rest_Api | | Payments | - | Helper | | Duplicate | - | ...20+ more | +-------------+ - +------+------+ - | - +------v------+ +-------------+ +-------------+ - | Database | | REST API | | Frontend | - | Register | | /sureforms/ | | Assets | - | Entries tbl | | v1/... | | Form Markup | - | Payments tbl| +-------------+ +-------------+ - +-------------+ -``` - -## Plugin Bootstrap Flow - -### 1. Entry Point: sureforms.php - -The main plugin file defines all global constants and then requires `plugin-loader.php`: - -```php -define( 'SRFM_FILE', __FILE__ ); -define( 'SRFM_DIR', plugin_dir_path( SRFM_FILE ) ); -define( 'SRFM_URL', plugins_url( '/', SRFM_FILE ) ); -define( 'SRFM_VER', '2.5.1' ); -define( 'SRFM_FORMS_POST_TYPE', 'sureforms_form' ); -define( 'SRFM_ENTRIES', 'sureforms_entries' ); -define( 'SRFM_PAYMENTS', 'sureforms_payments' ); -require_once 'plugin-loader.php'; -``` - -### 2. Plugin Loader: plugin-loader.php - -The `SRFM\Plugin_Loader` class is the central bootstrap orchestrator: - -1. Loads the Action Scheduler library from `inc/lib/action-scheduler/action-scheduler.php` -2. Registers the PSR-4-style autoloader via `spl_autoload_register()` -3. Hooks into WordPress lifecycle events: - - `plugins_loaded` (default priority) -- `load_textdomain()` - - `plugins_loaded` (priority 99) -- `load_plugin()` (main class initialization) - - `init` -- `load_classes()` (block registration, admin, payments) - - `admin_init` -- `activation_redirect()` -4. Instantiates `Analytics` singleton immediately -5. Registers activation hook (`Activator::activate()`) -6. Registers deactivation hook (clears scheduled events) - -The `Plugin_Loader` fires the `srfm_core_loaded` action after first instantiation. - -### 3. Class Initialization Order - -**On `plugins_loaded` (priority 99) via `load_plugin()`:** - -| Order | Class | Responsibility | -|-------|-------|---------------| -| 1 | `Post_Types` | Registers `sureforms_form` CPT and all post meta | -| 2 | `Form_Submit` | Registers `/submit-form` and `/refresh-nonces` REST endpoints | -| 3 | `Gutenberg_Hooks` | Block editor integration hooks | -| 4 | `Frontend_Assets` | Frontend CSS/JS enqueue | -| 5 | `Helper` | Utility/helper functions | -| 6 | `Activator` | First-run setup and migrations | -| 7 | `Admin_Ajax` | WordPress AJAX handlers | -| 8 | `Forms_Data` | Form data queries and listing | -| 9 | `Export` | Import/export functionality | -| 10 | `Smart_Tags` | Template tag processing | -| 11 | `Generate_Form_Markup` | Form HTML rendering | -| 12 | `Create_New_Form` | New form creation flow with default meta | -| 13 | `Global_Settings` | Plugin-wide settings management | -| 14 | `Email_Summary` | Scheduled email digest reports | -| 15 | `Compliance_Settings` | GDPR/privacy per-form settings | -| 16 | `Events_Scheduler` | Cron-based scheduled events | -| 17 | `AI_Form_Builder` | AI-powered form generation | -| 18 | `Field_Mapping` | AI response to Gutenberg block mapping | -| 19 | `Background_Process` | Background task processing via Action Scheduler | -| 20 | `Page_Builders` | Third-party page builder integrations | -| 21 | `Rest_Api` | Admin REST endpoint registration (20+ routes) | -| 22 | `AI_Helper` | AI utility functions | -| 23 | `AI_Auth` | AI authentication and billing portal | -| 24 | `Updater` | Plugin update logic | -| 25 | `Onboarding` | First-run onboarding wizard | -| 26 | `DatabaseRegister` | Custom table creation/migration | -| 27 | `Form_Restriction` | Submission limits, scheduling, and entry caps | -| 28 | `Astra` | Astra theme compatibility layer | - -**On `init` via `load_classes()`:** - -| Class | Condition | Responsibility | -|-------|-----------|---------------| -| `Blocks\Register` | Always | Gutenberg block type registration | -| `Admin` | `is_admin()` only | Admin menu pages, assets, and screens | -| `Notice_Manager` | `is_admin()` only | Admin notice display and management | -| `Payments` | Always | Payment gateway setup (Stripe) | -| `Duplicate_Form` | Always | Form duplication handler | - -## Namespace and Autoloading - -### Namespace Structure - -The root namespace is `SRFM`. All classes follow `SRFM\{directory}\{ClassName}`: - -``` -SRFM\ -> plugin-loader.php -SRFM\Inc\Helper -> inc/helper.php -SRFM\Inc\Rest_Api -> inc/rest-api.php -SRFM\Inc\Form_Submit -> inc/form-submit.php -SRFM\Inc\Database\Base -> inc/database/base.php -SRFM\Inc\Database\Tables\Entries -> inc/database/tables/entries.php -SRFM\Inc\AI_Form_Builder\* -> inc/ai-form-builder/ -SRFM\Inc\Traits\Get_Instance -> inc/traits/get-instance.php -SRFM\Admin\Admin -> admin/admin.php -``` - -### Autoloader Logic - -The autoloader in `Plugin_Loader::autoload()`: - -1. Strips the root `SRFM\` namespace prefix -2. Converts `CamelCase` to `kebab-case` -3. Replaces underscores with hyphens -4. Replaces namespace separators with directory separators -5. Converts to lowercase, prepends `SRFM_DIR`, appends `.php` -6. Rejects any path containing `..` (directory traversal protection) - -## Directory Layout - -``` -sureforms/ -|-- sureforms.php # Plugin entry point, all SRFM_* constants -|-- plugin-loader.php # Autoloader, bootstrap, hook registration -|-- inc/ # Core PHP classes (SRFM\Inc namespace) -| |-- helper.php # Static utility methods -| |-- rest-api.php # REST API endpoint registration -| |-- form-submit.php # Public form submission handler -| |-- post-types.php # sureforms_form CPT and post meta -| |-- entries.php # Entry CRUD operations -| |-- blocks/ # Gutenberg block server-side rendering -| |-- database/ # Custom DB table management -| |-- fields/ # Form field type definitions and validation -| |-- payments/ # Payment gateway integrations (Stripe) -| |-- email/ # Email template rendering -| |-- ai-form-builder/ # AI-powered form generation -| |-- global-settings/ # Plugin-wide settings -| |-- single-form-settings/ # Per-form configuration -| |-- compatibility/ # Theme/plugin compatibility -| |-- page-builders/ # Page builder integrations -| |-- traits/ # Shared PHP traits (Get_Instance) -| |-- lib/ # Bundled third-party libraries (do not modify) -|-- src/ # JS/React source (compiled by wp-scripts) -| |-- admin/ # Admin dashboard React app -| |-- blocks/ # Gutenberg block editor components -| |-- components/ # Shared React components -| |-- store/ # WordPress @wordpress/data store -| |-- utils/ # JS utilities -|-- modules/ # Feature modules (Gutenberg, Quick Action Sidebar) -|-- admin/ # Admin PHP classes and assets -|-- sass/ # SASS source files (compiled to assets/css/) -|-- assets/ # Compiled and static assets -|-- templates/ # PHP templates (BladeOne engine) -|-- tests/ # PHPUnit, Playwright, and Docker setup -``` - -## Design Patterns - -### Singleton via Get_Instance Trait - -Nearly every class uses the `Get_Instance` trait from `inc/traits/get-instance.php`: - -```php -trait Get_Instance { - private static $instance = null; - public static function get_instance() { - if ( null === self::$instance ) { - self::$instance = new self(); - } - return self::$instance; - } -} -``` - -### Abstract Base Class (Database) - -The database layer uses `SRFM\Inc\Database\Base` providing: - -- Table creation via `dbDelta()` -- Schema versioning with integer versions stored in options -- Column migration (`maybe_add_new_columns()`, `maybe_rename_columns()`) -- CRUD wrappers (`use_insert()`, `use_update()`, `use_delete()`) -- Query builder with parameterized WHERE supporting `=`, `!=`, `>`, `<`, `LIKE`, `IN` -- In-memory result caching keyed by MD5 of SQL query -- Schema-driven JSON encoding/decoding for array columns - -### Hook-Based Initialization - -Classes register WordPress hooks in their constructors, invoked during singleton instantiation in `load_plugin()` or `load_classes()`. - -### Filter-Driven Extensibility - -| Filter | Purpose | -|--------|---------| -| `srfm_rest_api_endpoints` | Extend REST API routes | -| `srfm_register_post_meta` | Register additional form post meta | -| `srfm_form_submit_data` | Modify form data before processing | -| `srfm_entry_value` | Transform entry field values | -| `srfm_is_special_block` | Declare custom blocks as special | -| `srfm_handle_special_block` | Custom processing for special blocks | -| `srfm_form_confirmation_params` | Extend form confirmation settings | -| `srfm_normalize_csv_field_value` | Custom CSV export formatting | - -## Data Flow - -``` -1. FORM CREATION - Admin creates form in Gutenberg block editor - -> Saved as sureforms_form CPT (wp_posts) - -> Settings stored as post meta (_srfm_* keys) - -> Block content in post_content column - -2. FORM DISPLAY - Form embedded via shortcode, Gutenberg block, or Instant Form URL - -> Generate_Form_Markup::get_form_markup() renders HTML - -> Frontend_Assets enqueues CSS/JS - -> Submission nonce generated - -3. FORM SUBMISSION - User submits via JavaScript fetch() - -> POST /wp-json/sureforms/v1/submit-form - -> Nonce verification, field validation - -> CAPTCHA validation (reCAPTCHA, hCaptcha, Turnstile) - -> Honeypot spam check - -4. ENTRY STORAGE - -> JSON-encoded form data inserted into wp_srfm_entries - -> Email notifications sent - -> Payment processed if applicable (wp_srfm_payments) - -> Confirmation response returned - -5. POST-SUBMISSION - -> srfm_form_submit and srfm_after_submission_process actions fired -``` - -## Key Constants - -| Constant | Value | Purpose | -|----------|-------|---------| -| `SRFM_FILE` | `__FILE__` | Main plugin file path | -| `SRFM_DIR` | Plugin dir path | File system operations | -| `SRFM_URL` | Plugin URL | Asset URL generation | -| `SRFM_VER` | `2.5.1` | Version for cache busting | -| `SRFM_SLUG` | `srfm` | Short identifier slug | -| `SRFM_FORMS_POST_TYPE` | `sureforms_form` | Custom post type slug | -| `SRFM_ENTRIES` | `sureforms_entries` | Entries identifier | -| `SRFM_PAYMENTS` | `sureforms_payments` | Payments identifier | -| `SRFM_PRO_RECOMMENDED_VER` | `2.5.1` | Minimum Pro version | - -## Related Pages - -- [Database Schema](Database-Schema) -- Custom tables, post types, and meta keys -- [Environment Configuration](Environment-Configuration) -- Development setup, build tools, and linting -- [REST API Reference](REST-API-Reference) -- Full endpoint documentation -- [Form Submission Flow](Form-Submission-Flow) -- Detailed submission pipeline -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- All actions and filters diff --git a/docs/wiki/Block-Editor-Controls.md b/docs/wiki/Block-Editor-Controls.md deleted file mode 100644 index 7d5cd1e33..000000000 --- a/docs/wiki/Block-Editor-Controls.md +++ /dev/null @@ -1,109 +0,0 @@ -# Block Editor Controls - -SureForms provides custom inspector controls and utilities for the Gutenberg block editor experience. - -## Directory Structure - -``` -src/srfm-controls/ -|-- addCommonData.js # Inject common data into blocks -|-- generateCSS.js # Dynamic CSS generation for blocks -|-- block-icons.js # Block icon definitions -|-- fonts.js # Font management utilities -|-- maybeGetColorForVariable.js # CSS variable color resolution -|-- parseIcon.js # SVG icon parser -|-- renderIcon.js # Icon rendering component - -src/blocks-attributes/ -|-- getMetakeys.js # Form meta key retrieval -|-- getFormId.js # Get current form ID -|-- getBlocksDefaultAttributes.js # Block attribute defaults -``` - -## CSS Generation System - -`generateCSS.js` dynamically generates CSS for block styling: - -- Converts block attributes to CSS properties -- Supports responsive breakpoints (desktop, tablet, mobile) -- Generates inline styles injected into the page -- Handles CSS variable references - -This system allows each block instance to have unique styling without class conflicts. - -## Block Icons - -`block-icons.js` provides SVG icon components for all SureForms blocks, used in: - -- Block inserter panel -- Block toolbar -- Block settings sidebar - -Additional icon sets from Spectra are available via `spectra-icons-v*.php` files. - -Icon utilities: -- `parseIcon.js` -- Parses SVG string into React-renderable format -- `renderIcon.js` -- React component for rendering parsed icons - -## Common Data Injection - -`addCommonData.js` injects shared data into block components: - -- Form-level settings accessible to all child blocks -- Common attributes and defaults -- Shared configuration values - -## Meta Key Integration - -`getMetakeys.js` retrieves form post meta keys for use in block controls: - -- Provides meta key names for form settings -- Used by inspector controls to read/write form meta -- Integrates with `@wordpress/data` store - -## Block Attribute Defaults - -`getBlocksDefaultAttributes.js` provides default attribute values for all block types: - -- Ensures consistent defaults across block instances -- Used during block registration -- Prevents undefined attribute errors - -## Form ID Resolution - -`getFormId.js` resolves the current form's post ID: - -- Used by blocks to identify their parent form -- Required for meta key operations -- Supports nested block contexts - -## Field Preview - -`src/blocks/FieldsPreview.jsx` renders a preview of form fields in the block editor: - -- Shows a visual representation of the field in editor mode -- Responds to attribute changes in real-time -- Handles edit vs preview states - -## Font Management - -`fonts.js` manages font loading for form styling: - -- Font family selection in block settings -- Google Fonts integration -- Font weight and style options - -## Color Variable Resolution - -`maybeGetColorForVariable.js` resolves WordPress CSS custom properties: - -- Maps color slugs to CSS variable references -- Supports theme color palette integration -- Falls back to raw color values when variables are not available - -## Related Pages - -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block types and registration -- [State Management](State-Management) -- WordPress data store -- [Frontend Assets](Frontend-Assets) -- Editor asset loading -- [Form Fields Architecture](Form-Fields-Architecture) -- Field types diff --git a/docs/wiki/Changelog.md b/docs/wiki/Changelog.md deleted file mode 100644 index 058c89f45..000000000 --- a/docs/wiki/Changelog.md +++ /dev/null @@ -1,39 +0,0 @@ -# Changelog - -Recent changes to SureForms. For the full changelog, see [sureforms.com/whats-new](https://sureforms.com/whats-new/). - -## 2.5.1 - 16th February 2026 - -- **New:** Added option to change currency position (e.g., $100, 100$, $ 100, 100 $) -- **Fix:** Fixed the payment block amount not updating correctly with conditional logic - -## 2.5.0 - 2nd February 2026 - -- **Improvement:** Added translation support for the country list in the phone number field -- **Fix:** Fixed dropdown placeholder and GDPR label translation on the frontend - -## 2.4.0 - 20th January 2026 - -- **New:** Added form scheduling restrictions with start and end date/time -- **New:** Added Previous/Next navigation for single entry page -- **Improvement:** Improved multi-choice block settings UI -- **Improvement:** Updated Syria flag in the Phone field -- **Fix:** Fixed HTML block content corruption when duplicating forms -- **Fix:** Fixed multi-choice options being translated in entries -- **Fix:** Fixed reCAPTCHA v3 validation triggering multiple times on the same page -- **Fix:** Fixed Spectra button styles being affected by SureForms CSS - -## Version Bumping - -When preparing a release, update the version in: - -1. `sureforms.php` -- Plugin header `Version:` and `SRFM_VER` constant -2. `readme.txt` -- `Stable tag:` and changelog section -3. `package.json` -- `version` field - -See [Deployment Guide](Deployment-Guide) for the full release process. - -## Related Pages - -- [Deployment Guide](Deployment-Guide) -- Release workflow and CI/CD -- [Contributing Guide](Contributing-Guide) -- Development workflow diff --git a/docs/wiki/Contributing-Guide.md b/docs/wiki/Contributing-Guide.md deleted file mode 100644 index 441736c7e..000000000 --- a/docs/wiki/Contributing-Guide.md +++ /dev/null @@ -1,205 +0,0 @@ -# Contributing Guide - -Thank you for your interest in contributing to SureForms! This guide covers the development workflow, coding standards, and submission process. - -## Prerequisites - -- **PHP** 7.4+ -- **Node.js** 24.16.0 (managed via [Volta](https://volta.sh/)) -- **Composer** for PHP dependencies -- **Git** for version control -- **Docker** (optional, for wp-env test environment) - -## Getting Started - -1. Fork the repository on GitHub -2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/sureforms.git - cd sureforms - ``` -3. Install dependencies: - ```bash - composer install - npm install - ``` -4. Create a feature branch from `next-release`: - ```bash - git checkout next-release - git pull origin next-release - git checkout -b feature/your-feature-name - ``` - -## Git Workflow - -``` -feature/branch --> next-release --> master --> tag/release - (development) (production) -``` - -- **Feature branches** are created from `next-release` -- **Pull requests** target `next-release` -- **Hotfixes** may target `master` directly -- **Release merges** go from `next-release` into `master` - -## Development Workflow - -### Running the Dev Server - -```bash -npm start # Start wp-scripts dev server (hot reload) -``` - -### Building - -```bash -npm run build # Full production build (JS + SASS + minify) -npm run build:script # Build JS only -npm run build:sass # Build SASS only -``` - -### Linting - -Run linters before submitting a PR: - -```bash -# JavaScript -npm run lint-js # ESLint check -npm run lint-js:fix # Auto-fix JS issues - -# CSS -npm run lint-css # Stylelint check -npm run lint-css:fix # Auto-fix CSS issues - -# Formatting -npm run pretty # Prettier check -npm run pretty:fix # Auto-format - -# PHP -vendor/bin/phpcs # PHP CodeSniffer (WPCS) -vendor/bin/phpcbf # Auto-fix PHPCS issues -``` - -### Testing - -All tests should pass before submitting a PR: - -```bash -# PHP unit tests -vendor/bin/phpunit - -# JavaScript unit tests -npm run test:unit - -# E2E tests (requires wp-env) -npm run play:up # Start environment -npm run play:run # Run Playwright tests -``` - -See [Testing Guide](Testing-Guide) for detailed test setup. - -## Coding Standards - -### PHP Standards - -SureForms follows **WordPress Coding Standards (WPCS)** enforced via `phpcs.xml`: - -- **Escaping**: Use `esc_html()`, `esc_attr()`, `esc_url()`, `wp_kses_post()` for all output -- **Nonces**: Use `wp_nonce_field()` / `check_ajax_referer()` for form submissions and AJAX -- **Capabilities**: Always check `current_user_can()` before privileged operations -- **Sanitization**: Use `sanitize_text_field()`, `absint()`, `wp_kses()` for all input -- **Prefixes**: `SRFM_` for constants, `srfm_` for function names and handles -- **Text domain**: `sureforms` for all translatable strings -- **PHP compatibility**: 7.4+ (enforced via `PHPCompatibilityWP`) - -### JavaScript Standards - -- Follow `@wordpress/scripts` ESLint configuration -- Use `@wordpress/data` store for state management -- Use `@wordpress/api-fetch` for REST API calls -- Import WordPress dependencies from `@wordpress/*` packages -- Use Force UI (`@bsf/force-ui`) components in admin dashboard pages - -### CSS Standards - -- **Admin dashboard**: Tailwind CSS 3.x utility classes -- **Block frontend**: SASS files in `sass/` directory -- **Class prefix**: `srfm-` for all frontend CSS classes -- Do not edit generated CSS in `assets/css/` -- edit SASS source files instead - -## Pull Request Process - -### PR Checklist - -Before submitting your PR, verify: - -- [ ] Code follows the coding standards (PHP, JS, CSS) -- [ ] All linters pass without errors -- [ ] Tests pass (PHPUnit, Jest) -- [ ] New features include tests -- [ ] Translatable strings use the `sureforms` text domain -- [ ] No debug code, `console.log`, or `var_dump` statements -- [ ] No secrets or credentials committed -- [ ] Build completes without errors (`npm run build`) - -### PR Title Format - -PR titles are validated by CI. Use a clear, descriptive title that summarizes the change. - -### CI Checks - -Pull requests automatically run: - -1. **PHPUnit** -- PHP unit tests -2. **PHPStan** -- Static analysis -3. **PHP Insights** -- Code quality -4. **PR Title Validation** -- Title format check -5. **E2E Tests** -- When the PR is labeled with `e2e` - -All checks must pass before merge. - -### Code Review - -- PRs require at least one approving review -- Address all review comments -- Keep PRs focused on a single feature or fix -- Large changes should be discussed in an issue first - -## Directory Guide for Contributors - -| Directory | Purpose | Edit? | -|-----------|---------|-------| -| `src/` | JS/React source code | Yes | -| `sass/` | SASS stylesheets | Yes | -| `inc/` | PHP classes and logic | Yes | -| `tests/` | Test files | Yes | -| `build/` | Compiled JS output | No (generated) | -| `assets/css/` | Compiled CSS output | No (generated) | -| `assets/js/minified/` | Minified JS | No (generated) | -| `inc/lib/` | Bundled third-party libraries | No | -| `node_modules/` | NPM packages | No | -| `vendor/` | Composer packages | No | - -## Adding a New Gutenberg Block - -See [Gutenberg Blocks](Gutenberg-Blocks) for the full guide on adding blocks, including: - -- Block registration (server-side and client-side) -- `block.json` schema -- Editor and frontend rendering - -## Adding a New REST Endpoint - -See [REST API Reference](REST-API-Reference) for endpoint patterns and the registration system. - -## Reporting Issues - -- Use [GitHub Issues](https://github.com/brainstormforce/sureforms/issues) for bug reports and feature requests -- Security vulnerabilities should be reported through the [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/) - -## Related Pages - -- [Environment Configuration](Environment-Configuration) -- Development setup details -- [Architecture Overview](Architecture-Overview) -- Codebase structure -- [Testing Guide](Testing-Guide) -- Test framework details -- [Deployment Guide](Deployment-Guide) -- Release process diff --git a/docs/wiki/Data-Export-Import.md b/docs/wiki/Data-Export-Import.md deleted file mode 100644 index fc07271ae..000000000 --- a/docs/wiki/Data-Export-Import.md +++ /dev/null @@ -1,120 +0,0 @@ -# Data Export & Import - -SureForms supports exporting and importing forms and entries for backup, migration, and data analysis. - -## Form Export - -### REST Endpoint - -`POST /wp-json/sureforms/v1/forms/export` - -**Parameters:** -- `form_ids` (required) -- Array of form IDs to export - -**Response:** JSON file containing form data including: -- Form post data (title, content, status) -- All form post meta (`_srfm_*` keys) -- Block content (Gutenberg blocks) - -### Export Format - -Forms are exported as a JSON structure containing all data needed to recreate the form on another site. - -## Form Import - -### REST Endpoint - -`POST /wp-json/sureforms/v1/forms/import` - -**Parameters:** -- File upload containing exported JSON data - -**Process:** -1. Validates the uploaded JSON structure -2. Creates new `sureforms_form` posts -3. Restores all post meta keys -4. Assigns new post IDs (no ID conflicts) -5. Returns count of imported forms - -## Form Duplication - -### REST Endpoint - -`POST /wp-json/sureforms/v1/forms/duplicate` - -**Parameters:** -- `form_id` (required) -- ID of the form to duplicate - -**Process:** - -Handled by `Duplicate_Form` class (`inc/duplicate-form.php`): - -1. Reads the original form post and all meta -2. Creates a new post with "(Copy)" appended to the title -3. Copies all `_srfm_*` post meta to the new form -4. Returns the new form ID - -## Entry Export - -### REST Endpoint - -`POST /wp-json/sureforms/v1/entries/export` - -**Parameters:** -- `entry_ids` -- Specific entry IDs to export (optional) -- `form_id` -- Export all entries for a specific form -- `status` -- Filter by status (all, read, unread) -- `search` -- Search filter -- `date_from` / `date_to` -- Date range filter - -**Response:** CSV file (or ZIP for large exports) - -### Export Format - -The `Export` class (`inc/export.php`) generates CSV with: -- One row per entry -- Columns for each form field -- Submission metadata (date, status, IP) -- Field values normalized via `srfm_normalize_csv_field_value` filter - -### CSV Field Value Normalization - -The `srfm_normalize_csv_field_value` filter allows customizing how field values appear in CSV exports: - -```php -add_filter( 'srfm_normalize_csv_field_value', function( $value, $field ) { - // Custom formatting for specific field types - return $value; -}, 10, 2 ); -``` - -## Form Lifecycle Management - -### REST Endpoint - -`POST /wp-json/sureforms/v1/forms/manage` - -**Parameters:** -- `form_ids` (required) -- Array of form IDs -- `action` (required) -- `trash`, `restore`, or `delete` - -| Action | Behavior | -|--------|----------| -| `trash` | Move forms to trash (recoverable) | -| `restore` | Restore trashed forms | -| `delete` | Permanently delete forms | - -## Create New Form - -The `Create_New_Form` class (`inc/create-new-form.php`) handles new form creation with default meta values: - -- Sets default styling options -- Configures default submit button text -- Initializes email notification defaults -- Applies default form confirmation settings - -## Related Pages - -- [REST API Reference](REST-API-Reference) -- All export/import endpoints -- [Database Schema](Database-Schema) -- Entry and form data structure -- [Admin Dashboard](Admin-Dashboard) -- UI for export/import operations diff --git a/docs/wiki/Database-Schema.md b/docs/wiki/Database-Schema.md deleted file mode 100644 index c0a47044b..000000000 --- a/docs/wiki/Database-Schema.md +++ /dev/null @@ -1,173 +0,0 @@ -# Database Schema - -SureForms uses two custom database tables alongside the WordPress `sureforms_form` custom post type. All custom tables use the `wp_srfm_` prefix. - -## Table Relationships - -``` -+-------------------+ +-------------------+ -| wp_posts | | wp_srfm_entries | -| (sureforms_form) |<---------| form_id | -| ID (PK) | | | ID (PK) | -| post_content | | | user_id | -| post_meta (_srfm)| | | form_data (JSON) | -+-------------------+ | | status | - | +--------+-----------+ - | | - | +--------v-----------+ - | | wp_srfm_payments | - +----| form_id | - | id (PK) | - | entry_id (FK) | - | transaction_id | - | total_amount | - +--------------------+ -``` - -## Custom Tables - -### wp_srfm_entries - -Stores all form submissions. Table version: 1. - -| Column | Type | Default | Description | -|--------|------|---------|-------------| -| `ID` | BIGINT(20) UNSIGNED | AUTO_INCREMENT | Primary key | -| `form_id` | BIGINT(20) UNSIGNED | -- | Reference to sureforms_form post ID | -| `user_id` | BIGINT(20) UNSIGNED | 0 | WordPress user ID (0 = anonymous) | -| `status` | VARCHAR(10) | `unread` | Entry status: `unread`, `read`, `trash` | -| `type` | VARCHAR(20) | -- | Form type: `quiz`, `standard`, etc. | -| `form_data` | LONGTEXT | `[]` | JSON-encoded submitted field values | -| `submission_info` | LONGTEXT | `[]` | JSON-encoded browser/device/IP metadata | -| `notes` | LONGTEXT | `[]` | JSON-encoded admin notes | -| `logs` | LONGTEXT | `[]` | JSON-encoded activity log entries | -| `extras` | LONGTEXT | `[]` | JSON-encoded misc additional data | -| `created_at` | TIMESTAMP | CURRENT_TIMESTAMP | Submission timestamp | -| `updated_at` | TIMESTAMP | CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | - -**Indexes:** -- `PRIMARY KEY (ID)` -- `INDEX idx_form_id (form_id)` -- `INDEX idx_user_id (user_id)` -- `INDEX idx_form_id_created_at_status (form_id, created_at, status)` -- composite index for list queries - -**Log Entry Structure:** -```json -{ - "title": "Form Submitted", - "messages": ["Email notification sent", "Entry stored"], - "timestamp": "2024-01-15 10:30:00" -} -``` - -### wp_srfm_payments - -Stores payment transaction records linked to form entries. Table version: 1. Added in v2.0.0. - -| Column | Type | Default | Description | -|--------|------|---------|-------------| -| `id` | BIGINT(20) UNSIGNED | AUTO_INCREMENT | Primary key | -| `form_id` | BIGINT(20) UNSIGNED | -- | Reference to sureforms_form post | -| `block_id` | VARCHAR(255) | `''` | Payment block identifier | -| `status` | VARCHAR(50) | `pending` | Payment status (see below) | -| `total_amount` | DECIMAL(26,8) | `0.00000000` | Total amount after discount | -| `refunded_amount` | DECIMAL(26,8) | `0.00000000` | Total refunded amount | -| `currency` | VARCHAR(10) | `''` | ISO 4217 currency code | -| `entry_id` | BIGINT(20) UNSIGNED | 0 | Reference to entries table | -| `gateway` | VARCHAR(20) | `''` | Payment gateway: `stripe` | -| `type` | VARCHAR(30) | `''` | Payment type: `payment`, `subscription`, `renewal` | -| `mode` | VARCHAR(20) | `''` | Mode: `test` or `live` | -| `transaction_id` | VARCHAR(50) | `''` | Gateway transaction ID | -| `customer_id` | VARCHAR(50) | `''` | Gateway customer ID | -| `subscription_id` | VARCHAR(50) | `''` | Subscription ID (if recurring) | -| `subscription_status` | VARCHAR(20) | `''` | Subscription status | -| `parent_subscription_id` | BIGINT(20) UNSIGNED | 0 | Parent subscription payment ID | -| `payment_data` | LONGTEXT | `[]` | JSON-encoded payment details (includes refunds) | -| `extra` | LONGTEXT | `[]` | JSON-encoded additional data | -| `log` | LONGTEXT | `[]` | JSON-encoded payment log | -| `created_at` | TIMESTAMP | CURRENT_TIMESTAMP | Creation timestamp | -| `updated_at` | TIMESTAMP | CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| `srfm_txn_id` | VARCHAR(100) | `''` | Custom transaction ID format | -| `customer_email` | VARCHAR(255) | `''` | Customer email | -| `customer_name` | VARCHAR(255) | `''` | Customer name | - -**Valid Payment Statuses:** -`pending`, `succeeded`, `failed`, `canceled`, `requires_action`, `requires_payment_method`, `processing`, `refunded`, `partially_refunded` - -**Valid Subscription Statuses:** -`active`, `canceled`, `past_due`, `unpaid`, `trialing`, `incomplete`, `incomplete_expired`, `paused` - -**Supported Currencies:** -USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, SEK, NZD, MXN, SGD, HKD, NOK, TRY, RUB, INR, BRL, ZAR, KRW - -## WordPress Post Type: sureforms_form - -Forms are stored as a custom post type (`sureforms_form`) in the standard `wp_posts` table. Block content is in `post_content`, and form configuration is stored as post meta with `_srfm_` prefixed keys. - -### Key Post Meta Keys - -| Meta Key | Type | Description | -|----------|------|-------------| -| `_srfm_forms_styling` | string | Form styling mode | -| `_srfm_color` | string | Primary form color | -| `_srfm_bg_color` | string | Background color | -| `_srfm_fontsize` | string | Font size | -| `_srfm_label_color` | string | Label text color | -| `_srfm_input_color` | string | Input text color | -| `_srfm_submit_button_text` | string | Submit button text | -| `_srfm_submit_alignment` | string | Button alignment | -| `_srfm_submit_width` | string | Button width | -| `_srfm_submit_width_backend` | string | Backend button width | -| `_srfm_is_inline_button` | boolean | Inline button mode | -| `_srfm_email_notification` | array | Email notification settings | -| `_srfm_form_confirmation` | array | Confirmation settings (message/redirect) | -| `_srfm_captcha` | string | CAPTCHA type (recaptcha_v2, v3, hcaptcha, turnstile) | -| `_srfm_honeypot` | boolean | Honeypot spam protection | -| `_srfm_is_page_break` | boolean | Multi-step form enabled | -| `_srfm_single_page_form_title` | boolean | Show title on instant form | -| `_srfm_instant_form_settings` | array | Instant form configuration | -| `_srfm_page_form_logo` | string | Form logo URL | -| `_srfm_submit_type` | string | Submission type | - -Post meta is extensible via the `srfm_register_post_meta` filter. - -## Database Base Class - -All custom tables extend `SRFM\Inc\Database\Base`, which provides: - -### Schema Versioning -- Each table has a `$table_version` integer -- Versions stored in `srfm_database_table_versions` option -- On version mismatch, `start_db_upgrade()` runs migrations (new columns, renames) - -### Query Builder -```php -// Simple equality -$results = $instance->get_results(['form_id' => 123]); - -// Complex conditions with operators -$results = $instance->get_results([ - [ - ['key' => 'status', 'compare' => '!=', 'value' => 'trash'], - ['key' => 'created_at', 'compare' => '>', 'value' => '2024-01-01'], - ] -]); - -// IN operator -$results = $instance->get_results([ - [['key' => 'ID', 'compare' => 'IN', 'value' => [1, 2, 3]]] -]); -``` - -### Caching -Results are cached in-memory using MD5 of the SQL query as the cache key. Cache is reset on insert, update, or delete operations. - -### Data Encoding -Array-type columns (defined in `get_schema()`) are automatically JSON-encoded on write and decoded on read. Number types are cast to integers. - -## Related Pages - -- [Architecture Overview](Architecture-Overview) -- Plugin bootstrap and class initialization -- [Payment Integration](Payment-Integration) -- Stripe payment flow -- [Form Submission Flow](Form-Submission-Flow) -- Entry creation pipeline -- [REST API Reference](REST-API-Reference) -- API endpoints for entries and payments diff --git a/docs/wiki/Deployment-Guide.md b/docs/wiki/Deployment-Guide.md deleted file mode 100644 index 1511856ec..000000000 --- a/docs/wiki/Deployment-Guide.md +++ /dev/null @@ -1,109 +0,0 @@ -# Deployment Guide - -## Release Process Overview - -SureForms follows a branch-based release workflow with automated CI/CD via GitHub Actions. - -## Git Workflow - -``` -feature/branch --> next-release --> master --> tag/release - (development) (production) -``` - -1. Feature branches are created from `next-release` -2. PRs target `next-release` for review and merge -3. `next-release` is merged to `master` for releases -4. Tags trigger release artifacts - -## Build Steps - -### Production Build - -```bash -# Full production build (JS + SASS + minify) -npm run build - -# Individual steps: -npm run build:script # Build JS via wp-scripts -npm run build:sass # Compile SASS via Grunt -``` - -The build process: -1. Webpack bundles JS/React source from `src/` to `assets/build/` -2. Grunt compiles SASS from `sass/` to `assets/css/` -3. Grunt minifies CSS to `assets/css/minified/` -4. Grunt minifies JS to `assets/js/minified/` -5. PostCSS applies Autoprefixer - -### i18n - -```bash -npm run makepot # Generate .pot translation file -``` - -## CI/CD Pipelines - -### GitHub Actions Workflows - -| Workflow | Trigger | Purpose | -|----------|---------|---------| -| `phpunit.yml` | Pull requests | Run PHP unit tests | -| `playwright.yml` | PR labeled `e2e` | Run E2E browser tests | -| `code-analysis.yml` | Pull requests | PHPStan static analysis | -| `insights.yml` | Pull requests | PHP Insights code quality | -| `pr-title.yml` | Pull requests | Validate PR title format | -| `push-to-deploy.yml` | Push to master | Deploy to production | -| `push-asset-readme-update.yml` | Push to master | Update readme/assets | -| `release-tag-draft.yml` | Tag push | Create draft GitHub release | -| `release-pr-template.yml` | Release PRs | Apply release PR template | -| `claude-code-review.yml` | Pull requests | AI-powered code review | - -### Test Pipeline - -Pull requests automatically run: -1. PHPUnit tests (PHP 8.2, WordPress trunk) -2. PHPStan static analysis -3. PHP Insights code quality -4. PR title validation -5. E2E tests (when labeled `e2e`) - -### Deployment Pipeline - -On push to `master`: -1. `push-to-deploy.yml` triggers deployment -2. `push-asset-readme-update.yml` updates WordPress.org assets - -### Release Pipeline - -1. Merge `next-release` into `master` -2. Create a version tag (e.g., `v2.5.1`) -3. `release-tag-draft.yml` creates a draft GitHub release -4. Review and publish the release - -## Pre-Deployment Checklist - -- [ ] All tests pass (PHPUnit, Playwright, Jest) -- [ ] PHPStan analysis clean -- [ ] PHPCS standards pass -- [ ] Production build completes without errors -- [ ] Version bumped in `sureforms.php` and `readme.txt` -- [ ] `SRFM_VER` constant updated -- [ ] `SRFM_PRO_RECOMMENDED_VER` checked -- [ ] Changelog updated in `readme.txt` -- [ ] Translation .pot file regenerated -- [ ] No debug code or console.log statements -- [ ] Database migrations tested (if any) - -## Version Bumping - -Update version in these files: -1. `sureforms.php` -- Plugin header `Version:` and `SRFM_VER` constant -2. `readme.txt` -- `Stable tag:` and changelog section -3. `package.json` -- `version` field - -## Related Pages - -- [Testing Guide](Testing-Guide) -- Test setup and execution -- [Contributing Guide](Contributing-Guide) -- Development workflow -- [Environment Configuration](Environment-Configuration) -- Build tools diff --git a/docs/wiki/Email-Notifications.md b/docs/wiki/Email-Notifications.md deleted file mode 100644 index 642751bcb..000000000 --- a/docs/wiki/Email-Notifications.md +++ /dev/null @@ -1,86 +0,0 @@ -# Email Notifications - -SureForms sends email notifications on form submission and supports scheduled email summary digests. - -## Email System Overview - -``` -Form Submission - -> Form_Submit::handle_form_submission() - -> Email_Template (inc/email/email-template.php) - -> Smart_Tags resolution - -> wp_mail() dispatch -``` - -## Email Template - -The `Email_Template` class (`inc/email/email-template.php`) handles: - -- HTML email template rendering -- Dynamic content insertion via smart tags -- Multiple notification recipients -- Customizable subject lines and body content -- Reply-to address configuration - -### Per-Form Email Settings - -Email notifications are configured per-form via the `_srfm_email_notification` post meta key. Each form can have: - -- **To** -- Recipient email addresses (supports multiple, comma-separated) -- **Subject** -- Email subject line (supports smart tags) -- **Body** -- Email content (supports smart tags and HTML) -- **Reply-To** -- Reply-to email address -- **CC/BCC** -- Additional recipients - -## Smart Tags - -The `Smart_Tags` class (`inc/smart-tags.php`) provides template variables that are resolved at email send time: - -| Smart Tag | Description | -|-----------|-------------| -| `{admin_email}` | WordPress admin email | -| `{site_title}` | WordPress site title | -| `{site_url}` | WordPress site URL | -| `{form_title}` | Name of the submitted form | -| `{form_id}` | ID of the submitted form | -| `{entry_id}` | ID of the created entry | -| `{all_fields}` | All submitted field values formatted | -| `{field:field_name}` | Specific field value by field name | -| `{user_ip}` | Submitter's IP address | -| `{user_agent}` | Submitter's browser user agent | -| `{submission_date}` | Date of submission | - -Smart tags are processed by replacing the tag placeholders with actual values from the form submission data. - -## Email Summary - -The `Email_Summary` class (`inc/global-settings/email-summary.php`) provides scheduled digest reports: - -- Configurable frequency (daily, weekly, monthly) -- Summary of form submissions over the period -- Entry counts per form -- Sent to configured admin email addresses -- Uses WordPress cron (`Events_Scheduler`) for scheduling - -## Customization - -### Modifying Email Content - -Email content is filtered through smart tag processing, allowing dynamic content: - -```php -// In per-form settings, the email body supports: -// - Smart tags: {form_title}, {all_fields}, etc. -// - HTML formatting -// - Custom text -``` - -### Extending Smart Tags - -Smart tags can be extended by hooking into the tag resolution process to add custom replacements. - -## Related Pages - -- [Form Submission Flow](Form-Submission-Flow) -- When emails are triggered -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- Email-related hooks -- [Architecture Overview](Architecture-Overview) -- Email_Template initialization diff --git a/docs/wiki/Environment-Configuration.md b/docs/wiki/Environment-Configuration.md deleted file mode 100644 index b4a89da2f..000000000 --- a/docs/wiki/Environment-Configuration.md +++ /dev/null @@ -1,197 +0,0 @@ -# Environment Configuration - -## Prerequisites - -| Requirement | Version | Notes | -|-------------|---------|-------| -| PHP | 7.4+ | WordPress minimum requirement | -| WordPress | 6.4+ | Gutenberg block editor required | -| Node.js | 24.16.0 | Managed via Volta (see `package.json`) | -| Composer | 2.x | PHP dependency management | -| npm | 11.x+ | Comes with Node 24 | - -## Plugin Constants - -Defined in `sureforms.php`: - -| Constant | Value | Purpose | -|----------|-------|---------| -| `SRFM_FILE` | `__FILE__` | Absolute path to main plugin file | -| `SRFM_BASENAME` | `sureforms/sureforms.php` | Plugin basename for hooks | -| `SRFM_DIR` | Plugin directory path | File system path operations | -| `SRFM_URL` | Plugin URL | Asset URL generation | -| `SRFM_VER` | `2.5.1` | Version for cache busting | -| `SRFM_SLUG` | `srfm` | Short identifier slug | -| `SRFM_FORMS_POST_TYPE` | `sureforms_form` | Custom post type slug | -| `SRFM_ENTRIES` | `sureforms_entries` | Entries identifier | -| `SRFM_PAYMENTS` | `sureforms_payments` | Payments identifier | -| `SRFM_WEBSITE` | `https://sureforms.com/` | Official website | -| `SRFM_AI_MIDDLEWARE` | `https://credits.startertemplates.com/sureforms/` | AI credits API | -| `SRFM_MIDDLEWARE_BASE_URL` | `https://api.sureforms.com/` | API middleware | -| `SRFM_BILLING_PORTAL` | `https://billing.sureforms.com/` | Billing portal | -| `SRFM_PRO_RECOMMENDED_VER` | `2.5.1` | Minimum recommended Pro version | -| `SRFM_SURETRIGGERS_INTEGRATION_BASE_URL` | `https://app.ottokit.com/` | OttoKit integration | - -## Development Environment Setup - -### Option 1: wp-env (Docker) - -```bash -# Start the WordPress environment -npm run play:up - -# Stop the environment -npm run play:stop - -# Clean environment data -npm run env:clean -``` - -Configuration is in `.wp-env.json`. Requires Docker to be installed. - -### Option 2: Local by Flywheel / MAMP / XAMPP - -1. Set up a local WordPress installation -2. Clone the repo into `wp-content/plugins/sureforms` -3. Run `composer install` for PHP dependencies -4. Run `npm install` for JS dependencies -5. Run `npm start` for development with hot reload - -## Build Tools - -### wp-scripts (JavaScript) - -Primary build tool for JS/React compilation via Webpack: - -```bash -npm start # Dev server with hot reload -npm run build:script # Production JS build -``` - -Entry points are defined in `webpack.config.js` and output to `assets/build/`. - -### Grunt (SASS / Minification) - -```bash -npm run build:sass # Compile SASS to CSS -npm run build # Full build (JS + SASS + minify) -``` - -The `Gruntfile.js` handles: -- SASS compilation (`sass/` -> `assets/css/`) -- CSS minification -- JS minification -- PostCSS with Autoprefixer - -### Tailwind CSS - -Used for admin dashboard styles. Configuration in `tailwind.config.js`: -- Content scanning paths: `src/admin/**/*.{js,jsx}` -- Plugin: `@tailwindcss/forms` -- Custom theme extensions for Force UI integration - -### PostCSS - -Configuration in `postcss.config.js`: -- Autoprefixer for browser compatibility -- cssnano for production minification - -## NPM Scripts Reference - -| Script | Command | Description | -|--------|---------|-------------| -| `start` | `wp-scripts start` | Dev server with hot reload | -| `build` | Full pipeline | JS + SASS + minify | -| `build:script` | `wp-scripts build` | Build JS only | -| `build:sass` | Grunt sass tasks | Build SASS only | -| `lint-js` | `wp-scripts lint-js` | ESLint check | -| `lint-js:fix` | `wp-scripts lint-js --fix` | Auto-fix JS lint | -| `lint-css` | `wp-scripts lint-style` | Stylelint check | -| `lint-css:fix` | `wp-scripts lint-style --fix` | Auto-fix CSS lint | -| `pretty` | `prettier --check` | Prettier check | -| `pretty:fix` | `prettier --write` | Prettier format | -| `test:unit` | `wp-scripts test-unit-js` | Jest unit tests | -| `play:up` | `wp-env start` | Start Docker env | -| `play:stop` | `wp-env stop` | Stop Docker env | -| `play:run` | `playwright test` | Run E2E tests | -| `play:run:interactive` | `playwright test --headed` | E2E tests (headed) | -| `env:clean` | `wp-env clean` | Clean Docker data | -| `makepot` | `wp i18n make-pot` | Generate .pot file | - -## Composer Scripts - -| Script | Description | -|--------|-------------| -| `vendor/bin/phpunit` | Run PHPUnit tests | -| `vendor/bin/phpcs` | PHP CodeSniffer (WPCS) | -| `vendor/bin/phpcbf` | Auto-fix PHPCS issues | -| `vendor/bin/phpstan` | PHPStan static analysis | - -## Linting Configuration - -### PHP (PHPCS) - -Configured via `phpcs.xml`: -- Ruleset: WordPress Coding Standards (WPCS) -- PHPCompatibility for PHP 7.4+ -- Text domain: `sureforms` -- Prefixes: `srfm`, `SRFM_` - -### JavaScript (ESLint) - -Uses `@wordpress/scripts` ESLint configuration which extends WordPress defaults. - -### CSS (Stylelint) - -WordPress Stylelint configuration via `@wordpress/scripts`. - -### Prettier - -Code formatting for JS/CSS files. - -## Volta Configuration - -Node version is pinned in `package.json`: - -```json -{ - "volta": { - "node": "24.16.0" - } -} -``` - -Install Volta to automatically use the correct Node version when entering the project directory. - -## Key Dependencies - -### PHP (Composer) - -| Package | Purpose | -|---------|---------| -| `yoast/phpunit-polyfills` | PHPUnit compatibility | -| `squizlabs/php_codesniffer` | Code style checking | -| `wp-coding-standards/wpcs` | WordPress standards | -| `phpcompatibility/php-compatibility` | PHP version checks | -| `phpstan/phpstan` | Static type analysis | -| `nunomaduro/phpinsights` | Code quality insights | - -### JavaScript (npm) - -| Package | Purpose | -|---------|---------| -| `@bsf/force-ui` | BSF internal UI component library | -| `@wordpress/scripts` | Build tooling | -| `@wordpress/api-fetch` | REST API client | -| `@tanstack/react-query` | Data fetching/caching | -| `react-router-dom` | Client-side routing | -| `tailwindcss` | Utility-first CSS | -| `@playwright/test` | E2E testing | -| `@wordpress/env` | Docker test environment | - -## Related Pages - -- [Architecture Overview](Architecture-Overview) -- Plugin structure and bootstrap -- [Testing Guide](Testing-Guide) -- Test setup and execution -- [Contributing Guide](Contributing-Guide) -- Coding standards and workflow -- [Frontend Assets](Frontend-Assets) -- Asset pipeline details diff --git a/docs/wiki/Form-Fields-Architecture.md b/docs/wiki/Form-Fields-Architecture.md deleted file mode 100644 index 285f4793c..000000000 --- a/docs/wiki/Form-Fields-Architecture.md +++ /dev/null @@ -1,137 +0,0 @@ -# Form Fields Architecture - -SureForms uses a layered architecture for form fields: Gutenberg blocks define the editor experience, field markup classes generate the frontend HTML, and the validation system ensures data integrity on submission. - -## Field Type Hierarchy - -``` -Fields\Base (inc/fields/base.php) - |-- Input_Markup (text, hidden) - |-- Email_Markup (email with validation) - |-- Textarea_Markup (multi-line text) - |-- Number_Markup (numeric with min/max) - |-- Phone_Markup (with country selector) - |-- URL_Markup (URL with validation) - |-- Dropdown_Markup (select options) - |-- Checkbox_Markup (single/multiple) - |-- Multichoice_Markup (radio/checkbox groups) - |-- Address_Markup (composite address field) - |-- GDPR_Markup (consent checkbox) - |-- Payment_Markup (Stripe payment field) - |-- Inlinebutton_Markup (inline buttons) -``` - -## Base Field Class - -`inc/fields/base.php` provides the foundation: - -- Common HTML wrapper generation -- Label rendering with required indicator -- Help text / description output -- Error message container -- CSS class assembly with `srfm-` prefix -- Conditional logic attribute output -- Accessibility attributes (aria-label, aria-required) - -## Markup Generation Pattern - -Each field markup class follows a consistent pattern: - -1. Receive block attributes from the Gutenberg block -2. Process and sanitize attribute values -3. Generate field-specific HTML using WordPress escaping functions -4. Wrap in the standard field container with label and error placeholder -5. Return the complete HTML string - -## Available Field Types - -### Input (input-markup.php) -Standard text input supporting `text`, `hidden`, and other HTML5 input types. Supports placeholder, default value, min/max length. - -### Email (email-markup.php) -Email input with built-in email format validation. Supports confirmation email field option. - -### Textarea (textarea-markup.php) -Multi-line text area with configurable rows and max length. - -### Number (number-markup.php) -Numeric input with min/max value constraints, step size, and format validation. - -### Phone (phone-markup.php) -Phone number input integrated with `intl-tel-input` library for country code selection. Uses `countries.json` for country data. - -### URL (url-markup.php) -URL input with format validation. - -### Dropdown (dropdown-markup.php) -Select dropdown with configurable options. Supports default selection and placeholder text. - -### Checkbox (checkbox-markup.php) -Single or multiple checkbox inputs with label positioning options. - -### Multi Choice (multichoice-markup.php) -Radio button or checkbox groups for multiple choice questions. Supports image choices and custom layouts. - -### Address (address-markup.php) -Composite field with subfields: street address, city, state/province, zip/postal code, and country. Each subfield is independently configurable. - -### GDPR (gdpr-markup.php) -GDPR consent checkbox with customizable compliance text. Integrates with form-level compliance settings. - -### Payment (payment-markup.php) -Payment collection field for Stripe integration. Renders the payment form elements and connects to the payment processing flow. - -## Validation System - -### Server-Side Validation (field-validation.php) - -`inc/field-validation.php` handles validation during form submission: - -- **Required field check** -- Ensures required fields have values -- **Type validation** -- Validates data types (email format, URL format, number ranges) -- **Unique value check** -- Optional unique validation against existing entries -- **Custom validation** -- Extensible via filters - -Common error messages are defined in `Helper::get_common_err_msg()`: -- `required`: "This field is required." -- `unique`: "Value needs to be unique." - -### Client-Side Validation - -Frontend JavaScript provides instant validation feedback before submission, matching server-side rules. - -## Field Rendering Pipeline - -``` -1. Block Editor (Gutenberg) - -> Block attributes saved in post_content - -2. Frontend Request - -> Generate_Form_Markup::get_form_markup() - -> Parses post_content blocks - -> For each block, calls its render callback - -3. Block Render Callback - -> Block class (inc/blocks/{type}/block.php) - -> Instantiates field markup class - -> Passes block attributes - -4. Field Markup Generation - -> Field markup class (inc/fields/{type}-markup.php) - -> Base class wraps with container, label, error placeholder - -> Field-specific HTML generated - -> Returns escaped HTML string - -5. Form Assembly - -> All field HTML concatenated inside
wrapper - -> Submit button appended - -> Nonce field added - -> Final form HTML returned -``` - -## Related Pages - -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block registration and editor components -- [Form Submission Flow](Form-Submission-Flow) -- How field data is validated and processed -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- Field-related filters -- [Payment Integration](Payment-Integration) -- Payment field details diff --git a/docs/wiki/Form-Submission-Flow.md b/docs/wiki/Form-Submission-Flow.md deleted file mode 100644 index 81a9e3806..000000000 --- a/docs/wiki/Form-Submission-Flow.md +++ /dev/null @@ -1,191 +0,0 @@ -# Form Submission Flow - -This page documents the end-to-end form submission process from frontend to entry storage. - -## Flow Diagram - -``` -+------------------+ +-------------------+ +------------------+ -| User fills form |---->| JavaScript fetch() |---->| REST API | -| in browser | | POST /submit-form | | Form_Submit | -+------------------+ +-------------------+ +--------+---------+ - | - +--------------------------------+ - | - +----------v-----------+ - | Permission Check | - | - X-WP-Submit-Nonce | - | - Form data exists | - +----------+-----------+ - | - +----------v-----------+ - | Form Restrictions | - | - Max entries check | - | - Date scheduling | - | - Login required | - +----------+-----------+ - | - +----------v-----------+ - | Field Validation | - | - Required fields | - | - Type validation | - | - Unique values | - +----------+-----------+ - | - +----------v-----------+ - | Security Checks | - | - CAPTCHA verify | - | - Honeypot check | - +----------+-----------+ - | - +----------v-----------+ - | Data Processing | - | - Sanitize fields | - | - Smart tag resolve | - | - Collect metadata | - +----------+-----------+ - | - +----------------+----------------+ - | | -+--------v--------+ +--------v--------+ -| Email Notify | | Entry Storage | -| Email_Template | | wp_srfm_entries | -+-----------------+ +--------+--------+ - | - +--------v--------+ - | Payment Process | - | (if applicable) | - | wp_srfm_payments| - +--------+--------+ - | - +--------v--------+ - | Confirmation | - | Response | - +-----------------+ -``` - -## Step-by-Step Process - -### 1. Frontend Submission - -The form is submitted via JavaScript `fetch()` to `POST /wp-json/sureforms/v1/submit-form` with: - -- Form field data as POST body -- `X-WP-Submit-Nonce` header for security (does not require logged-in user) -- Form ID to identify which form was submitted - -Nonces can be refreshed via `GET /refresh-nonces` (public, no auth required). - -### 2. Permission Check - -`Form_Submit::submit_form_permissions_check()`: - -- Verifies the `X-WP-Submit-Nonce` header -- Validates that form data and form ID are present -- Returns `WP_Error` on failure - -### 3. Form Restrictions - -`Form_Restriction` class checks: - -- **Maximum entries** -- Has the form reached its submission limit? -- **Date scheduling** -- Is the form within its active date range? -- **Login required** -- Does the form require a logged-in user? -- **Entry caps** -- Per-user submission limits - -### 4. Field Validation - -`Field_Validation` (`inc/field-validation.php`): - -- **Required fields** -- Ensures all required fields have non-empty values -- **Type validation** -- Email format, URL format, number ranges, phone format -- **Unique values** -- Checks existing entries for uniqueness if configured -- Returns validation errors that prevent submission - -### 5. Security Verification - -- **CAPTCHA** -- Validates reCAPTCHA v2/v3, hCaptcha, or Cloudflare Turnstile token -- **Honeypot** -- Checks the hidden honeypot field is empty (bots fill it) - -### 6. Data Processing - -- `apply_filters('srfm_form_submit_data', $data, $form_id)` -- allows modification -- Sanitizes each field value -- Resolves smart tags for email templates -- Collects submission metadata (IP, browser via Browser class, device info) -- `do_action('srfm_before_submission', $data, $form_id)` - -### 7. Email Notifications - -`Email_Template` sends configured email notifications: - -- Processes smart tags in subject and body -- Sends to configured recipients -- Handles CC/BCC -- See [Email Notifications](Email-Notifications) for details - -### 8. Entry Storage - -Checks GDPR compliance (`do_not_store_entries` setting). If storage is allowed: - -```php -Entries::add([ - 'form_id' => $form_id, - 'user_id' => get_current_user_id(), - 'form_data' => $sanitized_data, // JSON-encoded - 'submission_info' => $metadata, // JSON-encoded - 'logs' => $activity_logs, // JSON-encoded - 'status' => 'unread', - 'type' => $form_type, // 'standard', 'quiz', etc. -]); -``` - -### 9. Payment Processing - -If the form includes a payment block: - -- Stripe payment is processed -- Payment record created in `wp_srfm_payments` -- Payment ID linked to the entry -- See [Payment Integration](Payment-Integration) for details - -### 10. Post-Submission - -- `do_action('srfm_form_submit', $entry_id, $form_id, $form_data)` -- `do_action('srfm_after_submission_process', $entry_id, $form_id)` -- Returns confirmation response: - - **Message** -- Display a success message - - **Redirect** -- Redirect to a specified URL - -## Form Confirmation Types - -Configured via `_srfm_form_confirmation` post meta: - -| Type | Behavior | -|------|----------| -| Message | Display a success message on the same page | -| Redirect | Redirect to a specified URL after submission | - -Confirmation parameters are filterable via `srfm_form_confirmation_params`. - -## Error Handling - -Errors at any stage return a JSON error response to the frontend: - -```json -{ - "success": false, - "data": "Validation error message" -} -``` - -The frontend JavaScript displays errors inline next to the relevant fields. - -## Related Pages - -- [REST API Reference](REST-API-Reference) -- Submit endpoint details -- [Database Schema](Database-Schema) -- Entry and payment table structure -- [Email Notifications](Email-Notifications) -- Email dispatch details -- [Payment Integration](Payment-Integration) -- Payment processing -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- Submission hooks -- [Form Fields Architecture](Form-Fields-Architecture) -- Field validation diff --git a/docs/wiki/Frontend-Assets.md b/docs/wiki/Frontend-Assets.md deleted file mode 100644 index 99b4cca91..000000000 --- a/docs/wiki/Frontend-Assets.md +++ /dev/null @@ -1,122 +0,0 @@ -# Frontend Assets - -SureForms manages assets across three contexts: frontend (form display), admin (dashboard), and block editor. - -## Asset Loading Strategy - -### Frontend (Form Display) - -`inc/frontend-assets.php` (`Frontend_Assets` class) handles loading CSS/JS on pages containing SureForms forms: - -- Detects form blocks in page content -- Enqueues only necessary field-specific CSS/JS -- Loads Stripe JS SDK only when payment fields are present -- Enqueues `intl-tel-input` only when phone fields exist - -### Admin Dashboard - -Admin assets are enqueued only on SureForms admin pages: - -- React app bundle from `assets/build/` -- Tailwind CSS base styles from `src/admin/tw-base.scss` -- Localized data via `wp_localize_script()` - -### Block Editor - -`inc/gutenberg-hooks.php` (`Gutenberg_Hooks` class) loads editor assets: - -- Block editor JS/CSS from `assets/build/` -- Editor-specific stylesheets -- Block preview styles - -## Build Pipeline - -### wp-scripts (Webpack) - -Primary JS build tool. Configuration extends `@wordpress/scripts` defaults: - -```bash -npm start # Development with hot reload -npm run build:script # Production build -``` - -**Entry Points:** -- `src/blocks/blocks.js` -- Gutenberg block registration -- `src/admin/editor-scripts.js` -- Admin dashboard React app -- Additional entries per block type - -**Output:** `assets/build/` directory - -**Webpack Features:** -- Code splitting per entry point -- WordPress dependency extraction (`@wordpress/*` packages) -- Source maps in development -- Asset hashing for cache busting - -### Grunt (SASS / Minification) - -```bash -npm run build:sass # Compile SASS only -npm run build # Full build (JS + SASS + minify) -``` - -The `Gruntfile.js` tasks include: - -| Task | Description | -|------|-------------| -| `sass` | Compile SASS files from `sass/` to `assets/css/` | -| `postcss` | Run PostCSS with Autoprefixer | -| `cssmin` | Minify CSS to `assets/css/minified/` | -| `uglify` | Minify JS to `assets/js/minified/` | -| `copy` | Copy assets to distribution directories | - -### SASS Directory Structure - -``` -sass/ -|-- frontend block styles -|-- form field styles -|-- layout and grid styles -``` - -Compiled output: `assets/css/` (unminified) and `assets/css/minified/` - -### Tailwind CSS - -Used exclusively for admin dashboard pages: - -**Configuration (`tailwind.config.js`):** -- Content paths: `src/admin/**/*.{js,jsx}` -- Plugins: `@tailwindcss/forms` -- Custom theme extensions for Force UI compatibility -- `tailwind-merge` used for dynamic class composition - -**Base styles:** `src/admin/tw-base.scss` - -## CSS Naming Conventions - -| Context | Prefix | Example | -|---------|--------|---------| -| Frontend form styles | `srfm-` | `srfm-input-field`, `srfm-form-container` | -| Admin dashboard | Tailwind utilities | `flex items-center gap-2` | -| Block editor | `srfm-editor-` | `srfm-editor-preview` | - -## Asset Versioning - -All enqueued assets use `SRFM_VER` (currently `2.5.1`) for cache busting: - -```php -wp_enqueue_style( - 'srfm-frontend', - SRFM_URL . 'assets/css/minified/frontend.min.css', - [], - SRFM_VER -); -``` - -## Related Pages - -- [Admin Dashboard](Admin-Dashboard) -- React admin asset details -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block editor assets -- [Environment Configuration](Environment-Configuration) -- Build tool setup -- [Contributing Guide](Contributing-Guide) -- CSS/JS conventions diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md deleted file mode 100644 index a0f2ea908..000000000 --- a/docs/wiki/Getting-Started.md +++ /dev/null @@ -1,149 +0,0 @@ -# Getting Started - -This guide walks you through setting up a SureForms development environment from scratch. - -## Prerequisites - -| Tool | Version | Notes | -|------|---------|-------| -| PHP | 7.4+ | Required for plugin runtime | -| WordPress | 6.4+ | Required for block editor features | -| Node.js | 24.16.0 | Managed via Volta (auto-switches) | -| Composer | 2.x | PHP dependency manager | -| Git | 2.x | Version control | -| Docker | Latest | Optional, for wp-env test environment | - -### Installing Volta (Recommended) - -[Volta](https://volta.sh/) ensures you use the correct Node.js version automatically: - -```bash -curl https://get.volta.sh | bash -``` - -The `package.json` pins Node 24.16.0 via Volta config -- it activates automatically when you enter the project directory. - -## Initial Setup - -### 1. Clone the Repository - -```bash -git clone https://github.com/brainstormforce/sureforms.git -cd sureforms -``` - -### 2. Install Dependencies - -```bash -composer install # PHP dependencies -npm install # Node dependencies -``` - -### 3. Build the Plugin - -```bash -npm run build # Full production build (JS + SASS + minify) -``` - -This runs three steps: -1. `wp-scripts build` -- Bundles JS/React from `src/` to `build/` -2. SASS compilation -- Compiles `sass/` to `assets/css/` -3. `grunt minify` -- Minifies CSS and JS to `assets/css/minified/` and `assets/js/minified/` - -### 4. Start Development Server - -```bash -npm start # wp-scripts dev server with hot reload -``` - -This watches `src/` for changes and rebuilds JS automatically. For SASS changes, run `npm run build:sass` separately. - -## WordPress Environment - -### Option A: Local WordPress Installation - -1. Set up a local WordPress site (e.g., using Local, MAMP, or Docker) -2. Symlink or copy the plugin into `wp-content/plugins/sureforms/` -3. Activate the plugin from the WordPress admin - -### Option B: wp-env (Docker-Based) - -The project includes a `.wp-env.json` configuration for a Docker-based WordPress environment: - -```bash -npm run play:up # Start WordPress + MySQL containers -``` - -This provides: -- WordPress at `http://localhost:8888` (admin: `admin` / `password`) -- Test WordPress at `http://localhost:8889` -- Plugin auto-mounted and activated -- Consistent environment across developers - -Additional commands: -```bash -npm run play:stop # Stop containers -npm run env:clean # Reset all data -``` - -## Verifying Your Setup - -### Run Linters - -```bash -vendor/bin/phpcs # PHP CodeSniffer -npm run lint-js # ESLint -npm run lint-css # Stylelint -npm run pretty # Prettier -``` - -### Run Tests - -```bash -vendor/bin/phpunit # PHP unit tests -npm run test:unit # JavaScript unit tests -``` - -### Build Check - -```bash -npm run build # Should complete without errors -``` - -## Project Structure Overview - -``` -sureforms/ -├── sureforms.php # Plugin entry point -├── plugin-loader.php # Autoloader and bootstrap -├── inc/ # PHP classes (core logic) -├── src/ # JS/React source (compiled by wp-scripts) -│ ├── admin/ # Admin dashboard React app -│ ├── blocks/ # Gutenberg block components -│ ├── components/ # Shared React components -│ └── store/ # WordPress data store -├── sass/ # SASS source files -├── assets/ # Compiled assets (CSS, JS, images) -├── templates/ # PHP templates (BladeOne) -├── tests/ # Test files -├── modules/ # Feature modules -└── admin/ # Admin PHP + assets -``` - -See [Architecture Overview](Architecture-Overview) for a detailed breakdown. - -## Key Concepts - -- **Custom Post Type**: Forms are stored as `sureforms_form` posts with block content -- **Custom Database Tables**: Entries in `wp_srfm_entries`, payments in `wp_srfm_payments` -- **REST API**: All data operations use the `sureforms/v1` REST namespace -- **Block Editor**: Form fields are Gutenberg blocks registered via `block.json` -- **Admin SPA**: The dashboard is a React single-page application using Force UI components - -## Next Steps - -- [Architecture Overview](Architecture-Overview) -- Understand the codebase -- [Contributing Guide](Contributing-Guide) -- Development workflow and standards -- [Gutenberg Blocks](Gutenberg-Blocks) -- How form blocks work -- [REST API Reference](REST-API-Reference) -- Available API endpoints -- [Testing Guide](Testing-Guide) -- Running and writing tests diff --git a/docs/wiki/Gutenberg-Blocks.md b/docs/wiki/Gutenberg-Blocks.md deleted file mode 100644 index ed38cd9b3..000000000 --- a/docs/wiki/Gutenberg-Blocks.md +++ /dev/null @@ -1,129 +0,0 @@ -# Gutenberg Blocks - -SureForms provides 14+ Gutenberg block types for building forms in the WordPress block editor. - -## Block Types - -### Field Blocks - -| Block | Slug | Description | -|-------|------|-------------| -| Input | `srfm/input` | Text input (text, hidden, etc.) | -| Email | `srfm/email` | Email address input with validation | -| Textarea | `srfm/textarea` | Multi-line text area | -| Number | `srfm/number` | Numeric input with min/max | -| Phone | `srfm/phone` | Phone number with country selector (intl-tel-input) | -| URL | `srfm/url` | URL input with validation | -| Dropdown | `srfm/dropdown` | Select dropdown with options | -| Checkbox | `srfm/checkbox` | Single/multiple checkboxes | -| Multi Choice | `srfm/multichoice` | Radio/checkbox group | -| Address | `srfm/address` | Address field with subfields (street, city, state, zip, country) | -| GDPR | `srfm/gdpr` | GDPR consent checkbox | -| Payment | `srfm/payment` | Payment collection (Stripe integration) | -| Inline Button | `srfm/inlinebutton` | Inline form button | - -### Container Block - -| Block | Slug | Description | -|-------|------|-------------| -| SForm | `srfm/sform` | Main form wrapper block (required container for all field blocks) | - -## Block Registration - -### Server-Side (PHP) - -Blocks are registered in `inc/blocks/register.php` via the `Blocks\Register` class, initialized on the `init` hook: - -```php -// inc/blocks/register.php -register_block_type_from_metadata( - SRFM_DIR . 'inc/blocks/{block-type}/', - [ - 'render_callback' => [ $block_instance, 'render' ], - ] -); -``` - -Each block type has: -- `block.json` -- Block metadata, attributes, and category -- `block.php` -- Server-side rendering class extending `Blocks\Base` - -### Client-Side (JavaScript) - -Block registration happens in `src/blocks/blocks.js`: - -```javascript -// src/blocks/blocks.js -import { registerBlockType } from '@wordpress/blocks'; -// Block imports and registrations... -``` - -The `src/blocks/register-block.js` utility handles individual block registration. - -## Block Architecture - -### Base Class: `Blocks\Base` - -All block PHP classes extend `SRFM\Inc\Blocks\Base`, which provides: - -- `render()` -- Main render callback invoked by WordPress -- Access to block attributes from `block.json` -- Common rendering logic shared across field types -- Integration with field markup classes - -### Block to Field Mapping - -Each block delegates HTML generation to its corresponding field markup class in `inc/fields/`: - -``` -Block (inc/blocks/input/block.php) - -> Field Markup (inc/fields/input-markup.php) - -> Base Field (inc/fields/base.php) - -> HTML output -``` - -### block.json Structure - -Each block directory contains a `block.json` file defining: - -```json -{ - "apiVersion": 3, - "name": "srfm/input", - "title": "Input", - "category": "sureforms", - "attributes": { - "label": { "type": "string", "default": "Input" }, - "required": { "type": "boolean", "default": false }, - "placeholder": { "type": "string", "default": "" } - }, - "supports": { "html": false } -} -``` - -## Adding a New Block Type - -1. **Create block directory:** `inc/blocks/{new-type}/` -2. **Add `block.json`** with attributes, name, and category -3. **Create `block.php`** extending `Blocks\Base` with a `render()` method -4. **Create field markup:** `inc/fields/{new-type}-markup.php` extending `Fields\Base` -5. **Register in `Blocks\Register`** -- add to the blocks array -6. **Create editor component:** `src/blocks/{new-type}/` with `edit.js` and `index.js` -7. **Add to `src/blocks/blocks.js`** -- import and register the new block - -## The sform Container Block - -The `srfm/sform` block is the required wrapper for all form fields. It: - -- Renders the `` HTML element -- Handles form-level settings (styling, confirmation, CAPTCHA) -- Manages submission nonce generation -- Controls form layout and submit button -- Integrates with frontend JavaScript for submission handling - -## Related Pages - -- [Form Fields Architecture](Form-Fields-Architecture) -- Field markup and validation -- [Block Editor Controls](Block-Editor-Controls) -- Custom inspector controls -- [Architecture Overview](Architecture-Overview) -- Block registration in bootstrap -- [Frontend Assets](Frontend-Assets) -- Block editor asset loading diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md deleted file mode 100644 index 365d8af63..000000000 --- a/docs/wiki/Home.md +++ /dev/null @@ -1,58 +0,0 @@ -# SureForms Wiki - -Welcome to the SureForms developer wiki. This documentation covers the architecture, APIs, development workflows, and internals of the SureForms WordPress form builder plugin. - -**Version:** 2.5.1 | **WordPress:** 6.4+ | **PHP:** 7.4+ | **Node:** 24.16.0 - -## Quick Links - -- [Getting Started](Getting-Started) -- Set up your development environment -- [Architecture Overview](Architecture-Overview) -- Understand the codebase structure -- [Contributing Guide](Contributing-Guide) -- How to contribute - -## Architecture & Core - -| Page | Description | -|------|-------------| -| [Architecture Overview](Architecture-Overview) | Plugin bootstrap, namespaces, directory layout, design patterns | -| [Database Schema](Database-Schema) | Custom tables (`wp_srfm_entries`, `wp_srfm_payments`), post meta keys | -| [Environment Configuration](Environment-Configuration) | Constants, build tools, linting, development setup | -| [Form Submission Flow](Form-Submission-Flow) | End-to-end submission pipeline from frontend to storage | - -## WordPress / Backend - -| Page | Description | -|------|-------------| -| [Gutenberg Blocks](Gutenberg-Blocks) | Block types, registration, `block.json` structure | -| [Form Fields Architecture](Form-Fields-Architecture) | Field type hierarchy, validation, rendering pipeline | -| [WordPress Hooks Reference](WordPress-Hooks-Reference) | Custom actions, filters, and execution order | -| [Payment Integration](Payment-Integration) | Stripe setup, payment flow, webhooks, refunds | -| [Email Notifications](Email-Notifications) | Email templates, smart tags, notification settings | -| [AI Form Builder](AI-Form-Builder) | AI-powered form generation process | -| [Data Export & Import](Data-Export-Import) | Form/entry export, import, duplication | - -## Frontend / React - -| Page | Description | -|------|-------------| -| [Admin Dashboard](Admin-Dashboard) | React SPA architecture, routing, data fetching | -| [State Management](State-Management) | WordPress data store, actions, selectors | -| [Frontend Assets](Frontend-Assets) | Asset loading, build pipeline, SASS, Tailwind | -| [Block Editor Controls](Block-Editor-Controls) | Custom inspector controls, CSS generation, icons | - -## API - -| Page | Description | -|------|-------------| -| [REST API Reference](REST-API-Reference) | All endpoints with methods, parameters, authentication | - -## Development Workflow - -| Page | Description | -|------|-------------| -| [Getting Started](Getting-Started) | Prerequisites, installation, first build | -| [Contributing Guide](Contributing-Guide) | Git workflow, coding standards, PR process | -| [Testing Guide](Testing-Guide) | PHPUnit, Playwright, Jest setup and execution | -| [Deployment Guide](Deployment-Guide) | CI/CD pipelines, release process, version bumping | -| [Troubleshooting & FAQ](Troubleshooting-FAQ) | Common issues and debugging tips | -| [Changelog](Changelog) | Recent version history | diff --git a/docs/wiki/Payment-Integration.md b/docs/wiki/Payment-Integration.md deleted file mode 100644 index 28d985e6e..000000000 --- a/docs/wiki/Payment-Integration.md +++ /dev/null @@ -1,127 +0,0 @@ -# Payment Integration - -SureForms supports Stripe payment processing with one-time payments and recurring subscriptions. - -## Architecture Overview - -``` -inc/payments/ -|-- payments.php # Main payments orchestrator -|-- front-end.php # Frontend payment handling -|-- payment-helper.php # Shared payment utilities -|-- admin/ -| |-- admin-handler.php # Admin payment management UI -|-- stripe/ - |-- payments-settings.php # Stripe settings + REST routes - |-- stripe-helper.php # Stripe API utilities - |-- stripe-webhook.php # Webhook event handler - |-- admin-stripe-handler.php # Admin Stripe config -``` - -## Stripe Setup - -### Configuration - -Stripe settings are managed via the admin settings page and stored as WordPress options: - -1. Navigate to SureForms > Settings > Payments -2. Connect your Stripe account via OAuth flow -3. Configure test/live mode -4. Set default currency - -The `Payments_Settings` class registers REST routes for settings management and handles the Stripe OAuth callback via `admin_init`. - -### OAuth Flow - -1. Admin clicks "Connect Stripe" in settings -2. `intercept_stripe_callback()` on `admin_init` handles the OAuth redirect -3. Stripe credentials are securely stored in WordPress options - -## Payment Flow - -``` -1. FORM DISPLAY - - Payment block renders Stripe Elements - - Frontend_Assets enqueues Stripe JS SDK - - Payment form elements injected into the form - -2. FORM SUBMISSION - - User fills form + enters payment details - - Frontend JavaScript creates Stripe PaymentIntent - - Form data + payment token submitted to /submit-form - -3. SERVER PROCESSING - - Form_Submit validates form data - - Payment processing triggered - - Stripe API called to confirm payment - - Payment record created in wp_srfm_payments table - - Entry created in wp_srfm_entries table - - Payment ID linked to entry - -4. WEBHOOK HANDLING - - Stripe sends webhook events to /wp-json endpoint - - Stripe_Webhook validates signature - - Payment status updated in database - - Subscription events handled (renewal, cancellation, etc.) -``` - -## Payment Database Schema - -Payments are stored in the `wp_srfm_payments` custom table. See [Database Schema](Database-Schema) for full column definitions. - -### Payment Types - -| Type | Description | -|------|-------------| -| `payment` | One-time payment | -| `subscription` | Recurring subscription record | -| `renewal` | Subscription renewal payment | - -### Payment Statuses - -`pending`, `succeeded`, `failed`, `canceled`, `requires_action`, `requires_payment_method`, `processing`, `refunded`, `partially_refunded` - -### Subscription Statuses - -`active`, `canceled`, `past_due`, `unpaid`, `trialing`, `incomplete`, `incomplete_expired`, `paused` - -## Webhook Handling - -The `Stripe_Webhook` class (`inc/payments/stripe/stripe-webhook.php`) processes Stripe webhook events: - -- Validates webhook signature using Stripe signing secret -- Handles payment status updates (succeeded, failed, etc.) -- Processes subscription lifecycle events (renewal, cancellation) -- Creates renewal payment records for subscription billing cycles -- Updates payment and subscription statuses in the database - -## Refund Management - -The `Payments` table class provides refund tracking: - -- `add_refund_to_payment_data()` -- Record refund details in payment_data JSON -- `add_refund_amount()` -- Increment the refunded_amount column -- `get_refundable_amount()` -- Calculate remaining refundable balance -- `is_fully_refunded()` / `is_partially_refunded()` -- Status checks - -## Supported Currencies - -USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, SEK, NZD, MXN, SGD, HKD, NOK, TRY, RUB, INR, BRL, ZAR, KRW - -## Payment Modes - -| Mode | Description | -|------|-------------| -| `test` | Stripe test mode (test API keys) | -| `live` | Stripe live mode (production) | - -## Admin Payment Management - -The `Admin_Handler` class provides the admin interface for viewing and managing payments. Payment IDs in entry views are rendered as clickable links via the `srfm_entry_value` filter. - -## Related Pages - -- [Database Schema](Database-Schema) -- Payment table columns and relationships -- [Form Submission Flow](Form-Submission-Flow) -- Payment processing in submission pipeline -- [REST API Reference](REST-API-Reference) -- Payment-related endpoints -- [Gutenberg Blocks](Gutenberg-Blocks) -- Payment block type diff --git a/docs/wiki/REST-API-Reference.md b/docs/wiki/REST-API-Reference.md deleted file mode 100644 index d366470e5..000000000 --- a/docs/wiki/REST-API-Reference.md +++ /dev/null @@ -1,114 +0,0 @@ -# REST API Reference - -All endpoints are registered under the namespace `sureforms/v1` and accessible at `/wp-json/sureforms/v1/`. - -## Authentication - -### Admin Endpoints -All admin endpoints require the `manage_options` capability. Authentication is via WordPress REST API nonce (`X-WP-Nonce` header) or cookie-based auth. - -Permission check: `Helper::get_items_permissions_check()` verifies `current_user_can('manage_options')`. - -### Public Endpoints -The form submission endpoint uses a custom nonce (`X-WP-Submit-Nonce` header) that does not require a logged-in user. - -## Endpoints - -### AI Form Builder - -| Method | Endpoint | Description | -|--------|----------|-------------| -| POST | `/generate-form` | Generate a form using AI | -| POST | `/map-fields` | Map AI response to SureForms Gutenberg blocks | -| GET | `/initiate-auth` | Get auth URL for AI billing portal | -| POST | `/handle-access-key` | Decrypt and save AI access key | - -### Dashboard - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/entries-chart-data` | Get entries chart data for analytics | -| GET | `/form-data` | Get all forms data for dashboard | -| GET | `/plugin-status` | Check integration plugin install status | - -### Onboarding - -| Method | Endpoint | Description | -|--------|----------|-------------| -| POST | `/onboarding/set-status` | Mark onboarding as complete | -| GET | `/onboarding/get-status` | Get current onboarding status | - -### Entries - -| Method | Endpoint | Description | Parameters | -|--------|----------|-------------|------------| -| GET | `/entries/list` | Paginated entries listing | `form_id`, `status`, `search`, `date_from`, `date_to`, `orderby`, `order`, `per_page`, `page` | -| POST | `/entries/read-status` | Bulk mark entries read/unread | `entry_ids` (required), `action` (required: read/unread) | -| POST | `/entries/trash` | Bulk trash/restore entries | `entry_ids` (required), `action` (required: trash/restore) | -| POST | `/entries/delete` | Permanently delete entries | `entry_ids` (required) | -| POST | `/entries/export` | Export entries to CSV/ZIP | `entry_ids`, `form_id`, `status`, `search`, `date_from`, `date_to` | -| GET | `/entry/{id}/details` | Single entry with form data | `id` (required) | -| GET | `/entry/{id}/logs` | Entry activity logs | `id` (required), `page`, `per_page` | - -### Forms - -| Method | Endpoint | Description | Parameters | -|--------|----------|-------------|------------| -| GET | `/forms` | Paginated forms listing | `page`, `per_page`, `search`, `status`, `orderby`, `order`, `date_from`, `date_to` | -| POST | `/forms/export` | Export forms as JSON | `form_ids` (required) | -| POST | `/forms/import` | Import forms from JSON | File upload | -| POST | `/forms/manage` | Manage form lifecycle | `form_ids` (required), `action` (trash/restore/delete) | -| POST | `/forms/duplicate` | Duplicate a form | `form_id` (required) | - -### Form Submission (Public) - -| Method | Endpoint | Auth | Description | -|--------|----------|------|-------------| -| POST | `/submit-form` | Custom nonce | Public form submission | -| GET | `/refresh-nonces` | None | Refresh frontend nonces | - -## Extending the API - -Endpoints are extensible via the `srfm_rest_api_endpoints` filter: - -```php -add_filter( 'srfm_rest_api_endpoints', function( $endpoints ) { - $endpoints['custom/endpoint'] = [ - 'methods' => 'GET', - 'callback' => [ $this, 'handle_custom' ], - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ], - ]; - return $endpoints; -} ); -``` - -## Error Responses - -Errors are returned via `wp_send_json_error()`: - -```json -{ - "success": false, - "data": "Error message string" -} -``` - -## Pagination - -List endpoints support pagination with consistent parameters: - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `page` | 1 | Current page number | -| `per_page` | 20 | Items per page | -| `orderby` | `created_at` | Sort column | -| `order` | `DESC` | Sort direction (ASC/DESC) | - -Response includes total counts for pagination UI. - -## Related Pages - -- [Architecture Overview](Architecture-Overview) -- API registration flow -- [Form Submission Flow](Form-Submission-Flow) -- Submit endpoint details -- [Data Export Import](Data-Export-Import) -- Export/import endpoints -- [Payment Integration](Payment-Integration) -- Payment API endpoints diff --git a/docs/wiki/State-Management.md b/docs/wiki/State-Management.md deleted file mode 100644 index 6cb4b072a..000000000 --- a/docs/wiki/State-Management.md +++ /dev/null @@ -1,109 +0,0 @@ -# State Management - -SureForms uses the `@wordpress/data` package to create a Redux-like data store for managing application state in the block editor. - -## Store Structure - -Located in `src/store/`: - -``` -src/store/ -|-- store.js # Store registration -|-- reducer.js # State reducers -|-- actions.js # Action creators -|-- selectors.js # State selectors -|-- constants.js # Store constants -|-- setInitialState.js # Initial state configuration -``` - -## Store Registration - -The store is registered with `@wordpress/data` in `store.js`: - -```javascript -import { createReduxStore, register } from '@wordpress/data'; -import reducer from './reducer'; -import * as actions from './actions'; -import * as selectors from './selectors'; - -const store = createReduxStore('sureforms', { - reducer, - actions, - selectors, -}); - -register(store); -``` - -The store namespace is `sureforms`. - -## Initial State - -`setInitialState.js` configures the initial state from server-side localized data (`wp_localize_script`), providing: - -- Form settings and metadata -- Block defaults -- User preferences -- Plugin configuration - -## Actions - -Actions are defined in `actions.js` and dispatched to modify the store state. They follow the WordPress data store pattern: - -```javascript -// Dispatching an action -import { dispatch } from '@wordpress/data'; -dispatch('sureforms').updateFormSettings({ key: 'value' }); -``` - -## Selectors - -Selectors in `selectors.js` extract specific data from the store state: - -```javascript -// Using a selector -import { select } from '@wordpress/data'; -const settings = select('sureforms').getFormSettings(); -``` - -## Component Integration - -Components connect to the store using WordPress data hooks: - -```javascript -import { useSelect, useDispatch } from '@wordpress/data'; - -function MyComponent() { - const formData = useSelect((select) => { - return select('sureforms').getFormSettings(); - }, []); - - const { updateFormSettings } = useDispatch('sureforms'); - - return ( - - ); -} -``` - -## Constants - -`constants.js` defines store-wide constants used across actions, reducers, and selectors to ensure consistency. - -## Usage Context - -The WordPress data store is primarily used in: - -- **Block editor** -- Form-level settings and block state -- **Inspector controls** -- Sidebar settings panels -- **Block attributes** -- Synchronized state between blocks - -For the admin dashboard pages, `@tanstack/react-query` is used instead for server state management. See [Admin Dashboard](Admin-Dashboard) for details. - -## Related Pages - -- [Admin Dashboard](Admin-Dashboard) -- React admin app architecture -- [Block Editor Controls](Block-Editor-Controls) -- Inspector controls using the store -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block registration and attributes diff --git a/docs/wiki/Testing-Guide.md b/docs/wiki/Testing-Guide.md deleted file mode 100644 index ea129c084..000000000 --- a/docs/wiki/Testing-Guide.md +++ /dev/null @@ -1,172 +0,0 @@ -# Testing Guide - -SureForms uses three testing frameworks: PHPUnit for PHP unit tests, Playwright for end-to-end tests, and Jest for JavaScript unit tests. - -## Testing Strategy - -| Level | Framework | Location | Purpose | -|-------|-----------|----------|---------| -| Unit (PHP) | PHPUnit 9.x | `tests/unit/` | PHP class and function testing | -| Unit (JS) | Jest | Via `wp-scripts` | JavaScript utility testing | -| E2E | Playwright 1.48+ | `tests/play/` | Full browser-based workflow testing | - -## PHPUnit (PHP Unit Tests) - -### Setup - -1. Install PHP dependencies: - ```bash - composer install - ``` - -2. WordPress test environment must be available (via wp-env or local setup) - -### Running Tests - -```bash -# Run all PHP unit tests -vendor/bin/phpunit - -# Run a specific test file -vendor/bin/phpunit tests/unit/test-specific.php - -# Run tests with verbose output -vendor/bin/phpunit --verbose -``` - -### Test File Conventions - -- Location: `tests/unit/` -- File prefix: `test-` (e.g., `test-helper.php`) -- Class naming: `Test_ClassName` extending `WP_UnitTestCase` -- Method naming: `test_` prefix (e.g., `test_form_submission()`) - -### CI Configuration - -PHPUnit runs in GitHub Actions (`.github/workflows/phpunit.yml`): -- PHP: 8.2 -- WordPress: trunk -- MySQL: 5.7 -- Triggered on pull requests - -## Playwright (E2E Tests) - -### Prerequisites - -- Node.js 24.16.0 (Volta) -- Docker (for wp-env) - -### Setup - -```bash -# Install dependencies -npm install - -# Start the WordPress environment -npm run play:up - -# Build the plugin -npm run build - -# Install Playwright browsers -npx playwright install -``` - -### Running Tests - -```bash -# Run all E2E tests (headless) -npm run play:run - -# Run tests in headed mode (visible browser) -npm run play:run:interactive - -# Run a specific test file -npx playwright test tests/play/specific-test.spec.js -``` - -### Test File Conventions - -- Location: `tests/play/` -- File suffix: `.spec.js` -- Uses Playwright Test runner with `expect` assertions -- Page Object Model pattern for reusable interactions - -### CI Configuration - -Playwright runs in GitHub Actions (`.github/workflows/playwright.yml`): -- Triggered when PR is labeled with `e2e` -- Node: 18.15 -- Builds plugin and starts wp-env -- Generates HTML test report (30-day artifact retention) - -## Jest (JavaScript Unit Tests) - -### Running Tests - -```bash -npm run test:unit -``` - -Uses `@wordpress/scripts` Jest configuration with WordPress-specific transforms and module resolution. - -## Test Environment (wp-env) - -The Docker-based test environment is configured in `.wp-env.json`: - -```bash -# Start environment -npm run play:up - -# Stop environment -npm run play:stop - -# Clean all data -npm run env:clean -``` - -wp-env provides: -- WordPress installation with the plugin active -- MySQL database -- Consistent environment across developers -- Used by both Playwright E2E tests and local development - -## Writing New Tests - -### PHP Unit Test - -```php -class Test_My_Feature extends WP_UnitTestCase { - public function test_feature_works() { - // Arrange - $form_id = $this->factory->post->create([ - 'post_type' => 'sureforms_form', - ]); - - // Act - $result = some_function($form_id); - - // Assert - $this->assertEquals('expected', $result); - } -} -``` - -### Playwright E2E Test - -```javascript -const { test, expect } = require('@playwright/test'); - -test('form submission works', async ({ page }) => { - await page.goto('/form-page/'); - await page.fill('[name="email"]', 'test@example.com'); - await page.click('button[type="submit"]'); - await expect(page.locator('.success-message')).toBeVisible(); -}); -``` - -## Related Pages - -- [Environment Configuration](Environment-Configuration) -- Development setup -- [Contributing Guide](Contributing-Guide) -- Test requirements for PRs -- [Deployment Guide](Deployment-Guide) -- CI/CD test pipeline diff --git a/docs/wiki/Troubleshooting-FAQ.md b/docs/wiki/Troubleshooting-FAQ.md deleted file mode 100644 index 7afa4619a..000000000 --- a/docs/wiki/Troubleshooting-FAQ.md +++ /dev/null @@ -1,232 +0,0 @@ -# Troubleshooting & FAQ - -Common issues, debugging tips, and frequently asked questions for SureForms development. - -## Common Development Issues - -### Build Failures - -**Problem:** `npm run build` fails with errors. - -**Solutions:** -1. Ensure Node.js 24.16.0 is active (Volta manages this automatically via `package.json`): - ```bash - node -v # Should output v24.16.0 - ``` -2. Clear and reinstall dependencies: - ```bash - rm -rf node_modules - npm install - ``` -3. If SASS compilation fails, check that `sass/` source files have valid syntax -4. If Grunt minification fails, ensure Grunt CLI is available: - ```bash - npx grunt minify - ``` - -### wp-env Issues - -**Problem:** `npm run play:up` fails to start the Docker environment. - -**Solutions:** -1. Ensure Docker is installed and running -2. Clean the environment: - ```bash - npm run env:clean - ``` -3. Restart the environment: - ```bash - npm run play:stop - npm run play:up - ``` -4. If port conflicts occur, check for other services on ports 8888/8889 - -### PHPCS Errors - -**Problem:** `vendor/bin/phpcs` reports violations. - -**Solutions:** -1. Auto-fix what's possible: - ```bash - vendor/bin/phpcbf - ``` -2. Common issues: - - Missing text domain: Add `'sureforms'` to all translatable strings - - Unescaped output: Wrap output in `esc_html()`, `esc_attr()`, etc. - - Missing nonce verification: Add nonce checks for form/AJAX handlers - - Short array syntax: Use `[]` instead of `array()` (enforced by config) - -### PHPUnit Test Failures - -**Problem:** `vendor/bin/phpunit` tests fail. - -**Solutions:** -1. Ensure WordPress test environment is available (wp-env or local setup) -2. Check database connectivity for the test suite -3. Run a specific test to isolate the issue: - ```bash - vendor/bin/phpunit tests/unit/test-specific.php - ``` - -### JavaScript Lint Errors - -**Problem:** `npm run lint-js` reports errors. - -**Solutions:** -1. Auto-fix most issues: - ```bash - npm run lint-js:fix - ``` -2. Format code with Prettier: - ```bash - npm run pretty:fix - ``` -3. Common issues: - - Import ordering: WordPress dependencies should use `@wordpress/*` - - Unused variables: Remove or prefix with underscore - - Missing dependencies in `useEffect`: Add to dependency array - -## Form Submission Issues - -### Nonce Verification Fails - -**Cause:** The `X-WP-Submit-Nonce` has expired (default: 24-hour lifetime). - -**Debug:** -- Nonces can be refreshed via `GET /wp-json/sureforms/v1/refresh-nonces` -- Check that the nonce is being sent in the request header -- Caching plugins may cache the nonce -- exclude form pages from page cache - -### CAPTCHA Validation Fails - -**Cause:** CAPTCHA keys are misconfigured or the token is expired. - -**Debug:** -1. Verify CAPTCHA keys in SureForms global settings -2. Check browser console for CAPTCHA loading errors -3. Ensure the CAPTCHA provider (reCAPTCHA, hCaptcha, Turnstile) is correctly configured -4. reCAPTCHA v3 can trigger multiple times on pages with multiple forms - -### File Upload Errors - -**Debug:** -1. Check PHP `upload_max_filesize` and `post_max_size` in `php.ini` -2. Verify allowed file types in form field settings -3. Check file size against the configured maximum -4. Ensure the `wp-content/uploads/` directory is writable - -## Database Issues - -### Custom Tables Missing - -**Cause:** The `wp_srfm_entries` or `wp_srfm_payments` table was not created. - -**Debug:** -1. Deactivate and reactivate the plugin to trigger table creation -2. Check the `srfm_db_version` option in `wp_options` -3. Verify database user has `CREATE TABLE` permissions -4. Check for errors in `debug.log` during activation - -### Entry Data Not Saving - -**Debug:** -1. Check the `do_not_store_entries` form setting (GDPR compliance) -2. Verify the form submission response for error messages -3. Check `debug.log` for database insert errors -4. Ensure the entries table schema is up to date - -## REST API Issues - -### 401 Unauthorized on Admin Endpoints - -**Cause:** User is not authenticated or lacks required capabilities. - -**Debug:** -1. Admin endpoints require `manage_options` capability -2. Ensure the user is logged in with a valid session -3. Check that the REST API nonce is being sent (via `X-WP-Nonce` header) -4. WordPress cookie authentication must be active - -### 403 on Form Submission - -**Cause:** The submit nonce is invalid or form restrictions are blocking submission. - -**Debug:** -1. Check the `X-WP-Submit-Nonce` header value -2. Review form restriction settings (max entries, date scheduling, login required) -3. Check the `srfm_form_submit_permissions_check` filter - -## Performance Issues - -### Slow Admin Dashboard - -**Causes & Solutions:** -1. **Large number of entries:** Entries are paginated -- default page size is managed by REST API -2. **Unoptimized queries:** The entries table has indexes on `form_id`, `user_id`, and a composite index on `(form_id, created_at, status)` -3. **Asset loading:** Admin assets are conditionally loaded only on SureForms admin pages - -### Slow Form Rendering - -**Causes & Solutions:** -1. **Too many blocks:** Complex forms with many fields may benefit from multi-step layout -2. **External scripts:** CAPTCHA and payment scripts load conditionally based on form settings -3. **CSS generation:** Each block generates dynamic CSS -- review `generateCSS.js` for optimization - -## Debugging Tips - -### Enable WordPress Debug Mode - -Add to `wp-config.php`: -```php -define( 'WP_DEBUG', true ); -define( 'WP_DEBUG_LOG', true ); -define( 'WP_DEBUG_DISPLAY', false ); -define( 'SCRIPT_DEBUG', true ); -``` - -### Check REST API Responses - -Use the browser developer tools Network tab to inspect REST API calls: -- Filter by `sureforms/v1` to see plugin API requests -- Check response status codes and error messages - -### Inspect Form Data Store - -In the browser console on a form editor page: -```javascript -wp.data.select('starter-starter/starter-starter-forms-data').getFormSettings(); -``` - -## FAQ - -### What WordPress version is required? - -WordPress 6.4 or higher is required. - -### What PHP version is required? - -PHP 7.4 or higher. The `phpcs.xml` enforces PHP 7.4+ compatibility. - -### Can I modify bundled libraries? - -Files in `inc/lib/` are third-party libraries and should not be modified directly. If changes are needed, they should be managed upstream. - -### Where do I put new PHP classes? - -New PHP classes go in the `inc/` directory following the existing namespace structure (`SRFM\Inc\*`). The autoloader in `plugin-loader.php` handles class loading. - -### How do I add a new admin page? - -Admin pages are rendered by the React SPA in `src/admin/`. Add new routes using `react-router-dom` and register the corresponding menu items in PHP. See [Admin Dashboard](Admin-Dashboard) for details. - -### How do I add a new form field type? - -See [Form Fields Architecture](Form-Fields-Architecture) for the field type hierarchy and the process for adding new field types. - -## Related Pages - -- [Environment Configuration](Environment-Configuration) -- Development setup -- [Testing Guide](Testing-Guide) -- Running tests -- [Architecture Overview](Architecture-Overview) -- Codebase structure -- [REST API Reference](REST-API-Reference) -- API endpoints -- [Database Schema](Database-Schema) -- Table structure diff --git a/docs/wiki/Using-WPML-With-SureForms.md b/docs/wiki/Using-WPML-With-SureForms.md deleted file mode 100644 index 540b74aef..000000000 --- a/docs/wiki/Using-WPML-With-SureForms.md +++ /dev/null @@ -1,243 +0,0 @@ -# Using WPML With SureForms - -SureForms ships with native compatibility for **WPML**, the leading WordPress multilingual plugin. With WPML active, you can offer your forms in any number of languages without duplicating the form itself — one form serves every language, every submission is tagged with the visitor's language, and translators work from a single place. - -This guide walks you through everything you need to know as a site owner, content editor, or translator. - ---- - -## What you can do with WPML + SureForms - -Once both plugins are active and configured, SureForms automatically: - -- Translates **field labels, placeholders, help text, error messages** and **dropdown / multi-choice options** based on the visitor's language. -- Translates the **submit button** label. -- Translates the **success message** shown after a submission. -- Translates the **confirmation redirect content** (when "show same page" confirmation is used). -- Translates the **form-restriction notice** (e.g., "This form is closed"). -- Records the **submission language** alongside every entry, so you can see at a glance which language a lead came from. - -You keep one form. You collect entries from every language into the same list. Reports, integrations, and exports stay unified. - ---- - -## Before you start - -You will need: - -| Item | Notes | -|---|---| -| **WordPress 6.4 or higher** | SureForms minimum requirement. | -| **WPML Multilingual CMS** (4.5+) | Paid plugin from [wpml.org](https://wpml.org). | -| **WPML String Translation** add-on | Required to translate form labels, options, and messages. Bundled with most WPML licences. | -| **SureForms 2.5.1 or higher** | Native WPML support shipped in this release. | - -> **Polylang and other multilingual plugins:** SureForms is architected with a generic provider interface so other plugins can be supported in the future. WPML is the only multilingual plugin currently supported out of the box. - ---- - -## Step 1 — Install and configure WPML - -1. Install and activate the following plugins from your WordPress dashboard: - - **WPML Multilingual CMS** (`sitepress-multilingual-cms`) - - **WPML String Translation** - -2. Open **WPML → Languages** and choose at least two languages (for example, English as the default, plus German and French). - -3. Open **WPML → Settings → Translation Options** and confirm that **Posts** are set to *"Translatable — use translation"*. - -That's the only WPML setup that is required. SureForms takes care of the rest automatically. - ---- - -## Step 2 — Create your form (just once) - -Create a SureForms form exactly the way you always have: - -1. Go to **SureForms → Add New Form**. -2. Pick a template or start blank. -3. Add fields (text, email, dropdown, multi-choice, etc.) and configure their labels, placeholders, help text, options, and error messages in your default language. -4. Configure the submit button text, success message, and any other form settings. -5. Save the form. - -> **Important:** Do **not** try to duplicate the form for each language. SureForms intentionally registers the form post type as *non-translatable* in WPML. There is one canonical form post; WPML translates the strings on it. This keeps entries unified and avoids the maintenance nightmare of keeping multiple form copies in sync. - -When you save the form, SureForms automatically registers every translatable string with WPML under the `sureforms` text domain. - ---- - -## Step 3 — Translate your form strings - -1. Go to **WPML → String Translation**. -2. In the **"Select strings within domain"** dropdown at the top, choose `sureforms`. -3. You will see entries for every translatable piece of the form, grouped by form ID for easy scanning. For example, a form with ID `42` will show strings such as: - - - `form_42_submit_button` - - `form_42_confirmation_0_message` - - `form_42_notification_0_subject` - - `form_42_restriction_message` - - `form_42_block_{block-id}_label` - - `form_42_block_{block-id}_placeholder` - - `form_42_block_{block-id}_helpText` - - `form_42_block_{block-id}_errorMsg` - - `form_42_block_{block-id}_option_0_label` *(for dropdown / multi-choice options)* - -4. Click **"translations"** next to any string, enter the translated text for each language, and save. - -You can also use WPML's built-in **Translation Editor** workflow or send strings to a translation service from this screen — the standard WPML String Translation UI applies. - -> **Tip:** The per-form naming scheme (`form_{id}_...`) keeps every form's strings neatly grouped, so translators don't have to hunt across the entire site for a particular form's labels. - ---- - -## Step 4 — Display your form on translated pages - -SureForms forms are added to pages exactly the way they always have been — via the SureForms block or the `[sureforms id="..."]` shortcode. - -For the translated version of a form to render correctly, the **host page must also be translated in WPML**: - -1. Create the original page and embed your form on it. -2. In WPML, translate that page into each language. The form block / shortcode is carried over automatically. -3. Visit the translated URL (e.g., `/de/contact/`) — the form will render with all the labels, placeholders, options, and the submit button in German. - -If you instead use SureForms' **instant form** URL (`/form/{id}/`), append the language code to switch languages: - -``` -https://example.com/form/42/?lang=de -https://example.com/form/42/?lang=fr -``` - ---- - -## Step 5 — Verify everything is translating - -Open the translated form in the browser and confirm that the following are all in the target language: - -- ✅ Field labels -- ✅ Field placeholders -- ✅ Help text -- ✅ Inline error messages (e.g., "This field is required") -- ✅ Dropdown / multi-choice / checkbox option labels -- ✅ Submit button text -- ✅ Success / confirmation message after submission -- ✅ Form restriction notice (if the form is closed or capped) - -If anything appears in the original language: - -1. Check that the host page is translated in WPML. -2. Check that the specific string has a translation in **WPML → String Translation** (filter by domain `sureforms`). -3. Re-save the form once — this regenerates the string registry. - ---- - -## Step 6 — Collect entries and review them by language - -Submissions made on the German version of the page are stored against the **same form** as English submissions, with the submission language recorded on the entry row. - -1. Go to **SureForms → Entries**. -2. You will see a new **Language** column between *Date & Time* and *Actions*. -3. Each entry displays an uppercase language code: `EN`, `DE`, `FR`, etc. -4. Click the **Language** column header to sort entries by language. - -Older entries (and entries submitted on sites where WPML is not active) display `—`. - -> **Tip:** All your usual entry features — search, view details, delete, export — work as normal. Language is just an extra column for filtering and reporting. - ---- - -## Step 7 — Form restriction messages - -If you have configured form restrictions (a closing date or maximum entry count), the **restriction notice** shown to visitors is automatically translated: - -1. Open the form and enable restrictions (e.g., set a close date in the past, or set a max-entry limit). -2. Customise the restriction message in the form settings. -3. Translate `form_{id}_restriction_message` in **WPML → String Translation**. -4. Trigger the restriction (visit the form in a translated URL once it's closed) — visitors see the translated restriction notice. - ---- - -## What about email notifications? - -> **Note:** In the current release, email notifications (the message sent to your admin or to the visitor after a successful submission) are sent in the site's default language. Multilingual email notifications — where each email is composed in the visitor's submission language — are planned for a future SureForms release. - -The submission language is already recorded on the entry, so any custom integration that hooks into the SureForms submission flow can use it. - ---- - -## Graceful fallback when WPML is not active - -You can safely install and uninstall WPML at any time: - -- **WPML active**: forms translate based on the visitor's language. Entries are tagged. -- **WPML deactivated**: forms render in their default language as if WPML had never been installed. New entries record an empty language string. No errors, no broken pages. - -The plugin is designed so that the multilingual layer is completely silent when WPML is not active. - ---- - -## Troubleshooting - -**Translated strings don't appear in WPML String Translation** - -- Confirm that *WPML String Translation* is active (not just WPML core). -- Re-save your form once. The string registry runs on form save. -- Filter by domain `sureforms` in the WPML String Translation UI — strings from other plugins live in different domains. - -**Translated labels still render in the original language on `/de/...`** - -- Make sure the host page is translated in WPML and you are visiting the translated URL. -- Open WPML → String Translation, filter by `sureforms`, and confirm the specific strings actually have translations saved. -- Check that the page language switcher confirms you are on the German (or other target) version of the page. - -**The success message is in English even though I submitted in German** - -- Confirm `form_{id}_confirmation_0_message` has a German translation in WPML String Translation. -- Confirm the form is being submitted from a German page (`/de/...`), not from a query-string variant in some WPML modes. - -**Entries show `—` in the Language column** - -- Older entries submitted before WPML support shipped will not have a language stored. -- Entries created from the admin (e.g., test submissions, or imports) may not carry a language code. -- New front-end submissions on translated pages with WPML active will always carry a language code. - -**An entry's language code is wrong** - -- The visitor's language is determined by WPML based on the URL of the page they submitted from. If your WPML configuration uses query-string mode (`?lang=de`), make sure the parameter is on the page URL when the form is submitted. - ---- - -## Frequently asked questions - -**Do I need to duplicate the form for each language?** - -No. There is one canonical form post regardless of how many languages you support. WPML translates the strings on it. - -**Will entries from different languages mix in the same list?** - -Yes — by design. All entries from a given form, regardless of the visitor's language, belong to that one form. The Language column lets you filter and sort. - -**Can I export entries with the language?** - -The submission language is stored alongside each entry and is available to any integration that reads SureForms entries. A future SureForms release will include the language column in the standard CSV export header. - -**What languages are supported?** - -Any language WPML supports. SureForms records the standard WPML language code (`en`, `de`, `hi`, `zh-cn`, `pt-br`, etc.) — up to 20 characters, which covers the full WPML and BCP-47 locale-code range. - -**Does this work with Polylang or other multilingual plugins?** - -SureForms is built on a generic multilingual provider interface, so support for Polylang and other plugins is straightforward to add. WPML is the only multilingual plugin currently supported in the box. - ---- - -## Need help? - -If you run into anything not covered here, contact the SureForms support team at [sureforms.com/support](https://sureforms.com/support) with: - -1. Your SureForms version (Plugins → Installed Plugins → SureForms). -2. Your WPML version, including whether *WPML String Translation* is active. -3. Your form ID. -4. The specific string or behaviour that isn't translating. -5. A screenshot if possible. - -We're happy to help. diff --git a/docs/wiki/WordPress-Hooks-Reference.md b/docs/wiki/WordPress-Hooks-Reference.md deleted file mode 100644 index 1db16e372..000000000 --- a/docs/wiki/WordPress-Hooks-Reference.md +++ /dev/null @@ -1,152 +0,0 @@ -# WordPress Hooks Reference - -SureForms exposes custom actions and filters for extensibility, and hooks into WordPress core lifecycle events. - -## Custom Actions - -| Action | Parameters | Description | -|--------|-----------|-------------| -| `srfm_core_loaded` | -- | Fired after Plugin_Loader singleton is created | -| `srfm_before_submission` | `$form_data`, `$form_id` | Before form submission is processed | -| `srfm_form_submit` | `$entry_id`, `$form_id`, `$form_data` | After form submission is complete | -| `srfm_after_submission_process` | `$entry_id`, `$form_id` | After all post-submission processing | -| `srfm_before_delete_entry` | `$entry_id` | Before an entry is permanently deleted | - -## Custom Filters - -### REST API - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_rest_api_endpoints` | `$endpoints` | Add/modify REST API endpoints | - -### Form Data - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_form_submit_data` | `$form_data`, `$form_id` | Modify form data before processing | -| `srfm_entry_value` | `$value`, `$args` | Transform entry field values in responses | -| `srfm_normalize_csv_field_value` | `$value`, `$field` | Custom CSV export value formatting | -| `srfm_form_confirmation_params` | `$params`, `$form_id` | Modify confirmation settings | - -### Post Meta - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_register_post_meta` | `$meta_keys` | Register additional form post meta keys | - -### Blocks - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_is_special_block` | `$is_special`, `$block_name` | Declare a block as special (nested) | -| `srfm_handle_special_block` | `$result`, `$block` | Custom processing for special blocks | -| `srfm_extract_form_fields_field_name` | `$field_name`, `$block` | Modify generated field names | - -### Settings - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_global_settings_data` | `$settings` | Extend global settings data | -| `srfm_disable_nps_survey` | `$disable` | Disable NPS survey module | -| `srfm_enable_redirect_activation` | `$enable` | Control activation redirect behavior | - -## WordPress Core Hooks Used - -### plugins_loaded - -| Priority | Callback | Class | -|----------|----------|-------| -| default | `load_textdomain()` | Plugin_Loader | -| 99 | `load_plugin()` | Plugin_Loader | - -### init - -| Priority | Callback | Purpose | -|----------|----------|---------| -| 10 | `load_classes()` | Block registration, admin, payments | - -### rest_api_init - -| Callback | Class | Purpose | -|----------|-------|---------| -| `register_endpoints()` | Rest_Api | Register all admin REST endpoints | -| `register_routes()` | Form_Submit | Register public submission endpoints | -| `register_rest_routes()` | Payments_Settings | Register payment REST routes | - -### admin_init - -| Callback | Purpose | -|----------|---------| -| `activation_redirect()` | Redirect to onboarding after activation | -| `intercept_stripe_callback()` | Handle Stripe OAuth callback | - -### wp_enqueue_scripts - -| Callback | Class | Purpose | -|----------|-------|---------| -| Asset enqueue | Frontend_Assets | Load form CSS/JS on pages with forms | - -### enqueue_block_editor_assets - -| Callback | Class | Purpose | -|----------|-------|---------| -| Editor asset enqueue | Gutenberg_Hooks | Load block editor JS/CSS | - -## Hook Execution Order (Form Submission) - -``` -1. REST API receives POST /submit-form -2. Form_Submit::submit_form_permissions_check() - -> Nonce verification -3. Form_Submit::handle_form_submission() - -> apply_filters('srfm_form_submit_data', $data, $form_id) - -> Field validation - -> CAPTCHA verification - -> Honeypot check -4. do_action('srfm_before_submission', $data, $form_id) -5. Email notification sent -6. Entry stored in database -7. do_action('srfm_form_submit', $entry_id, $form_id, $data) -8. do_action('srfm_after_submission_process', $entry_id, $form_id) -9. Confirmation response returned -``` - -## Usage Examples - -### Adding a Custom REST Endpoint - -```php -add_filter( 'srfm_rest_api_endpoints', function( $endpoints ) { - $endpoints['my-custom/data'] = [ - 'methods' => 'GET', - 'callback' => 'my_custom_callback', - 'permission_callback' => [ \SRFM\Inc\Helper::class, 'get_items_permissions_check' ], - ]; - return $endpoints; -} ); -``` - -### Modifying Form Data Before Processing - -```php -add_filter( 'srfm_form_submit_data', function( $data, $form_id ) { - // Add custom processing - $data['custom_field'] = 'custom_value'; - return $data; -}, 10, 2 ); -``` - -### Running Code After Submission - -```php -add_action( 'srfm_form_submit', function( $entry_id, $form_id, $form_data ) { - // Send to external service, trigger automation, etc. -}, 10, 3 ); -``` - -## Related Pages - -- [Architecture Overview](Architecture-Overview) -- Plugin bootstrap and hook registration -- [REST API Reference](REST-API-Reference) -- Full endpoint documentation -- [Form Submission Flow](Form-Submission-Flow) -- Submission hook execution order diff --git a/docs/wiki/_Footer.md b/docs/wiki/_Footer.md deleted file mode 100644 index 0a54d2e24..000000000 --- a/docs/wiki/_Footer.md +++ /dev/null @@ -1,3 +0,0 @@ ---- - -**SureForms** by [Brainstorm Force](https://brainstormforce.com/) | [GitHub](https://github.com/brainstormforce/sureforms) | [Website](https://sureforms.com/) | [Support](https://support.brainstormforce.com/) diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md deleted file mode 100644 index 4fe3b09d1..000000000 --- a/docs/wiki/_Sidebar.md +++ /dev/null @@ -1,36 +0,0 @@ -**[Home](Home)** - -**Getting Started** -- [Getting Started](Getting-Started) -- [Environment Configuration](Environment-Configuration) - -**Architecture** -- [Architecture Overview](Architecture-Overview) -- [Database Schema](Database-Schema) -- [Form Submission Flow](Form-Submission-Flow) - -**WordPress / Backend** -- [Gutenberg Blocks](Gutenberg-Blocks) -- [Form Fields Architecture](Form-Fields-Architecture) -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- [Payment Integration](Payment-Integration) -- [Email Notifications](Email-Notifications) -- [AI Form Builder](AI-Form-Builder) -- [Data Export & Import](Data-Export-Import) -- [Using WPML With SureForms](Using-WPML-With-SureForms) - -**Frontend / React** -- [Admin Dashboard](Admin-Dashboard) -- [State Management](State-Management) -- [Frontend Assets](Frontend-Assets) -- [Block Editor Controls](Block-Editor-Controls) - -**API** -- [REST API Reference](REST-API-Reference) - -**Development** -- [Contributing Guide](Contributing-Guide) -- [Testing Guide](Testing-Guide) -- [Deployment Guide](Deployment-Guide) -- [Troubleshooting & FAQ](Troubleshooting-FAQ) -- [Changelog](Changelog) diff --git a/inc/abilities/CLAUDE.md b/inc/abilities/CLAUDE.md deleted file mode 100644 index 0a7af5e04..000000000 --- a/inc/abilities/CLAUDE.md +++ /dev/null @@ -1,66 +0,0 @@ -# Abilities — Development Guide - -## Adding a New Ability - -1. Create a class extending `Abstract_Ability` in the appropriate subdirectory: - - `forms/` — form CRUD operations - - `entries/` — entry/submission operations - - `embedding/` — shortcodes, block markup, rendering - - `settings/` — global settings read/write - - `analytics/` — form analytics and reporting - - `export/` — form and entry import/export -2. Register it in `Abilities_Registrar::register_abilities()` - -## Abstract_Ability Contract - -Every ability must implement: -- **Constructor** — set `$id`, `$label`, `$description`, `$capability` -- **`get_input_schema()`** — JSON Schema for input validation -- **`get_output_schema()`** — JSON Schema for output contract -- **`execute( $input )`** — business logic, return array or `WP_Error` -- **`get_annotations()`** — override if not default (`readonly`, `destructive`, `idempotent`) - -## Conventions - -- ID format: `sureforms/-` (e.g. `sureforms/list-forms`) -- Namespace: `SRFM\Inc\Abilities\\` -- Return `WP_Error` for failures, never throw exceptions -- Default capability: `edit_posts` — override per-ability where needed -- Destructive abilities must set `destructive: true` in annotations -- All classes need `@since x.x.x` tags -- Add PHPUnit tests in `tests/unit/inc/abilities/` for every new ability - -## Architecture - -``` -inc/abilities/ -├── abstract-ability.php # Base class — do not modify without updating all abilities -├── abilities-registrar.php # Singleton — registers category + all abilities -├── forms/ -│ ├── list-forms.php # readonly, idempotent -│ ├── create-form.php # uses Field_Mapping engine -│ ├── get-form.php # readonly, idempotent — parses Gutenberg blocks -│ ├── delete-form.php # destructive -│ ├── duplicate-form.php # delegates to \SRFM\Inc\Duplicate_Form -│ ├── update-form.php # write, idempotent — title/status/metadata -│ └── get-form-stats.php # readonly, idempotent — entry counts -├── entries/ -│ ├── list-entries.php # readonly, idempotent — paginated listing -│ ├── get-entry.php # readonly, idempotent — decrypted form_data -│ ├── update-entry-status.php # write, idempotent — read/unread/trash/restore -│ └── delete-entry.php # destructive — permanent delete -├── embedding/ -│ └── get-shortcode.php # readonly, idempotent -├── settings/ -│ ├── get-global-settings.php # readonly, idempotent — all setting categories -│ └── update-global-settings.php # write, idempotent — delegates to Global_Settings -└── analytics/ - └── get-form-analytics.php # readonly, idempotent — submission date queries -``` - -## Extensibility - -- **Filter:** `srfm_register_abilities` — third-party plugins can add abilities (must extend `Abstract_Ability`) -- **Hooks:** `srfm_before_ability_execute` / `srfm_after_ability_execute` — fire around every execution -- **Collision guard:** Registrar checks `wp_has_ability()` before registering to avoid duplicates with zipwp-mcp -- **WP < 6.9:** Registrar bails in constructor — zero overhead when Abilities API is unavailable diff --git a/internal-docs/README.md b/internal-docs/README.md deleted file mode 100644 index 9a933cae0..000000000 --- a/internal-docs/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# SureForms Internal Documentation - -**Version:** 2.5.0 -**Last Updated:** February 2026 -**Target Audience:** Developers, AI Agents, Technical Team - ---- - -## Welcome - -This is the internal documentation for **SureForms Free** and **SureForms Pro** - WordPress form builder plugins used by 300,000+ websites. - -**SureForms Free:** AI-powered form builder with payments (Stripe), native WordPress blocks, 16+ fields -**SureForms Pro:** Advanced features - conversational forms, multi-step, conditional logic, PayPal, 24+ integrations - ---- - -## Quick Start - -### For New Developers (1 Hour) - -1. **Install locally:** - ```bash - cd /path/to/wordpress/wp-content/plugins - git clone https://github.com/brainstormforce/sureforms.git - git clone https://github.com/brainstormforce/sureforms-pro.git - cd sureforms && npm install && npm run build - cd ../sureforms-pro && npm install && npm run build - ``` - -2. **Activate plugins:** - - SureForms Free (required) - - SureForms Pro (optional, requires Free) - -3. **Create first form:** - - Dashboard → SureForms → Add New - - Try AI form builder or blank form - - Add fields from block inserter - - Publish and test submission - -4. **Read these docs:** - - [Architecture](architecture.md) - System design - - [Codebase Map](codebase-map.md) - Where is what - - [APIs](apis.md) - REST, AJAX, hooks - -### For AI Agents - -Start with [ai-agent-guide.md](ai-agent-guide.md) for codebase conventions, patterns, and agent-specific guidance. - ---- - -## Key Files & Directories - -| Path | Purpose | -|------|---------| -| `sureforms/inc/` | Core PHP logic | -| `sureforms/inc/blocks/` | Gutenberg block PHP | -| `sureforms/src/` | React components (admin UI) | -| `sureforms/inc/payments/` | Stripe payment processing | -| `sureforms/inc/ai-form-builder/` | AI form generation | -| `sureforms-pro/inc/business/` | Pro features (PayPal, registration, PDF) | -| `sureforms-pro/inc/pro/native-integrations/` | 24+ service integrations | - ---- - -## Build System - -**SureForms Free:** -```bash -npm run build # Full build (webpack + sass + grunt) -npm run start # Dev mode (watch) -npm run lint-js:fix # Fix JS linting -npm run makepot # Generate translation files -``` - -**SureForms Pro:** -```bash -npm run build # Full build -npm run package # Create distributable zip -``` - -Both use: -- **@wordpress/scripts** (webpack 5, Babel, ESLint) -- **Grunt** (minification, compression) -- **Sass** for CSS compilation - ---- - -## Database Tables - -Custom tables (via `inc/database/`): - -| Table | Purpose | -|-------|---------| -| `wp_sureforms_entries` | Form submissions | -| `wp_sureforms_payments` | Payment transactions (Stripe/PayPal) | -| `wp_sureforms_integrations` | Native integration credentials (encrypted) | -| `wp_sureforms_save_resume` | Draft form state (Pro) | - ---- - -## Testing - -**Unit Tests:** -```bash -composer test # PHPUnit -npm run test:unit # Jest (JS) -``` - -**E2E Tests:** -```bash -npm run play:up # Start wp-env -npm run play:run # Run Playwright tests -``` - -**Code Quality:** -```bash -composer lint # PHP_CodeSniffer -composer phpstan # Static analysis -npm run lint-js # ESLint -``` - ---- - -## Release Process - -**Free (WordPress.org):** -1. Update version in `sureforms.php` and `package.json` -2. Run `npm run build && grunt release` -3. Files excluded via `.distignore` -4. SVN commit to WordPress.org repo - -**Pro (Custom Distribution):** -1. Update version in `sureforms-pro.php` -2. Run `npm run build && npm run package` -3. Generates `sureforms-pro.zip` -4. Deploy to licensing server - ---- - -## Support & Resources - -- **GitHub (Free):** https://github.com/brainstormforce/sureforms -- **GitHub (Pro):** https://github.com/brainstormforce/sureforms-pro -- **Documentation:** https://sureforms.com/docs/ -- **Bug Bounty:** https://brainstormforce.com/bug-bounty-program/ -- **Support:** support@brainstormforce.com - ---- - -## Next Steps - -- **Product Vision:** [product-vision.md](product-vision.md) - Understand goals and personas -- **Architecture:** [architecture.md](architecture.md) - System design and data flow -- **Onboarding:** [onboarding.md](onboarding.md) - 1-hour, 1-day, 1-week learning paths - ---- - -**Maintained by:** SureForms Engineering Team -**Contact:** engineering@brainstormforce.com diff --git a/internal-docs/ai-agent-guide.md b/internal-docs/ai-agent-guide.md deleted file mode 100644 index 867f3c7ea..000000000 --- a/internal-docs/ai-agent-guide.md +++ /dev/null @@ -1,459 +0,0 @@ -# AI Agent Guide - -**Version:** 2.5.0 -**For:** AI Coding Agents & LLMs - ---- - -## Overview - -This guide helps AI agents understand SureForms codebase conventions, make safe changes, and avoid common pitfalls. - ---- - -## Core Principles - -1. **Security First:** Sanitize inputs, escape outputs, use prepared statements -2. **WordPress Standards:** Follow WP coding standards, use WP functions -3. **Backward Compatibility:** Don't break existing forms/entries -4. **Performance:** Avoid N+1 queries, cache when possible -5. **Testing:** Write tests, run existing tests before committing - ---- - -## File Modification Guidelines - -### ✅ Safe to Modify - -**Add features (low risk):** -- New blocks: `inc/blocks/new-block/` -- New integrations (Pro): `inc/pro/native-integrations/integrations/new-service/` -- New validation rules: Add to `inc/field-validation.php` -- New hooks: Add filters/actions (don't remove existing) - -**Extend existing:** -- Add sanitization functions to `inc/helper.php` -- Add validation methods to `inc/field-validation.php` -- Add utility functions to helper classes - -### ⚠️ Modify with Caution - -**Database schema changes:** -- Requires migration script -- Must handle existing data -- Version increments in `inc/database/tables/*.php` - -**Payment processing:** -- `inc/payments/stripe/stripe-webhook.php` -- `inc/business/payments/pay-pal/webhook-listener.php` -- **Risk:** Financial impact if broken - -**Form submission pipeline:** -- `inc/form-submit.php` -- **Risk:** Could break all forms - -### 🚫 High Risk - Avoid Unless Necessary - -**Core WordPress integration:** -- `plugin-loader.php` -- `sureforms.php` / `sureforms-pro.php` - -**Database base class:** -- `inc/database/base.php` (inherited by all tables) - -**Critical security:** -- Nonce verification logic -- Capability checks -- Encryption (Pro): `inc/pro/native-integrations/encryption.php` - ---- - -## Common Patterns - -### Adding a New Field Type - -1. **Create block:** - ``` - inc/blocks/my-field/block.php - src/blocks/my-field/ - ``` - -2. **Register block:** - ```php - // inc/blocks/my-field/block.php - namespace SRFM\Inc\Blocks\My_Field; - - class Block extends \SRFM\Inc\Blocks\Base { - public static function register() { - register_block_type( - 'sureforms/my-field', - [ - 'render_callback' => [__CLASS__, 'render'], - 'attributes' => [/* schema */], - ] - ); - } - - public static function render($attributes) { - // Return HTML - } - } - ``` - -3. **Add to block registry:** - ```php - // inc/blocks/register.php - My_Field\Block::register(); - ``` - -4. **Add sanitization:** - ```php - // inc/helper.php - public static function sanitize_by_field_type($type, $value) { - switch ($type) { - case 'my-field': - return sanitize_text_field($value); - } - } - ``` - -### Adding a REST Endpoint - -```php -// inc/rest-api.php or new file -public function register_routes() { - register_rest_route( - 'sureforms/v1', - '/my-endpoint', - [ - 'methods' => 'POST', - 'callback' => [$this, 'handle_request'], - 'permission_callback' => [$this, 'check_permissions'], - ] - ); -} - -public function check_permissions() { - // CRITICAL: Always verify permissions - return current_user_can('manage_options'); -} - -public function handle_request($request) { - // CRITICAL: Always verify nonce - $nonce = $request->get_header('X-WP-Nonce'); - if (!wp_verify_nonce($nonce, 'wp_rest')) { - return new \WP_Error('invalid_nonce', 'Invalid nonce', ['status' => 403]); - } - - // Sanitize inputs - $data = $request->get_params(); - $clean_data = Helper::sanitize_array_recursively($data); - - // Process... - - return ['success' => true]; -} -``` - -### Adding an Integration (Pro) - -1. **Create integration folder:** - ``` - inc/pro/native-integrations/integrations/myservice/ - ├── config.json - └── actions/ - └── send-data.php - ``` - -2. **config.json:** - ```json - { - "name": "MyService", - "slug": "myservice", - "auth_method": "api_key", - "api_endpoint": "https://api.myservice.com/v1/", - "actions": [ - { - "name": "Send Data", - "slug": "send-data", - "endpoint": "contacts" - } - ] - } - ``` - -3. **actions/send-data.php:** - ```php - namespace SRFM_PRO\Inc\Pro\Native_Integrations\Integrations\Myservice\Actions; - - class Send_Data { - public function execute($integration_data, $form_data) { - $api_key = $integration_data['api_key']; - $response = wp_remote_post( - 'https://api.myservice.com/v1/contacts', - [ - 'headers' => ['Authorization' => 'Bearer ' . $api_key], - 'body' => json_encode($form_data), - ] - ); - - if (is_wp_error($response)) { - return ['success' => false, 'error' => $response->get_error_message()]; - } - - return ['success' => true, 'response' => wp_remote_retrieve_body($response)]; - } - } - ``` - ---- - -## Security Patterns - -### Input Sanitization - -```php -// Text -$name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); - -// Email -$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ); - -// Number -$id = absint( $_POST['id'] ?? 0 ); - -// Array (recursive) -$data = Helper::sanitize_array_recursively( $_POST['data'] ?? [] ); - -// Textarea -$message = Helper::sanitize_textarea( wp_unslash( $_POST['message'] ?? '' ) ); -``` - -### Output Escaping - -```php -// HTML content -echo esc_html( $user_name ); - -// Attributes -echo '
'; - -// URLs -echo ''; - -// Rich text (allows safe HTML) -echo wp_kses_post( $content ); -``` - -### Database Queries - -```php -global $wpdb; - -// ✅ Good - Prepared statement -$results = $wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}sureforms_entries WHERE form_id = %d AND status = %s", - $form_id, - $status - ) -); - -// ❌ Bad - SQL injection risk -$results = $wpdb->query("DELETE FROM {$wpdb->prefix}sureforms_entries WHERE id = $_POST[id]"); -``` - ---- - -## Testing Your Changes - -### Before Committing - -```bash -# 1. Lint PHP -composer lint - -# 2. Run PHP tests -composer test - -# 3. Lint JavaScript -npm run lint-js - -# 4. Run JS tests -npm run test:unit - -# 5. Build assets -npm run build - -# 6. Test in browser -# - Create form -# - Submit form -# - Check entries -# - Verify no console errors -``` - -### E2E Testing - -```bash -# Start environment -npm run play:up - -# Run tests -npm run play:run - -# Interactive mode (with browser) -npm run play:run:interactive -``` - ---- - -## Database Queries - Common Patterns - -### Get Entries - -```php -use SRFM\Inc\Database\Tables\Entries; - -// Get all for form -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='] - ], - 'orderby' => 'created_at', - 'order' => 'DESC', -]); - -// With pagination -$entries = Entries::get_all([ - 'where' => [['key' => 'form_id', 'value' => 123, 'compare' => '=']], - 'limit' => 20, - 'offset' => 40 // Page 3 (20 per page) -]); - -// Count entries -$count = Entries::get_count([ - 'where' => [['key' => 'status', 'value' => 'published', 'compare' => '=']] -]); -``` - -### Get Payments - -```php -use SRFM\Inc\Database\Tables\Payments; - -// Get completed payments -$payments = Payments::get_all([ - 'where' => [ - ['key' => 'status', 'value' => 'completed', 'compare' => '='] - ] -]); - -// Get payment by transaction ID -$payment = Payments::get_by_transaction_id('ch_xxxxx', 'stripe'); -``` - ---- - -## Hooks - When to Use - -### Modify Data Before Save - -```php -// Filter entry data before save -add_filter('sureforms_submit_form_data', function($data, $form_id) { - // Add custom field - $data['custom_timestamp'] = current_time('mysql'); - return $data; -}, 10, 2); -``` - -### Trigger External Action - -```php -// Action after entry save -add_action('sureforms_after_entry_save', function($entry_id, $form_id, $data) { - // Send to external CRM - send_to_crm($data); -}, 10, 3); -``` - -### Modify Email - -```php -// Filter email template -add_filter('sureforms_email_template', function($html, $entry_id) { - // Add custom header - return '
...
' . $html; -}, 10, 2); -``` - ---- - -## Debugging Tips - -### Enable Debug Mode - -```php -// wp-config.php -define('WP_DEBUG', true); -define('WP_DEBUG_LOG', true); -define('WP_DEBUG_DISPLAY', false); -``` - -### Check Debug Log - -```bash -tail -f wp-content/debug.log -``` - -### JavaScript Console - -```javascript -// Check for errors -// Open browser DevTools → Console - -// SureForms global -console.log(window.sureforms); -``` - -### Database Queries - -```php -// Install Query Monitor plugin -// Shows all queries, slow queries, errors -``` - ---- - -## Common Pitfalls to Avoid - -1. **Don't bypass sanitization:** "It's internal, it's safe" → Still sanitize -2. **Don't skip nonce checks:** Every AJAX/REST endpoint needs verification -3. **Don't use `$_POST` directly:** Always sanitize first -4. **Don't concatenate SQL:** Use `$wpdb->prepare()` -5. **Don't echo user data raw:** Use `esc_html()`, `esc_attr()`, etc. -6. **Don't modify database schema without migration:** Breaks existing sites -7. **Don't remove existing hooks:** Other plugins may depend on them -8. **Don't hardcode `wp_` prefix:** Use `$wpdb->prefix` - ---- - -## Getting Help - -**Documentation:** -- This folder: All internal docs -- WordPress Codex: https://codex.wordpress.org/ -- React Docs: https://react.dev/ - -**Code Search:** -- Use Glob/Grep tools to find patterns -- Example: `grep -r "sanitize_text_field" inc/` → See how sanitization is used - -**Ask Before:** -- Breaking changes -- Database schema changes -- Removing existing functionality - ---- - -**Next:** [Troubleshooting](troubleshooting.md) - Common problems and solutions diff --git a/internal-docs/apis.md b/internal-docs/apis.md deleted file mode 100644 index 0a7a16ec7..000000000 --- a/internal-docs/apis.md +++ /dev/null @@ -1,418 +0,0 @@ -# APIs - -Complete API reference for SureForms Free & Pro. - ---- - -## REST API Endpoints - -### Free (`/sureforms/v1/`) - -**Form Management:** -```http -POST /sureforms/v1/generate-form -GET /sureforms/v1/forms -GET /sureforms/v1/forms/{id} -DELETE /sureforms/v1/forms/{id} -``` - -**Form Submission:** -```http -POST /sureforms/v1/submit-form -``` -**Body:** `{ form_id, nonce, field_data... }` -**Response:** `{ success, message, entry_id }` - -**Entries:** -```http -GET /sureforms/v1/entries/list -GET /sureforms/v1/entries/{id} -DELETE /sureforms/v1/entries/{id} -POST /sureforms/v1/entries/export -``` - -**Payments:** -```http -GET /sureforms/v1/payments/list -GET /sureforms/v1/payments/{id} -``` - -**Stripe Webhooks:** -```http -POST /sureforms/webhook_test # Stripe webhooks -POST /sureforms/webhook_live -``` - -**Settings:** -```http -GET /sureforms/v1/settings -POST /sureforms/v1/settings/update -``` - -### Pro (`/sureforms-pro/v1/`) - -**OAuth:** -```http -POST /sureforms-pro/v1/oauth/authorize -POST /sureforms-pro/v1/oauth/callback -``` - -**PayPal Webhooks:** -```http -POST /sureforms-pro/paypal-test-webhook -POST /sureforms-pro/paypal-live-webhook -``` - -**User Registration:** -```http -POST /sureforms-pro/v1/login -POST /sureforms-pro/v1/lost-password -POST /sureforms-pro/v1/reset-password -``` - ---- - -## AJAX Handlers - -### Free (30 handlers) - -**Admin:** -```php -'srfm_admin_action' # Generic admin handler -'srfm_get_forms' # Fetch forms list -'srfm_delete_form' # Delete form -'srfm_duplicate_form' # Duplicate form -'srfm_export_entries' # Export CSV -``` - -**Payments (Public):** -```php -'wp_ajax_srfm_create_payment_intent' # Stripe payment -'wp_ajax_nopriv_srfm_create_payment_intent' -'wp_ajax_srfm_create_subscription_intent' # Stripe subscription -'wp_ajax_nopriv_srfm_create_subscription_intent' -``` - -**Admin Stripe:** -```php -'srfm_stripe_cancel_subscription' # Cancel subscription -'srfm_stripe_pause_subscription' # Pause subscription -``` - -**Admin Payments:** -```php -'srfm_create_payment_refund' # Create refund -``` - -**Form Validation (Public):** -```php -'wp_ajax_srfm_field_unique_validation' # Check unique fields -'wp_ajax_nopriv_srfm_field_unique_validation' -``` - -### Pro (22 handlers) - -**PayPal (Public):** -```php -'wp_ajax_srfm_pro_create_paypal_order' -'wp_ajax_nopriv_srfm_pro_create_paypal_order' -'wp_ajax_srfm_pro_create_paypal_subscription' -'wp_ajax_nopriv_srfm_pro_create_paypal_subscription' -``` - -**PayPal Admin:** -```php -'srfm_pro_paypal_cancel_subscription' -'srfm_pro_paypal_pause_subscription' -``` - -**PDF:** -```php -'srfm_pro_generate_pdf' # Generate PDF from entry -'srfm_pro_download_pdf' # Download PDF -``` - -**Licensing:** -```php -'srfm_pro_activate_license' -'srfm_pro_deactivate_license' -``` - -**Entries:** -```php -'sureforms_pro_entry_delete_file' # Delete uploaded file -``` - ---- - -## WordPress Hooks - -### Actions (Form Submission) - -```php -// Before entry save -do_action('sureforms_before_entry_save', $form_id, $entry_data); - -// After entry save -do_action('sureforms_after_entry_save', $entry_id, $form_id, $entry_data); - -// After payment completed -do_action('sureforms_payment_completed', $payment_id, $entry_id, $gateway); - -// After integration success -do_action('sureforms_integration_success', $integration_type, $response); - -// Before email send -do_action('sureforms_before_email_send', $to, $subject, $message); - -// After form submission (success) -do_action('sureforms_form_submitted', $entry_id, $form_id); -``` - -### Filters (Data Modification) - -```php -// Modify form data before save -$entry_data = apply_filters('sureforms_submit_form_data', $entry_data, $form_id); - -// Modify payment amount -$amount = apply_filters('sureforms_payment_amount', $amount, $form_id, $entry_data); - -// Modify email template -$html = apply_filters('sureforms_email_template', $html, $entry_id, $form_id); - -// Modify email subject -$subject = apply_filters('sureforms_email_subject', $subject, $form_id); - -// Modify email recipients -$to = apply_filters('sureforms_email_recipients', $to, $entry_id, $form_id); - -// Modify confirmation message -$message = apply_filters('sureforms_confirmation_message', $message, $entry_id); - -// Modify field validation -$is_valid = apply_filters('sureforms_validate_field', $is_valid, $field, $value); - -// Modify sanitized value -$value = apply_filters("sureforms_sanitize_{$field_type}", $value, $field); -``` - -### Integration Hooks (Pro) - -```php -// Before integration request -do_action('sureforms_pro_before_integration', $integration, $data); - -// After integration request -do_action('sureforms_pro_after_integration', $integration, $response); - -// Integration error -do_action('sureforms_pro_integration_error', $integration, $error); -``` - ---- - -## JavaScript APIs - -### Global Objects - -```javascript -// Form settings -window.sureforms = { - ajaxUrl: '/wp-admin/admin-ajax.php', - nonce: 'xxx', - formId: 123, - settings: {...} -}; - -// Block editor -window.sureformsBlocks = { - registerBlock: (name, settings) => {...}, - getBlock: (name) => {...} -}; -``` - -### Custom Events - -```javascript -// Form submitted -document.addEventListener('sureforms-form-submitted', (e) => { - console.log('Entry ID:', e.detail.entryId); -}); - -// Payment completed -document.addEventListener('sureforms-payment-completed', (e) => { - console.log('Payment ID:', e.detail.paymentId); -}); - -// Validation error -document.addEventListener('sureforms-validation-error', (e) => { - console.log('Field:', e.detail.field, 'Error:', e.detail.message); -}); -``` - ---- - -## Database APIs - -### Entry Operations - -```php -use SRFM\Inc\Database\Tables\Entries; - -// Get all entries for a form -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='] - ], - 'orderby' => 'created_at', - 'order' => 'DESC', - 'limit' => 20, - 'offset' => 0 -]); - -// Get single entry -$entry = Entries::get_by_id(456); - -// Insert entry -$entry_id = Entries::insert([ - 'form_id' => 123, - 'entry_data' => json_encode($data), - 'status' => 'published' -]); - -// Update entry -Entries::update(456, ['status' => 'trash']); - -// Delete entry -Entries::delete(456); -``` - -### Payment Operations - -```php -use SRFM\Inc\Database\Tables\Payments; - -// Get payments -$payments = Payments::get_all_main_payments([ - 'where' => [ - ['key' => 'status', 'value' => 'completed', 'compare' => '='] - ] -]); - -// Create payment -$payment_id = Payments::insert([ - 'entry_id' => 456, - 'transaction_id' => 'ch_xxx', - 'gateway' => 'stripe', - 'status' => 'pending', - 'total_amount' => 99.99, - 'currency' => 'USD' -]); - -// Update payment status -Payments::update($payment_id, ['status' => 'completed']); -``` - ---- - -## Helper APIs - -### Sanitization - -```php -use SRFM\Inc\Helper; - -// Sanitize by field type -$clean = Helper::sanitize_by_field_type('email', $value); -$clean = Helper::sanitize_by_field_type('textarea', $value); -$clean = Helper::sanitize_by_field_type('number', $value); - -// Email-specific -$email = Helper::sanitize_email_header($raw_email); - -// Recursive sanitization -$clean_array = Helper::sanitize_array_recursively($array); -``` - -### Validation - -```php -use SRFM\Inc\Field_Validation; - -// Validate field -$result = Field_Validation::validate_field($field_config, $value); -// Returns: ['valid' => true/false, 'message' => 'Error message'] - -// Validate entire form -$result = Field_Validation::validate_form_data($form_id, $data); -``` - -### Payment Helpers - -```php -use SRFM\Inc\Payments\Payment_Helper; - -// Validate amount against form config -$result = Payment_Helper::validate_payment_amount($amount, $currency, $form_id, $block_id); - -// Get currency -$currency = Payment_Helper::get_currency(); -``` - ---- - -## Encryption API (Pro) - -```php -use SRFM_PRO\Inc\Pro\Native_Integrations\Encryption; - -$enc = new Encryption(); - -// Encrypt sensitive data -$encrypted = $enc->encrypt('oauth_token_12345'); - -// Decrypt -$decrypted = $enc->decrypt($encrypted); - -// Storage: wp_sureforms_integrations.data column -``` - -**Algorithm:** AES-256-CTR -**Key:** `SRFM_ENCRYPTION_KEY` constant or `LOGGED_IN_KEY` - ---- - -## Integration Workflow API (Pro) - -```php -use SRFM_PRO\Inc\Pro\Native_Integrations\Services\Workflow_Processor; - -// Execute integration workflow -$processor = new Workflow_Processor(); -$result = $processor->execute_workflow($integration_config, $entry_data); - -// Returns: ['success' => true/false, 'response' => API response] -``` - ---- - -## Rate Limiting - -**Note:** No built-in rate limiting. Implementations should add: - -```php -// Example pattern (not in core) -$key = 'form_submit_' . $user_ip; -$attempts = get_transient($key) ?: 0; - -if ($attempts >= 5) { - wp_send_json_error('Too many submissions', 429); -} - -set_transient($key, $attempts + 1, 15 * MINUTE_IN_SECONDS); -``` - ---- - -**Next:** [Coding Standards](coding-standards.md) - PHP/JS conventions diff --git a/internal-docs/architecture.md b/internal-docs/architecture.md deleted file mode 100644 index 9e59e0b81..000000000 --- a/internal-docs/architecture.md +++ /dev/null @@ -1,329 +0,0 @@ -# Architecture - -**Version:** 2.5.0 - ---- - -## System Overview - -SureForms is a WordPress form builder with a **Free + Pro** plugin architecture: - -``` -WordPress Site -├── SureForms Free (Required) -│ ├── Core form functionality -│ ├── Stripe payments -│ ├── AI form builder -│ └── Entry management -└── SureForms Pro (Optional Extension) - ├── Advanced features (conditional logic, multi-step) - ├── PayPal payments - ├── User registration/login - └── 24+ native integrations -``` - ---- - -## High-Level Architecture - -``` -┌─────────────────────────────────────────────────────────┐ -│ WordPress Admin │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ React Admin UI (src/) │ │ -│ │ - Form Builder (Gutenberg Blocks) │ │ -│ │ - Entry Dashboard │ │ -│ │ - Settings Panels │ │ -│ └──────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - ↕ -┌─────────────────────────────────────────────────────────┐ -│ REST API Layer │ -│ - /sureforms/v1/* (Free) │ -│ - /sureforms-pro/v1/* (Pro) │ -│ - AJAX handlers (wp_ajax_*) │ -└─────────────────────────────────────────────────────────┘ - ↕ -┌─────────────────────────────────────────────────────────┐ -│ Business Logic (inc/) │ -│ ┌──────────┬──────────┬──────────┬──────────────────┐ │ -│ │ Forms │ Payments │ AI │ Integrations │ │ -│ │ Submit │ Stripe │ Builder │ (Pro: 24+) │ │ -│ │ Validate │ PayPal │ Field │ Mailchimp, etc │ │ -│ │ Store │ Webhooks │ Mapping │ │ │ -│ └──────────┴──────────┴──────────┴──────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - ↕ -┌─────────────────────────────────────────────────────────┐ -│ Data Layer │ -│ ┌──────────────┬────────────────┬──────────────────┐ │ -│ │ WP Options │ Custom Tables │ Encrypted Data │ │ -│ │ Form configs │ Entries │ OAuth tokens │ │ -│ │ Settings │ Payments │ API keys │ │ -│ └──────────────┴────────────────┴──────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - ↕ -┌─────────────────────────────────────────────────────────┐ -│ Frontend Rendering │ -│ - Gutenberg Blocks (PHP + JS) │ -│ - Form submission (AJAX) │ -│ - reCAPTCHA/Turnstile │ -│ - Real-time validation │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## Component Breakdown - -### 1. Block System (Gutenberg) - -**Free Blocks (16):** -- Input types: Text, Email, Number, Phone, URL, Textarea, Dropdown -- Special: Address, Checkbox, Multiple Choice, GDPR, Payment -- Layout: Separator, Heading, Image, Icon, Custom Button - -**Pro Blocks (8):** -- Input: Date Picker, Time Picker, Rating, Slider, Hidden, Password -- Advanced: File Upload, HTML, Page Break (multi-step) -- User Registration: Login, Register, Lost Password, Reset Password - -**Location:** `inc/blocks/*/block.php` and `src/blocks/*/` - -### 2. Form Submission Pipeline - -``` -User Submits Form - ↓ -1. Nonce Verification (inc/form-submit.php:91-95) - ↓ -2. Field Validation (inc/field-validation.php) - - Type validation (email, URL, number) - - Required fields - - Min/max constraints - - Custom regex - ↓ -3. Sanitization (inc/helper.php:258-274) - - Per-field type sanitization - - XSS prevention (wp_kses_post) - - SQL injection prevention - ↓ -4. CAPTCHA Validation (inc/form-submit.php:317-386) - - reCAPTCHA v2/v3 - - Cloudflare Turnstile - - hCaptcha - ↓ -5. Business Logic (Pro: Conditional Logic) - ↓ -6. Payment Processing (if enabled) - - Stripe: Create PaymentIntent - - PayPal: Create Order - ↓ -7. Entry Storage (inc/database/tables/entries.php) - ↓ -8. Integrations (Pro: Send to external services) - ↓ -9. Email Notifications (inc/email/email-template.php) - ↓ -10. Confirmation/Redirect -``` - -### 3. Payment Processing - -**Stripe (Free):** -- Location: `inc/payments/stripe/` -- Flow: - 1. Frontend creates PaymentIntent (AJAX) - 2. User completes payment on Stripe - 3. Webhook updates payment status - 4. Entry marked as paid - -**PayPal (Pro):** -- Location: `inc/business/payments/pay-pal/` -- Flow: - 1. Frontend creates Order via API - 2. User redirected to PayPal - 3. Webhook confirms payment - 4. Entry status updated - -**Database:** `wp_sureforms_payments` table - -### 4. AI Form Builder - -**Location:** `inc/ai-form-builder/` - -**Flow:** -``` -User Prompt ("Create a contact form") - ↓ -AI Middleware (SRFM_AI_MIDDLEWARE) - ↓ -Field Mapping (field-mapping.php) - ↓ -Gutenberg Block Generation - ↓ -Form Preview -``` - -**API:** `https://credits.startertemplates.com/sureforms/` - -### 5. Native Integrations (Pro) - -**Location:** `inc/pro/native-integrations/` - -**Supported Services (24+):** -- Email: Mailchimp, Brevo, Constant Contact -- CRM: HubSpot, Salesforce, Zoho -- Messaging: Telegram, Slack, Discord -- Marketing: FluentCRM, MailerPress -- Booking: LatePoint -- Automation: Zapier, OttoKit - -**Architecture:** -``` -Generic Provider Base Class - ↓ -Integration-Specific Classes -(inc/pro/native-integrations/integrations/*/actions/) - ↓ -OAuth Handler (oauth-handler.php) - ↓ -Encrypted Storage (encryption.php - AES-256-CTR) - ↓ -Workflow Processor (services/workflow-processor.php) -``` - -### 6. Database Schema - -**Custom Tables:** - -```sql --- Entries -wp_sureforms_entries -- id (INT, PK) -- form_id (INT) -- entry_data (LONGTEXT, JSON) -- created_at (DATETIME) -- updated_at (DATETIME) -- status (VARCHAR - 'published', 'trash') -- user_agent (TEXT) -- ip_address (VARCHAR) - --- Payments -wp_sureforms_payments -- id (INT, PK) -- entry_id (INT, FK) -- transaction_id (VARCHAR) -- gateway (ENUM 'stripe', 'paypal') -- status (ENUM 'pending', 'completed', 'failed', 'refunded') -- total_amount (DECIMAL) -- refunded_amount (DECIMAL) -- currency (VARCHAR) - --- Integrations (Pro) -wp_sureforms_integrations -- id (INT, PK) -- type (VARCHAR - 'mailchimp', 'hubspot', etc.) -- data (LONGTEXT, encrypted JSON) -- status (ENUM 'active', 'inactive') - --- Save & Resume (Pro) -wp_sureforms_save_resume -- id (INT, PK) -- form_id (INT) -- token (VARCHAR, unique) -- form_data (LONGTEXT, JSON) -- expires_at (DATETIME) -``` - ---- - -## Security Architecture - -**Input Validation:** -- Nonce verification on all AJAX/REST requests -- Sanitization via `sanitize_*` functions -- Type casting (absint, floatval) - -**Output Escaping:** -- `esc_html()`, `esc_attr()`, `esc_url()` throughout -- `wp_kses_post()` for rich text - -**SQL Security:** -- `$wpdb->prepare()` for all queries -- No direct SQL concatenation - -**Payment Security:** -- Webhook signature verification (Stripe, PayPal) -- Server-side amount validation -- PCI-DSS compliant (payments handled by Stripe/PayPal) - -**Encryption (Pro):** -- AES-256-CTR for OAuth tokens -- Key derivation: `SRFM_ENCRYPTION_KEY` constant or `LOGGED_IN_KEY` -- Storage: `wp_sureforms_integrations.data` (encrypted JSON) - ---- - -## Performance Optimizations - -1. **Lazy Loading:** React components loaded on-demand -2. **Asset Minification:** Grunt uglifies JS/CSS -3. **Database Indexing:** Indexes on `form_id`, `entry_id`, `status` -4. **Caching:** Transients for integration API calls -5. **Query Optimization:** `SELECT` only needed columns - ---- - -## Extensibility - -**Hooks (Filters):** -- `sureforms_submit_form_data` - Modify data before save -- `sureforms_email_template` - Customize email HTML -- `sureforms_payment_amount` - Modify payment amount - -**Hooks (Actions):** -- `sureforms_after_entry_save` - Trigger after entry created -- `sureforms_payment_completed` - After successful payment -- `sureforms_integration_success` - After integration sends data - -**Developer APIs:** -- REST: `/sureforms/v1/*` -- PHP: `SRFM\Inc\Helper` class -- JavaScript: `window.sureforms.*` globals - ---- - -## Technology Stack - -| Layer | Technologies | -|-------|-------------| -| Frontend (Admin) | React 18, @wordpress/components, TanStack Query | -| Frontend (Public) | Vanilla JS, DOMPurify, intl-tel-input | -| Backend | PHP 7.4+, WordPress 6.4+ | -| Build | webpack 5, Babel, Sass, Grunt | -| Testing | PHPUnit, Jest, Playwright | -| Database | MySQL 5.7+, MariaDB 10.3+ | - ---- - -## Deployment Architecture - -``` -Developer Machine - ↓ -Git Commit → GitHub Actions CI - ↓ -Automated Tests (PHPUnit, Playwright) - ↓ -Build Assets (npm run build) - ↓ -Free: WordPress.org SVN -Pro: Licensing Server (ZIP) - ↓ -End User WordPress Sites (300,000+) -``` - ---- - -**Next:** [Codebase Map](codebase-map.md) for detailed folder structure diff --git a/internal-docs/codebase-map.md b/internal-docs/codebase-map.md deleted file mode 100644 index 9d85000c2..000000000 --- a/internal-docs/codebase-map.md +++ /dev/null @@ -1,303 +0,0 @@ -# Codebase Map - -**Version:** 2.5.0 - -Complete folder-by-folder guide to both plugins. - ---- - -## SureForms Free (`sureforms/`) - -### Root Files - -| File | Purpose | -|------|---------| -| `sureforms.php` | Main plugin file, defines constants, loads `plugin-loader.php` | -| `plugin-loader.php` | Bootstraps plugin, registers hooks, loads dependencies | -| `composer.json` | PHP dependencies (nps-survey, bsf-analytics, astra-notices) | -| `package.json` | JS dependencies, build scripts | -| `Gruntfile.js` | Grunt tasks (minify, compress, release) | - -### `admin/` - WordPress Admin UI - -| File | Purpose | -|------|---------| -| `admin.php` | Admin page registration, AJAX handlers for admin actions | -| `analytics.php` | Usage analytics, event tracking | -| `notice-manager.php` | Admin notices, dismissible alerts | - -### `inc/` - Core PHP Logic - -#### `inc/blocks/` - Gutenberg Block Definitions (PHP) - -Each block has a `block.php` with: -- Block registration (`register_block_type`) -- Server-side rendering callback -- Attribute schema -- Enqueue scripts/styles - -**Blocks (16):** -- `address/`, `checkbox/`, `dropdown/`, `email/`, `gdpr/`, `inlinebutton/` -- `input/`, `multichoice/`, `number/`, `payment/`, `phone/`, `sform/`, `textarea/`, `url/` -- `base.php` - Base block class - -#### `inc/fields/` - Field Markup Generation - -Mirrors `blocks/` - generates HTML markup for each field type. - -**Pattern:** -```php -*-markup.php files: -- render_field() method -- Outputs escaped HTML -- Handles field attributes, validation states -``` - -#### `inc/payments/` - Payment Processing - -**Core Files:** -| File | Purpose | -|------|---------| -| `front-end.php` | Public AJAX handlers (`srfm_create_payment_intent`, `srfm_create_subscription_intent`) | -| `payment-helper.php` | Utility functions, amount validation, currency formatting | - -**`payments/stripe/`:** -| File | Purpose | -|------|---------| -| `stripe-webhook.php` | Webhook handler (charge, refund, subscription events) | -| `stripe-helper.php` | Stripe API wrapper, PaymentIntent creation | -| `admin-stripe-handler.php` | Admin AJAX for subscription management | -| `payments-settings.php` | Stripe connection settings, API keys | - -**`payments/admin/`:** -| File | Purpose | -|------|---------| -| `admin-handler.php` | Admin AJAX for refunds, payment management | - -#### `inc/ai-form-builder/` - AI Form Generation - -| File | Purpose | -|------|---------| -| `ai-form-builder.php` | Main AI form builder class, API integration | -| `ai-auth.php` | Middleware authentication | -| `ai-helper.php` | Helper functions, prompt processing | -| `field-mapping.php` | Maps AI response to Gutenberg blocks | - -**API:** `SRFM_AI_MIDDLEWARE` - `https://credits.startertemplates.com/sureforms/` - -#### `inc/database/` - Custom Database Tables - -| File | Purpose | -|------|---------| -| `base.php` | Abstract base class for all table classes (CRUD operations) | -| `register.php` | Table registration, schema creation | -| `tables/entries.php` | `wp_sureforms_entries` table (1,300+ lines) | -| `tables/payments.php` | `wp_sureforms_payments` table (1,200+ lines) | - -**Key Methods:** -- `get_all()`, `get_by_id()`, `insert()`, `update()`, `delete()` -- `get_all_main_payments()` - Payment queries with filtering - -#### Other `inc/` Files - -| File | Purpose | Lines | -|------|---------|-------| -| `form-submit.php` | Form submission handler (AJAX + REST) | 1,280 | -| `helper.php` | Utility functions (sanitization, escaping, validation) | 1,500+ | -| `field-validation.php` | Server-side field validation | 800+ | -| `rest-api.php` | REST API registration (`/sureforms/v1/*`) | 800 | -| `entries.php` | Entry management (list, view, export) | 1,000+ | -| `email/email-template.php` | Email notification templates | 400 | -| `frontend-assets.php` | Enqueue public scripts/styles | 300 | - -### `src/` - React Admin UI - -**Structure:** -``` -src/ -├── admin/ # Main admin app -│ ├── components/ # Reusable React components -│ ├── pages/ # Page-level components -│ └── index.js # Entry point -├── blocks/ # Block editor components -│ ├── address/ # Block-specific React -│ ├── email/ -│ └── ... -└── common/ # Shared utilities - ├── api.js # API client (wp.apiFetch) - └── utils.js # Helper functions -``` - -**Build:** webpack via `@wordpress/scripts` - ---- - -## SureForms Pro (`sureforms-pro/`) - -### Root Files - -| File | Purpose | -|------|---------| -| `sureforms-pro.php` | Main plugin file, requires Free plugin | -| `plugin-loader.php` | Bootstraps Pro features | - -### `inc/business/` - Pro Business Features - -#### `inc/business/payments/pay-pal/` - PayPal Integration - -| File | Purpose | Lines | -|------|---------|-------| -| `webhook-listener.php` | PayPal webhook handler (orders, refunds, subscriptions) | 1,173 | -| `frontend.php` | Public AJAX for PayPal orders, subscriptions | 1,100 | -| `api-payments.php` | PayPal API wrapper (create order, capture, refund) | 178 | -| `helper.php` | PayPal utilities, amount formatting | 520 | -| `settings.php` | PayPal connection settings | AJAX | - -#### `inc/business/user-registration/` - Login & Registration - -| File | Purpose | Lines | -|------|---------|-------| -| `init.php` | REST API registration, login/password endpoints | 600+ | -| `processor.php` | Registration processing, `wp_insert_user()` | 1,000+ | -| `login/block.php` | Login block definition | | -| `register/block.php` | Registration block | | -| `lost-password/block.php` | Lost password block | | -| `reset-password/block.php` | Reset password block | | - -**⚠️ Security Note:** Registration processor creates WordPress users from form submissions. - -#### `inc/business/pdf/` - PDF Generation - -| File | Purpose | -|------|---------| -| `pdf.php` | PDF generation API, AJAX handlers | -| `document.php` | PDF document builder (uses TCPDF or similar) | -| `utils.php` | PDF utilities | - -#### `inc/business/custom-app/` - Custom Application Builder - -| File | Purpose | -|------|---------| -| `load.php` | Custom post type forms, standalone applications | -| `utils.php` | App utilities | - -#### `inc/business/custom-post-type/` - Custom Post Type Integration - -Allows forms to create/update custom post types. - -### `inc/pro/native-integrations/` - 24+ Service Integrations - -**Core:** -| File | Purpose | Lines | -|------|---------|-------| -| `native-integrations.php` | Main integration manager | | -| `encryption.php` | AES-256-CTR encryption for OAuth tokens | 200 | -| `oauth-handler.php` | OAuth authorization flow | 400+ | -| `generic-provider.php` | Base provider class | | -| `integration-provider.php` | Provider interface | | - -**`inc/pro/native-integrations/integrations/`:** - -Each integration has its own folder: -``` -mailchimp/ -├── config.json # API endpoints, auth method -└── actions/ - ├── add-contact.php # Action: Add subscriber - └── remove-contact.php - -hubspot/ -├── config.json -└── actions/ - └── create-contact.php - -...24+ integrations -``` - -**Supported:** -- Email: Mailchimp, Brevo, Constant Contact -- CRM: HubSpot, Salesforce, Zoho -- Messaging: Telegram, Slack, Discord -- WP: FluentCRM, MailerPress, MailPoet -- Booking: LatePoint -- Automation: Zapier, OttoKit - -**Encryption:** OAuth tokens encrypted via `encryption.php` before storage in `wp_sureforms_integrations`. - -### `inc/pro/database/tables/` - Pro Database Tables - -| File | Purpose | -|------|---------| -| `integrations.php` | `wp_sureforms_integrations` table | -| `save-resume.php` | `wp_sureforms_save_resume` table (draft forms) | - -### `inc/extensions/` - Pro Extensions - -| File | Purpose | -|------|---------| -| `conditional-logic.php` | Show/hide fields based on conditions | -| `conditional-emails.php` | Conditional email notifications | -| `conditional-confirmations.php` | Conditional redirects | -| `field-validation.php` | Extended validation (file uploads, MIME types) | -| `entries-management.php` | Enhanced entry features (edit, delete files) | -| `page-break.php` | Multi-step form logic | - -### `inc/blocks/` - Pro Blocks (8) - -- `date-picker/`, `time-picker/`, `rating/`, `slider/` -- `upload/`, `hidden/`, `html/`, `page-break/` - ---- - -## Key Patterns & Conventions - -### Naming Conventions - -**Functions:** -- Public: `srfm_function_name()` -- Internal: `_srfm_internal_function()` - -**Classes:** -- Namespaced: `SRFM\Inc\ClassName` -- Pro: `SRFM_PRO\Inc\ClassName` - -**Hooks:** -- Actions: `sureforms_action_name` -- Filters: `sureforms_filter_name` - -**Database:** -- Tables: `wp_sureforms_table_name` -- Options: `sureforms_option_name` - -### File Organization - -``` -feature/ -├── block.php # Block registration -├── feature-markup.php # HTML generation -├── api-handler.php # AJAX/REST handlers -└── helper.php # Utilities -``` - -### REST API Convention - -**Free:** `/sureforms/v1/endpoint` -**Pro:** `/sureforms-pro/v1/endpoint` - ---- - -## Where to Find Things - -**Need to:** -- **Add a new field type?** → `inc/blocks/` + `inc/fields/` + `src/blocks/` -- **Modify form submission?** → `inc/form-submit.php` -- **Change email template?** → `inc/email/email-template.php` -- **Add payment gateway?** → `inc/payments/` (copy Stripe/PayPal pattern) -- **Add integration?** → `inc/pro/native-integrations/integrations/new-service/` -- **Debug entries?** → `inc/database/tables/entries.php` -- **Fix validation?** → `inc/field-validation.php` -- **Customize admin UI?** → `src/admin/` - ---- - -**Next:** [APIs](apis.md) - REST endpoints, AJAX handlers, hooks diff --git a/internal-docs/coding-standards.md b/internal-docs/coding-standards.md deleted file mode 100644 index 0b746674c..000000000 --- a/internal-docs/coding-standards.md +++ /dev/null @@ -1,312 +0,0 @@ -# Coding Standards - -**Version:** 2.5.0 - ---- - -## PHP Standards - -**WordPress Coding Standards:** https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/ - -### Code Style - -- **PSR-12 inspired** but follows WordPress conventions -- **Tabs for indentation** (not spaces) -- **Yoda conditions:** `if ( 'value' === $variable )` -- **Braces:** Required for all control structures - -```php -// ✅ Good -if ( $condition ) { - do_something(); -} - -// ❌ Bad -if ($condition) do_something(); -``` - -### Naming Conventions - -**Functions:** -```php -srfm_public_function() // Public functions -_srfm_private_function() // Internal (leading underscore) -``` - -**Classes:** -```php -namespace SRFM\Inc; -class Form_Submit { } // Underscores in class names - -namespace SRFM_PRO\Inc\Business; -class PayPal_Helper { } -``` - -**Variables:** -```php -$snake_case_variable = 'value'; // Lowercase with underscores -``` - -**Constants:** -```php -define( 'SRFM_CONSTANT', 'value' ); -``` - -### Security - -**Always:** -- Sanitize input: `sanitize_text_field()`, `absint()`, `sanitize_email()` -- Validate: Check types, ranges, allowed values -- Escape output: `esc_html()`, `esc_attr()`, `esc_url()` -- Use nonces: `wp_verify_nonce()` for AJAX/forms -- Use `$wpdb->prepare()` for SQL queries -- Check capabilities: `current_user_can()` - -**Example:** -```php -// Input -$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ); - -// Database -$results = $wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}sureforms_entries WHERE form_id = %d", - $form_id - ) -); - -// Output -echo '
' . esc_html( $user_name ) . '
'; -``` - -### Documentation - -**PHPDoc blocks:** -```php -/** - * Short description. - * - * Longer description if needed. - * - * @since 2.5.0 - * - * @param int $form_id Form ID. - * @param array $data Entry data. - * @return int|false Entry ID on success, false on failure. - */ -function srfm_create_entry( $form_id, $data ) { - // Implementation -} -``` - ---- - -## JavaScript Standards - -**WordPress JavaScript Coding Standards:** https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/ - -### Code Style (ESLint) - -**Config:** `@wordpress/eslint-plugin` - -```javascript -// ✅ Good - ES6+, const/let, arrow functions -const handleSubmit = (event) => { - event.preventDefault(); - const formData = new FormData(event.target); - // ... -}; - -// ❌ Bad - var, function expressions -var handleSubmit = function(event) { - event.preventDefault(); -}; -``` - -### React Conventions - -**Functional components with hooks:** -```jsx -import { useState, useEffect } from '@wordpress/element'; - -function FormBuilder({ formId }) { - const [fields, setFields] = useState([]); - - useEffect(() => { - fetchFields(formId).then(setFields); - }, [formId]); - - return ( -
- {fields.map(field => )} -
- ); -} -``` - -**Naming:** -- Components: `PascalCase` -- Hooks: `useCamelCase` -- Functions: `camelCase` -- Constants: `UPPER_SNAKE_CASE` - ---- - -## CSS/SCSS Standards - -**BEM-inspired naming:** -```scss -.srfm-form { - &__field { - // Field styles - } - - &__field--error { - // Error state - } -} -``` - -**Prefix:** All classes start with `srfm-` to avoid conflicts. - ---- - -## Database Standards - -### Table Names - -```php -$wpdb->prefix . 'sureforms_entries' -$wpdb->prefix . 'sureforms_payments' -``` - -**Always use `$wpdb->prefix`**, never hardcode `wp_`. - -### Queries - -**Always use prepared statements:** -```php -// ✅ Good -$wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}sureforms_entries WHERE id = %d", - $entry_id - ) -); - -// ❌ Bad -$wpdb->query( "DELETE FROM wp_sureforms_entries WHERE id = $entry_id" ); -``` - ---- - -## Git Commit Messages - -**Format:** -``` -type(scope): brief description - -Longer explanation if needed. - -Co-Authored-By: Name -``` - -**Types:** -- `feat`: New feature -- `fix`: Bug fix -- `docs`: Documentation -- `style`: Formatting, no code change -- `refactor`: Code restructuring -- `test`: Adding tests -- `chore`: Build/tooling - -**Example:** -``` -feat(payments): add PayPal subscription support - -- Implement PayPal billing agreement API -- Add webhook handler for subscription events -- Update admin UI for PayPal subscriptions - -Co-Authored-By: Claude Sonnet 4.5 -``` - ---- - -## File Organization - -**One class per file:** -``` -inc/payments/stripe/stripe-helper.php → class Stripe_Helper -``` - -**Group related functionality:** -``` -inc/payments/ -├── payment-helper.php # Shared utilities -├── front-end.php # Public AJAX handlers -├── stripe/ -│ ├── stripe-helper.php -│ ├── stripe-webhook.php -│ └── admin-stripe-handler.php -└── admin/ - └── admin-handler.php -``` - ---- - -## Testing Standards - -**PHPUnit:** -```php -class Test_Form_Submit extends WP_UnitTestCase { - public function test_form_submission_saves_entry() { - $entry_id = srfm_submit_form( $data ); - $this->assertIsInt( $entry_id ); - } -} -``` - -**Playwright:** -```javascript -test('form submission works', async ({ page }) => { - await page.goto('/test-form/'); - await page.fill('[name="email"]', 'test@example.com'); - await page.click('button[type="submit"]'); - await expect(page.locator('.success-message')).toBeVisible(); -}); -``` - ---- - -## Code Review Checklist - -Before submitting PR: - -- [ ] Code follows WordPress/project standards -- [ ] All inputs sanitized -- [ ] All outputs escaped -- [ ] SQL queries use `prepare()` -- [ ] Nonces verified for AJAX/forms -- [ ] Capabilities checked for admin actions -- [ ] PHPDoc blocks added -- [ ] No PHP/JS errors in console -- [ ] Tested in latest WordPress version -- [ ] Tests pass: `composer test && npm run test:unit` -- [ ] Linting passes: `composer lint && npm run lint-js` - ---- - -**Automated Checks:** - -```bash -# PHP linting -composer lint # PHP_CodeSniffer - -# JS linting -npm run lint-js # ESLint - -# Auto-fix -composer format # phpcbf -npm run lint-js:fix # ESLint --fix -``` diff --git a/internal-docs/faq.md b/internal-docs/faq.md deleted file mode 100644 index 8e174c45f..000000000 --- a/internal-docs/faq.md +++ /dev/null @@ -1,1003 +0,0 @@ -# Frequently Asked Questions - -**Version:** 2.5.0 - ---- - -## General Questions - -### What is SureForms? - -SureForms is an AI-powered WordPress form builder that uses Gutenberg blocks. - -**Key features:** -- AI form generation (describe in plain language, get complete form) -- Built-in payments (Stripe in Free, PayPal in Pro) -- Mobile-first design -- Native WordPress (no proprietary builder) -- Custom database tables (not post meta) - -**Target users:** Website owners, designers, developers who need forms without complexity. - ---- - -### What's the difference between Free and Pro? - -**Free (sureforms):** -- Unlimited forms & submissions -- All 15+ field types -- Stripe payments (one-time & subscriptions) -- Email notifications -- Spam protection (reCAPTCHA, Honeypot) -- Form analytics -- CSV export - -**Pro (sureforms-pro):** -- **All Free features, plus:** -- PayPal payments -- 24+ native integrations (Mailchimp, HubSpot, Salesforce, etc.) -- Conditional logic -- Multi-step & conversational forms -- User registration & login -- PDF generation -- File upload fields -- Priority support (< 24hr response) - -**Pricing:** -- Free: $0 (WordPress.org) -- Pro: $99-$299/year (3 sites to unlimited) - ---- - -### Can I use SureForms on multiple sites? - -**Free:** Yes, unlimited sites. - -**Pro:** Depends on license: -- Essential ($99/year): 3 sites -- Plus ($199/year): 20 sites -- Agency ($299/year): Unlimited sites - -**License activation:** -- Enter license key in SureForms → Settings → License -- Deactivate from one site to move to another -- All sites must be owned by you (not for client sites unless Agency plan) - ---- - -### Is SureForms GDPR compliant? - -**Yes.** SureForms provides tools for GDPR compliance: - -**Features:** -- GDPR checkbox field (explicit consent) -- Data export (users can request their data) -- Data deletion (users can request deletion) -- No third-party data sharing (unless you enable integrations) -- Privacy policy link support - -**What you must do:** -1. Add GDPR checkbox to forms collecting personal data -2. Link to your privacy policy -3. Honor data export/deletion requests -4. Configure data retention policies - -**Data storage:** -- Submissions stored in your WordPress database -- No data sent to SureForms servers (AI calls go through middleware but don't store data) -- Payment data stored encrypted (PCI-compliant) - ---- - -### Does SureForms work with my theme? - -**Yes.** SureForms is designed to work with any properly-coded WordPress theme. - -**Tested with:** -- Astra -- GeneratePress -- Kadence -- Neve -- Blocksy -- Twenty Twenty-Three/Four -- Page builder themes (Elementor, Divi, Beaver Builder) - -**If you experience styling conflicts:** -1. Check [troubleshooting guide](troubleshooting.md#form-breaks-after-theme-update) -2. Use SureForms → Settings → Custom CSS to override -3. Contact support with theme name - ---- - -### Can I use SureForms with page builders? - -**Yes.** SureForms works with: -- **Gutenberg** (native, best experience) -- **Elementor** (via shortcode or WordPress widget) -- **Divi** (via shortcode or code module) -- **Beaver Builder** (via WordPress widget) -- **Bricks** (via shortcode or WordPress element) - -**How to add:** - -**Elementor:** -1. Add "Shortcode" widget -2. Paste: `[sureforms id="123"]` -3. Replace 123 with your form ID - -**Divi:** -1. Add "Code" module -2. Paste shortcode -3. Save - -**Beaver Builder:** -1. Add "WordPress Widget" module -2. Select "SureForms" widget -3. Choose form - ---- - -## Technical Questions - -### What are the system requirements? - -**Minimum:** -- WordPress 6.4+ -- PHP 7.4+ -- MySQL 5.6+ or MariaDB 10.1+ -- HTTPS (for payments) - -**Recommended:** -- WordPress 6.6+ -- PHP 8.1+ -- MySQL 8.0+ or MariaDB 10.6+ -- 256MB PHP memory limit -- Object caching (Redis or Memcached) - -**Browser support:** -- Chrome 90+ -- Firefox 88+ -- Safari 14+ -- Edge 90+ - ---- - -### Does SureForms slow down my site? - -**No.** SureForms is optimized for performance: - -**Impact on page load:** -- Additional CSS: ~15KB (minified) -- Additional JS: ~25KB (minified, deferred) -- Total: ~40KB additional assets - -**Database queries:** -- Form render: 2-3 queries (cached) -- Form submission: 4-5 queries (optimized with indexes) -- No N+1 query issues - -**Performance features:** -- Lazy loading for non-critical assets -- Custom database tables (not post meta) -- Query optimization (indexed columns) -- Minified & concatenated assets -- CDN-friendly (versioned static files) - -**Benchmarks:** -- Lighthouse score: 95+ (with proper hosting) -- Time to Interactive: < 2 seconds on 3G -- Form submission latency: < 500ms - ---- - -### Can I export my data? - -**Yes.** Multiple export options: - -**Entries export:** -1. Go to SureForms → Entries -2. Select entries (or "Select All") -3. Click "Export to CSV" -4. Download file - -**Format:** CSV with all fields as columns - -**Form export:** -- Export form as JSON (Settings → Export) -- Import on another site (Settings → Import) - -**Database export:** -```bash -# Export all SureForms data -wp db export sureforms-backup.sql --tables=wp_sureforms_entries,wp_sureforms_payments,wp_sureforms_integrations -``` - ---- - -### How do I migrate forms to another site? - -**Method 1: Export/Import (Recommended)** - -**On old site:** -1. Go to SureForms → Forms -2. Hover over form → "Export" -3. Download JSON file - -**On new site:** -1. Install SureForms (same version) -2. Go to SureForms → Import -3. Upload JSON file -4. Map any dependencies (integrations, etc.) - -**Method 2: Database migration** - -```bash -# On old site -wp db export sureforms-data.sql --tables=wp_sureforms_entries,wp_sureforms_payments - -# Transfer file to new site - -# On new site -wp db import sureforms-data.sql - -# Update URLs in entries (if domain changed) -wp search-replace 'https://oldsite.com' 'https://newsite.com' wp_sureforms_entries -``` - ---- - -### Can I use SureForms offline / localhost? - -**Yes, with limitations:** - -**Works offline:** -- Form creation -- Form editing -- Local submissions (saved to database) - -**Requires internet:** -- AI form generation (external API) -- Payment processing (Stripe/PayPal API) -- Native integrations (Mailchimp, HubSpot, etc.) -- Google Fonts (if used) -- reCAPTCHA (if enabled) - -**Development setup:** -```bash -# Use Local by Flywheel or similar -# Or manual setup: -cd ~/Sites/my-wordpress-site -wp core download -wp core config --dbname=sureforms_dev --dbuser=root -wp core install --url=http://localhost:8080 --title="Dev Site" -wp plugin install sureforms --activate -``` - ---- - -## Payment Questions - -### Which payment gateways are supported? - -**Free plugin:** -- Stripe (one-time payments & subscriptions) - -**Pro plugin:** -- Stripe (one-time payments & subscriptions) -- PayPal (one-time payments & subscriptions) - -**Coming soon:** -- Razorpay (India) -- Mollie (Europe) - -**Not planned:** -- WooCommerce (use WooCommerce for full e-commerce) -- Square (low demand) -- Bitcoin/crypto (regulatory complexity) - ---- - -### How do I test payments before going live? - -**Stripe test mode:** - -1. Get test API keys: - - Go to https://dashboard.stripe.com/test/apikeys - - Copy "Publishable key" (pk_test_...) - - Copy "Secret key" (sk_test_...) - -2. In SureForms: - - Go to Settings → Payments → Stripe - - Enable "Test Mode" - - Paste test keys - - Save - -3. Test with card: - - Card number: `4242 4242 4242 4242` - - Expiry: Any future date (e.g., 12/28) - - CVC: Any 3 digits (e.g., 123) - -4. Verify in Stripe dashboard: - - Go to Payments → Test Mode - - See your test payment - -**PayPal sandbox (Pro):** - -1. Create sandbox account: - - Go to https://developer.paypal.com/ - - Sign in with PayPal account - - Create sandbox business & buyer accounts - -2. Get sandbox credentials: - - Apps & Credentials → Sandbox - - Copy Client ID & Secret - -3. In SureForms: - - Settings → Payments → PayPal - - Enable "Sandbox Mode" - - Paste credentials - - Save - -4. Test with sandbox buyer account - ---- - -### How are payment fees handled? - -**SureForms fees:** $0 (we charge nothing) - -**Gateway fees:** - -**Stripe:** -- Standard: 2.9% + $0.30 per transaction -- International cards: +1.5% -- Currency conversion: +1% -- (Varies by country, see Stripe pricing) - -**PayPal:** -- Standard: 2.9% + $0.30 per transaction -- International: +1.5% -- (Varies by country, see PayPal pricing) - -**Who pays fees:** -- Fees deducted from amount received -- Example: $100 charge → You receive ~$97 - -**Passing fees to customer:** -- Not built-in (would require payment gateway approval) -- Workaround: Add fee as separate amount field - ---- - -### Can I accept subscriptions? - -**Yes** (both Stripe and PayPal). - -**Setup:** - -1. Create form with Payment block -2. Payment block settings: - - Payment Type: ● Subscription - - Interval: Monthly/Yearly/Weekly - - Amount: [price] - -3. Customer pays once, charged automatically recurring - -**Management:** - -**Stripe:** -- View subscriptions: Stripe Dashboard → Subscriptions -- Cancel: Click subscription → "Cancel subscription" -- Refund: Not automatic, manual refund in Stripe - -**PayPal (Pro):** -- View subscriptions: PayPal Dashboard → Subscriptions -- Cancel: Click subscription → "Cancel" - -**Customer cancellation:** -- Customers can cancel in Stripe/PayPal customer portal -- You can provide link in confirmation email - ---- - -## AI & Form Generation Questions - -### How does AI form generation work? - -**Technical flow:** - -``` -User enters prompt ("job application form") - ↓ -SureForms sends to AI middleware - ↓ -Middleware calls GPT-4 API - ↓ -GPT-4 returns structured JSON (field types, labels, validations) - ↓ -Middleware sends back to SureForms - ↓ -SureForms creates Gutenberg blocks from JSON - ↓ -Form appears in editor, ready to customize -``` - -**What data is sent:** -- Your prompt ("create a contact form") -- Form context (if modifying existing form) -- No user data, no submissions, no PII - -**What data is stored:** -- None. AI responses are not logged. -- Form configuration stored in WordPress database only - -**Privacy:** -- AI calls routed through SureForms middleware (not direct to OpenAI) -- No data retention -- No training on your data - ---- - -### Can I use AI without internet? - -**No.** AI requires external API call to GPT-4. - -**Offline alternatives:** -- Create forms manually (no AI) -- Use templates (no AI needed) -- Import pre-made forms - ---- - -### What languages does AI support? - -**Prompts:** Any language GPT-4 supports (100+ languages) - -**Field labels:** Generated in same language as prompt - -**Example:** -- Prompt (Spanish): "formulario de contacto" -- Result: Fields with Spanish labels ("Nombre", "Correo electrónico", etc.) - -**Limitations:** -- Some languages better than others (English, Spanish, French, German excellent) -- Less common languages may have lower quality - ---- - -## Integration Questions (Pro) - -### What integrations are available? - -**24+ native integrations:** - -**Email Marketing:** -- Mailchimp -- Brevo (Sendinblue) -- ActiveCampaign -- ConvertKit -- MailerLite - -**CRM:** -- HubSpot -- Salesforce -- Zoho CRM -- Pipedrive - -**Communication:** -- Slack -- Telegram -- Discord (coming soon) - -**Productivity:** -- Google Sheets -- Airtable (coming soon) -- Notion (coming soon) - -**WordPress Plugins:** -- FluentCRM (native) -- OttoKit (SureTriggers) - -**Webhooks:** -- Custom webhooks (POST to any URL) - -**Not included:** -- Zapier (use webhooks instead) -- Make/Integromat (use webhooks) - ---- - -### How do I connect Mailchimp? - -**Setup (OAuth):** - -1. Go to SureForms → Settings → Integrations -2. Click "Mailchimp" → "Connect" -3. Redirected to Mailchimp → "Allow access" -4. Redirected back, connection confirmed - -**Use in form:** - -1. Edit form -2. Settings → Actions → "Add Action" -3. Select "Mailchimp" -4. Choose audience (list) -5. Map fields: - - Email field → Mailchimp EMAIL - - Name field → Mailchimp FNAME -6. Save - -**On form submission:** -- Contact added to Mailchimp list automatically -- Tags applied (if configured) -- Double opt-in email sent (if enabled in Mailchimp) - ---- - -### Can I send form data to custom webhook? - -**Yes** (Pro feature). - -**Setup:** - -1. Form settings → Actions → "Add Action" -2. Select "Webhook" -3. Configure: - - URL: `https://yourapi.com/endpoint` - - Method: POST (or GET, PUT) - - Headers: (optional, e.g., `Authorization: Bearer token`) -4. Save - -**Payload format:** - -```json -{ - "form_id": 123, - "entry_id": 456, - "fields": { - "email": "user@example.com", - "name": "John Doe", - "message": "Hello!" - }, - "meta": { - "submitted_at": "2026-02-12 10:30:00", - "user_ip": "192.168.1.1", - "user_agent": "Mozilla/5.0..." - } -} -``` - -**Retry logic:** -- Failed webhooks retried 3 times -- Exponential backoff (1s, 5s, 25s) -- After 3 failures, logged as error - ---- - -## Security & Privacy Questions - -### Is SureForms secure? - -**Yes.** Security is a core priority. - -**Security measures:** - -**Input sanitization:** -- All user input sanitized before storage -- WordPress functions: `sanitize_text_field()`, `sanitize_email()`, etc. -- Custom sanitization for complex fields - -**Output escaping:** -- All output escaped before rendering -- `esc_html()`, `esc_attr()`, `esc_url()` used throughout -- Prevents XSS attacks - -**SQL injection prevention:** -- All database queries use `$wpdb->prepare()` -- No direct SQL concatenation -- Custom database class with built-in protection - -**CSRF protection:** -- Nonce verification on all AJAX/REST endpoints -- `wp_verify_nonce()` checked before state-changing operations - -**Authorization:** -- Capability checks: `current_user_can()` -- Admin actions require `manage_options` capability -- User-specific data access controlled - -**Payment security:** -- PCI DSS compliant (no card storage) -- Stripe/PayPal handle sensitive data -- API credentials encrypted at rest (AES-256) - -**File uploads (Pro):** -- MIME type validation (server-side) -- File size limits enforced -- Path traversal prevention -- Disallowed file types: `.php`, `.js`, `.exe`, etc. - ---- - -### Where is my data stored? - -**Form submissions:** -- Your WordPress database -- Table: `wp_sureforms_entries` -- No data sent to SureForms servers - -**Payment data:** -- Stripe: Stored in Stripe (not in WordPress) -- PayPal: Stored in PayPal (not in WordPress) -- WordPress stores: Transaction ID, amount, status only - -**Integration credentials (Pro):** -- WordPress database -- Table: `wp_sureforms_integrations` -- Encrypted with AES-256 -- Encryption key in `wp-config.php` (recommended) - -**AI prompts:** -- Sent to AI middleware → OpenAI GPT-4 -- Not stored or logged -- Not used for training - ---- - -### Can users delete their data? - -**Yes** (GDPR requirement). - -**Manual deletion:** - -1. User requests deletion via email -2. Admin finds submissions by email -3. Delete from SureForms → Entries - -**Automatic (Pro):** -- Data retention policy: Auto-delete entries after X days -- Settings → Privacy → Data Retention -- Configure per form or globally - -**What gets deleted:** -- Entry data (all fields) -- Entry metadata (IP, user agent) -- File uploads (if any) - -**What's preserved:** -- Form configuration (for re-use) -- Analytics (anonymized counts only) - ---- - -## Troubleshooting Questions - -### My forms aren't showing on the frontend. Why? - -**Common causes:** - -**1. Form not published:** -- Check form status in SureForms → Forms -- Must be "Published" not "Draft" - -**2. Form not embedded:** -- Add SureForms block to page -- Select your form from dropdown -- Or use shortcode: `[sureforms id="123"]` - -**3. Theme conflict:** -- Try default theme: `wp theme activate twentytwentythree` -- If works → Theme issue, contact theme developer - -**4. JavaScript disabled:** -- Check browser console for errors -- Disable other plugins to find conflict - -**Debug:** -```bash -# Enable debugging -wp config set WP_DEBUG true --raw -wp config set WP_DEBUG_LOG true --raw - -# Check debug log -tail -f wp-content/debug.log -``` - -See [troubleshooting guide](troubleshooting.md) for more. - ---- - -### Form submissions aren't saving. What's wrong? - -**Check:** - -**1. Database tables exist:** -```bash -wp db query "SHOW TABLES LIKE '%sureforms%';" -``` - -Should show `wp_sureforms_entries` table. - -**2. Database permissions:** -```bash -wp db query "SHOW GRANTS;" -``` - -User must have INSERT privilege. - -**3. Nonce verification:** -- If using caching, exclude AJAX endpoints -- WP Super Cache: Exclude `/wp-admin/admin-ajax.php` - -**4. Debug log:** -```bash -tail -f wp-content/debug.log -# Submit form, watch for errors -``` - -See [troubleshooting: submissions not saving](troubleshooting.md#form-submissions-not-saving) - ---- - -### Email notifications aren't sending. Help! - -**Diagnose:** - -```bash -# Test WordPress mail function -wp eval "wp_mail('your@email.com', 'Test', 'Testing');" -``` - -If no email received → Server mail() disabled. - -**Fix:** - -**Install SMTP plugin:** -```bash -wp plugin install wp-mail-smtp --activate -``` - -**Configure with:** -- Gmail -- SendGrid -- Mailgun -- Amazon SES - -**Check spam folder:** -- Default WordPress sender: `wordpress@yourdomain.com` -- Often flagged as spam -- SMTP fixes this - -See [troubleshooting: emails not sending](troubleshooting.md#email-notifications-not-sending) - ---- - -## Customization Questions - -### Can I customize form styling? - -**Yes.** Multiple options: - -**Option 1: Settings panel (no code):** -1. Edit form -2. Settings sidebar → Design -3. Customize: - - Colors (background, text, buttons) - - Typography (fonts, sizes) - - Spacing (padding, margins) - - Border radius - -**Option 2: Custom CSS:** -1. SureForms → Settings → Custom CSS -2. Add your styles: -```css -.srfm-form { - background: #f9f9f9; - padding: 30px; - border-radius: 8px; -} - -.srfm-form input { - border: 2px solid #333; -} -``` - -**Option 3: Theme stylesheet:** - -Add to `style.css`: -```css -.srfm-form { /* Your styles */ } -``` - -**Option 4: Page builder:** - -Use page builder's CSS editor (if using Elementor/Divi). - ---- - -### Can I add custom fields? - -**Yes** (requires development). - -**Process:** - -1. Create block folder: `inc/blocks/my-field/` -2. Register block: `block.php` -3. Create React component: `src/blocks/my-field/edit.js` -4. Add sanitization: `inc/helper.php` -5. Add validation: `inc/field-validation.php` - -**Example:** See [onboarding guide](onboarding.md#step-2-create-block-structure) - -**Pre-built custom fields (Pro):** -- Star rating -- Signature -- File upload -- Date/time picker - ---- - -### Can I change the submit button text? - -**Yes.** - -**Per form:** -1. Edit form -2. Click submit button block -3. Settings sidebar → Button Text -4. Change text -5. Update form - -**Globally:** - -Add to `functions.php`: -```php -add_filter('sureforms_submit_button_text', function($text) { - return __('Send Message', 'sureforms'); -}); -``` - ---- - -## Business & Licensing Questions - -### Can I use SureForms on client sites? - -**Free:** Yes, unlimited sites. - -**Pro:** -- **Agency license required** ($299/year, unlimited sites) -- Essential ($99/year, 3 sites) - only for your own sites -- Plus ($199/year, 20 sites) - only for your own sites - -**License terms:** -- You must own the sites (not client-owned sites with Essential/Plus) -- Agency license allows client sites -- Each site needs separate activation - ---- - -### Can I white-label SureForms? - -**Not officially.** - -**What you can do:** -- Remove branding from admin (CSS) -- Custom email footer (replace "Powered by SureForms") -- Custom confirmation messages - -**What you can't do:** -- Remove copyright notices from code -- Rebrand and resell as your own product -- Remove attribution from free plugin - -**Enterprise white-label:** -- Contact sales for custom licensing -- Available for high-volume agencies - ---- - -### What's your refund policy? - -**14-day money-back guarantee** (Pro licenses) - -**Refund eligibility:** -- Purchased within last 14 days -- Tried to use but encountered issues -- Support unable to resolve - -**Not eligible:** -- After 14 days -- Changed mind (not technical issue) -- Purchased wrong license (can upgrade instead) - -**Process:** -1. Contact support: support@sureforms.com -2. Describe issue (we'll try to help first) -3. If unresolved, refund processed within 3-5 business days - ---- - -### How long do I get updates? - -**Free:** Lifetime updates. - -**Pro:** -- Updates: For duration of license (1 year) -- Renewals: Discounted renewal price after year 1 -- If license expires: Plugin continues working, no new updates - -**Version compatibility:** -- Major updates: Included -- Security updates: Prioritized -- Feature updates: Based on roadmap - ---- - -## Developer Questions - -### Is the code open source? - -**Free plugin:** Yes. -- License: GPL v2 or later -- GitHub: https://github.com/brainstormforce/sureforms - -**Pro plugin:** No. -- Proprietary license -- Source code visible but not redistributable - ---- - -### Can I contribute to SureForms? - -**Yes!** We welcome contributions. - -**How to contribute:** - -1. Fork repo: https://github.com/brainstormforce/sureforms -2. Create branch: `git checkout -b feat/my-feature` -3. Make changes -4. Run tests: `composer test && npm run test:unit` -5. Submit PR - -**Contribution guidelines:** -- Follow [coding standards](coding-standards.md) -- Add tests for new features -- Update docs if needed -- Sign off commits: `Co-Authored-By: Your Name ` - ---- - -### Where can I find the API documentation? - -**API docs:** [apis.md](apis.md) - -**Key resources:** -- REST endpoints: [apis.md#rest-api](apis.md#rest-api) -- Hooks & filters: [apis.md#hooks--filters](apis.md#hooks--filters) -- JavaScript APIs: [apis.md#javascript-apis](apis.md#javascript-apis) -- Database APIs: [apis.md#database-apis](apis.md#database-apis) - -**Code examples:** [ai-agent-guide.md](ai-agent-guide.md) - ---- - -## Need More Help? - -**Documentation:** -- [README.md](README.md) - Quick start -- [Troubleshooting](troubleshooting.md) - Common problems -- [Onboarding](onboarding.md) - Developer guide -- [Glossary](glossary.md) - Technical terms - -**Support:** -- **Free users:** WordPress.org forum, GitHub issues -- **Pro users:** https://support.brainstormforce.com/ (< 24hr response) - -**Community:** -- Facebook: https://www.facebook.com/groups/surecart -- GitHub Discussions: https://github.com/brainstormforce/sureforms/discussions - ---- - -**Next:** [Glossary](glossary.md) diff --git a/internal-docs/glossary.md b/internal-docs/glossary.md deleted file mode 100644 index 440e34b70..000000000 --- a/internal-docs/glossary.md +++ /dev/null @@ -1,723 +0,0 @@ -# Glossary - -**Version:** 2.5.0 - ---- - -## A - -### AJAX (Asynchronous JavaScript and XML) -Technique for updating parts of a page without full reload. Used in SureForms for form submissions, payment processing, and file uploads. - -**Example:** -```javascript -// AJAX request to submit form -wp.ajax.post('srfm_submit_form', { data: formData }); -``` - -### API (Application Programming Interface) -Set of functions and methods for interacting with software. SureForms provides REST API, database API, and JavaScript API. - -**Types in SureForms:** -- **REST API:** HTTP endpoints for external access -- **Database API:** Methods to query entries/payments -- **JavaScript API:** Frontend form manipulation - -### Attribute (Gutenberg) -Data stored with a block. In SureForms, attributes define field configuration (label, required, placeholder, etc.). - -**Example:** -```json -{ - "label": "Email", - "required": true, - "placeholder": "name@example.com" -} -``` - ---- - -## B - -### Block (Gutenberg) -Reusable component in WordPress editor. SureForms forms are composed of blocks (Email block, Textarea block, Submit button block, etc.). - -**Types:** -- **Static blocks:** Rendered client-side (React) -- **Dynamic blocks:** Rendered server-side (PHP) - -### Block Editor -WordPress's editing interface (formerly called "Gutenberg"). Native to WordPress 5.0+. - -**Why SureForms uses it:** -- Users already familiar -- No proprietary builder to learn -- Future-proof (WordPress core) - ---- - -## C - -### Capability -WordPress permission level. SureForms checks capabilities before allowing admin actions. - -**Common capabilities:** -- `manage_options` - Admin settings access -- `edit_posts` - Create forms (default) -- `read` - View forms (logged-in users) - -**Example:** -```php -if (current_user_can('manage_options')) { - // Allow admin action -} -``` - -### Conditional Logic (Pro) -Show or hide form fields based on user answers. Example: Show "Dietary restrictions" only if user selects "Yes" to "Will you attend?" - -**Types:** -- **Show when:** Display field if condition matches -- **Hide when:** Hide field if condition matches -- **AND/OR logic:** Multiple conditions combined - -### Conversational Form (Pro) -Form that displays one question at a time, like a chat conversation. Increases engagement and completion rates. - -**Benefits:** -- Less overwhelming than long forms -- Higher completion rate (3x vs single-page) -- Better mobile experience - -### CSRF (Cross-Site Request Forgery) -Attack where malicious site tricks user into submitting request to your site. SureForms prevents with nonce verification. - -**Example attack (prevented):** -```html - - - - - - -``` - -### CSV (Comma-Separated Values) -File format for spreadsheet data. SureForms can export form entries as CSV. - -**Example:** -``` -Name,Email,Message -John Doe,john@example.com,Hello! -Jane Smith,jane@example.com,Question about... -``` - ---- - -## D - -### Database Table -MySQL table storing structured data. SureForms uses 4 custom tables: -- `wp_sureforms_entries` - Form submissions -- `wp_sureforms_payments` - Payment records -- `wp_sureforms_integrations` - OAuth credentials -- `wp_sureforms_save_resume` - Draft submissions (Pro) - -**Why custom tables?** -- Faster queries (indexed columns) -- More efficient than post meta -- Easier to export - -### Dynamic Block -Gutenberg block rendered server-side (PHP). Most SureForms field blocks are dynamic for security (server-side validation). - -**Example:** -```php -register_block_type('sureforms/email', [ - 'render_callback' => 'render_email_field' -]); -``` - ---- - -## E - -### Encryption -Converting data to unreadable format. SureForms encrypts OAuth credentials and API keys. - -**Algorithm:** AES-256-CTR - -**Example:** -```php -$encrypted = Encryption::encrypt($api_key); -// Stored in database: "base64encodedIV.encryptedData" -``` - -### Entry -Single form submission. Stored in `wp_sureforms_entries` table. - -**Contains:** -- Form ID -- User ID (if logged in) -- Field values (JSON) -- Metadata (IP, user agent, timestamp) -- Status (published, spam, trash) - -### Escaping (Output Escaping) -Converting special characters to prevent XSS. SureForms escapes all user data before display. - -**Functions:** -- `esc_html()` - For HTML content -- `esc_attr()` - For HTML attributes -- `esc_url()` - For URLs -- `esc_js()` - For JavaScript strings - -**Example:** -```php -echo '
' . esc_html($user_input) . '
'; -``` - ---- - -## F - -### Field Type -Type of input field (email, text, number, etc.). SureForms has 15+ field types. - -**Common types:** -- Text, Email, URL, Textarea -- Number, Phone -- Multiple Choice, Checkbox, Dropdown -- Address, Upload (Pro) - -### Filter (WordPress Hook) -Function that modifies data. SureForms provides 30+ filters for customization. - -**Example:** -```php -add_filter('sureforms_email_subject', function($subject, $form_id) { - return "[Form $form_id] $subject"; -}, 10, 2); -``` - ---- - -## G - -### GDPR (General Data Protection Regulation) -European privacy law. SureForms provides GDPR-compliance features (consent checkbox, data export/deletion). - -**Requirements:** -- Explicit consent for data collection -- Right to access data (export) -- Right to deletion -- Privacy policy link - -### Gutenberg -WordPress's block editor (officially called "Block Editor"). Named after Johannes Gutenberg, inventor of printing press. - -**Why "Gutenberg"?** -- Revolutionized content creation (like printing press) -- Blocks are reusable components - ---- - -## H - -### Honeypot -Anti-spam technique using hidden field. Bots fill all fields (including hidden), revealing themselves. - -**How it works:** -```html - - -``` - -**SureForms logic:** -```php -if (!empty($_POST['srfm-honeypot'])) { - // Spam detected -} -``` - -### Hook (WordPress) -Point where developers can inject custom code. Two types: actions and filters. - -**SureForms hooks:** -- 50+ action hooks (e.g., `sureforms_after_entry_save`) -- 30+ filter hooks (e.g., `sureforms_email_template`) - ---- - -## I - -### Integration (Native Integration) -Connection to third-party service. SureForms Pro has 24+ native integrations (Mailchimp, HubSpot, etc.). - -**Types:** -- **OAuth:** User authorizes connection (Mailchimp, Google) -- **API Key:** User provides key from service -- **Webhook:** SureForms sends POST request to URL - -### Instant Form -SureForms feature allowing forms to be published with unique URL (no embedding needed). - -**Example URL:** -``` -https://yoursite.com/?srfm_form=abc123 -``` - -**Use cases:** -- Email signatures -- Social media links -- QR codes - ---- - -## J - -### JSON (JavaScript Object Notation) -Data format for structured information. SureForms stores entry data and block attributes as JSON. - -**Example entry data:** -```json -{ - "email": "john@example.com", - "name": "John Doe", - "message": "Hello!" -} -``` - ---- - -## L - -### Localization (l10n) -Adapting software for specific language/region. SureForms supports translation. - -**Files:** -- `languages/sureforms-{locale}.po` - Translation strings -- `languages/sureforms-{locale}.mo` - Compiled translations - -**Functions:** -```php -__('Email', 'sureforms'); // Translate string -_e('Submit', 'sureforms'); // Translate and echo -``` - ---- - -## M - -### Middleware -Intermediate server between client and API. SureForms uses middleware for AI form generation (client → middleware → OpenAI). - -**Why middleware?** -- Hides API keys from client -- Rate limiting -- Request logging -- Caching - -### Multi-Step Form (Pro) -Form split into multiple pages/steps. Improves completion rate by reducing cognitive load. - -**Example:** -``` -Step 1: Personal Information (name, email) -Step 2: Address Details -Step 3: Payment Information -``` - ---- - -## N - -### Namespace -PHP feature to organize code and prevent name conflicts. SureForms uses namespaces. - -**Example:** -```php -namespace SRFM\Inc\Database\Tables; - -class Entries { - // ... -} -``` - -### Native Integration -Integration built into SureForms (not requiring Zapier or external connector). - -**Examples:** -- Mailchimp (OAuth) -- HubSpot (OAuth) -- Slack (Webhook URL) - -### Nonce (Number Used Once) -Security token to verify request came from your site. SureForms verifies nonces on all AJAX/REST requests. - -**Example:** -```php -// Generate nonce -$nonce = wp_create_nonce('srfm_submit_form'); - -// Verify nonce -if (!wp_verify_nonce($_POST['nonce'], 'srfm_submit_form')) { - die('Invalid nonce'); -} -``` - ---- - -## O - -### OAuth (Open Authorization) -Protocol for secure authorization. Used by SureForms integrations (Mailchimp, HubSpot, etc.). - -**Flow:** -1. User clicks "Connect to Mailchimp" -2. Redirected to Mailchimp login -3. User approves access -4. Mailchimp redirects back with token -5. SureForms stores encrypted token - ---- - -## P - -### Payment Gateway -Service that processes credit card payments. SureForms supports Stripe and PayPal. - -**Comparison:** -- **Stripe:** 2.9% + $0.30, better for US/Europe -- **PayPal:** 2.9% + $0.30, more global recognition - -### Payment Intent (Stripe) -Stripe object representing payment attempt. Created before customer enters card details. - -**States:** -- `requires_payment_method` - Awaiting card -- `requires_confirmation` - Card entered, needs confirm -- `succeeded` - Payment successful -- `canceled` - Payment canceled - -### PCI DSS (Payment Card Industry Data Security Standard) -Security standard for handling credit card data. SureForms is PCI-compliant (never stores card data). - -**Compliance:** -- Card data sent directly to Stripe/PayPal -- SureForms only stores transaction ID -- No PAN (Primary Account Number) stored - -### Placeholder -Example text shown in empty input field. Disappears when user types. - -**Example:** -```html - -``` - -**Accessibility note:** Never use placeholder as label (screen readers may miss it). - -### Post Meta -WordPress method for storing additional data with posts. SureForms **doesn't** use post meta (uses custom tables instead). - -**Why not post meta?** -- Slow queries (no indexes) -- Hard to search -- Not optimized for large datasets - -### Post Type (Custom Post Type) -WordPress content type. SureForms forms are stored as custom post type `sureforms_form`. - -**Advantages:** -- Uses WordPress admin UI -- Revision history -- Trash/restore functionality - ---- - -## R - -### reCAPTCHA -Google anti-spam service. SureForms supports reCAPTCHA v2 and v3. - -**Versions:** -- **v2:** "I'm not a robot" checkbox -- **v3:** Invisible, scores user behavior (0-1) - -**Setup:** -1. Get API keys from Google reCAPTCHA admin -2. Add to SureForms → Settings → Security -3. Enable on forms - -### REST API -HTTP API for accessing SureForms data. Used by frontend, mobile apps, integrations. - -**Base URL:** `/wp-json/sureforms/v1/` - -**Example endpoints:** -- `POST /submit-form` - Submit form -- `GET /forms/{id}` - Get form data -- `POST /generate-form` - AI form generation - -### RTL (Right-to-Left) -Text direction for languages like Arabic, Hebrew. SureForms supports RTL. - -**CSS:** -```css -.srfm-field { - margin-inline-start: 10px; /* Not margin-left */ -} -``` - ---- - -## S - -### Sanitization (Input Sanitization) -Cleaning user input to prevent attacks. SureForms sanitizes all input before storage. - -**Functions:** -- `sanitize_text_field()` - Remove HTML/PHP -- `sanitize_email()` - Validate email format -- `sanitize_url()` - Validate URL format -- `absint()` - Force integer - -**Example:** -```php -$email = sanitize_email($_POST['email']); -$id = absint($_POST['id']); -``` - -### Schema (Database Schema) -Structure of database table (columns, types, indexes). - -**Example (wp_sureforms_entries):** -```sql -CREATE TABLE wp_sureforms_entries ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - form_id BIGINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED, - entry_data LONGTEXT NOT NULL, - status VARCHAR(20) DEFAULT 'published', - created_at DATETIME NOT NULL, - INDEX idx_form_id (form_id), - INDEX idx_created_at (created_at) -); -``` - -### Shortcode -WordPress feature for embedding content. SureForms forms can be embedded via shortcode. - -**Syntax:** -``` -[sureforms id="123"] -``` - -**Parameters:** -- `id` - Form ID (required) -- `title` - Show title (true/false) -- `description` - Show description (true/false) - -### SQL Injection -Attack injecting malicious SQL into queries. SureForms prevents with `$wpdb->prepare()`. - -**Vulnerable code (DON'T DO THIS):** -```php -$id = $_GET['id']; -$wpdb->query("DELETE FROM table WHERE id = $id"); -// Attack: ?id=1 OR 1=1 (deletes all records!) -``` - -**Safe code:** -```php -$id = absint($_GET['id']); -$wpdb->query($wpdb->prepare("DELETE FROM table WHERE id = %d", $id)); -``` - -### Subscription -Recurring payment. SureForms supports subscriptions via Stripe and PayPal. - -**Intervals:** -- Daily -- Weekly -- Monthly -- Yearly - ---- - -## T - -### Template (Form Template) -Pre-built form configuration. SureForms provides 100+ templates (contact, registration, survey, etc.). - -**Structure:** -```json -{ - "name": "Contact Form", - "fields": [ - { "type": "text", "label": "Name" }, - { "type": "email", "label": "Email" }, - { "type": "textarea", "label": "Message" } - ] -} -``` - -### Transient -WordPress temporary cache. SureForms uses transients for AI rate limiting and API responses. - -**Example:** -```php -// Store for 1 hour -set_transient('srfm_ai_rate_limit_' . $user_id, true, HOUR_IN_SECONDS); - -// Check -if (get_transient('srfm_ai_rate_limit_' . $user_id)) { - // Rate limited -} -``` - ---- - -## U - -### User Agent -String identifying browser/device. SureForms logs user agent for spam detection. - -**Example:** -``` -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 -``` - ---- - -## V - -### Validation (Form Validation) -Checking if user input meets requirements. SureForms validates both client-side (JavaScript) and server-side (PHP). - -**Types:** -- **Required:** Field must not be empty -- **Format:** Email/URL must be valid -- **Range:** Number within min/max -- **Length:** String length constraints - -**Example:** -```php -if (empty($email) || !is_email($email)) { - $errors[] = 'Please enter a valid email'; -} -``` - ---- - -## W - -### Webhook -HTTP callback sent when event occurs. SureForms can send webhooks on form submission (Pro). - -**Flow:** -``` -User submits form - ↓ -SureForms processes submission - ↓ -Sends POST to webhook URL - ↓ -External service receives data -``` - -**Example payload:** -```json -{ - "event": "form_submit", - "form_id": 123, - "entry_id": 456, - "fields": { ... } -} -``` - -### WordPress Coding Standards (WPCS) -PHP coding style guide for WordPress. SureForms follows WPCS. - -**Key rules:** -- Tabs for indentation (not spaces) -- Yoda conditions: `if ( 'value' === $variable )` -- Braces required for all control structures -- Space after control keywords: `if (` not `if(` - -**Check compliance:** -```bash -composer lint -``` - ---- - -## X - -### XSS (Cross-Site Scripting) -Attack injecting malicious JavaScript into pages. SureForms prevents with output escaping. - -**Vulnerable code (DON'T DO THIS):** -```php -echo '
' . $_POST['name'] . '
'; -// Attack: -``` - -**Safe code:** -```php -echo '
' . esc_html($_POST['name']) . '
'; -// Output: <script>steal(document.cookie)</script> -``` - ---- - -## Z - -### Zapier -Third-party automation service. SureForms doesn't have native Zapier integration but supports webhooks (which Zapier can consume). - -**Alternative:** Use SureForms webhooks to trigger Zapier zaps. - ---- - -## Acronyms Quick Reference - -| Acronym | Full Term | Meaning | -|---------|-----------|---------| -| AJAX | Asynchronous JavaScript and XML | Update page without reload | -| API | Application Programming Interface | Methods to interact with software | -| CSRF | Cross-Site Request Forgery | Attack tricking users into unwanted actions | -| CSV | Comma-Separated Values | Spreadsheet file format | -| GDPR | General Data Protection Regulation | EU privacy law | -| JSON | JavaScript Object Notation | Data format | -| OAuth | Open Authorization | Secure authorization protocol | -| PCI DSS | Payment Card Industry Data Security Standard | Credit card security | -| REST | Representational State Transfer | Web API architecture | -| RTL | Right-to-Left | Text direction (Arabic, Hebrew) | -| SQL | Structured Query Language | Database query language | -| URL | Uniform Resource Locator | Web address | -| WCAG | Web Content Accessibility Guidelines | Accessibility standards | -| WPCS | WordPress Coding Standards | PHP coding style | -| XSS | Cross-Site Scripting | JavaScript injection attack | - ---- - -## Common Abbreviations - -| Abbr | Full Term | -|------|-----------| -| Pro | SureForms Pro (premium plugin) | -| CRM | Customer Relationship Management | -| UI | User Interface | -| UX | User Experience | -| CPT | Custom Post Type | -| DB | Database | -| WP | WordPress | -| PHP | PHP: Hypertext Preprocessor | -| JS | JavaScript | -| CSS | Cascading Style Sheets | - ---- - -**Next:** [Maintenance](maintenance.md) diff --git a/internal-docs/maintenance.md b/internal-docs/maintenance.md deleted file mode 100644 index 71c2f2604..000000000 --- a/internal-docs/maintenance.md +++ /dev/null @@ -1,842 +0,0 @@ -# Documentation Maintenance Guide - -**Version:** 2.5.0 - ---- - -## Purpose - -This guide explains how to keep SureForms internal documentation accurate and useful. - -**Goal:** Documentation should always reflect current reality. - -**Principle:** Update docs when code changes, not as afterthought. - ---- - -## When to Update Documentation - -### Always Update - -**1. New Feature Added** -- Update: `architecture.md`, `apis.md`, `product-vision.md` -- Add: Code examples to `ai-agent-guide.md` -- Update: Onboarding guide if workflow changes - -**2. API Changes** -- Update: `apis.md` (new endpoints, hooks, parameters) -- Update: Code examples in all docs referencing changed API -- Mark deprecated APIs in `apis.md` - -**3. Database Schema Changes** -- Update: `architecture.md` (database section) -- Update: `codebase-map.md` (new files) -- Update: Migration guide if schema change requires migration - -**4. Security Fix** -- Update: `ai-agent-guide.md` (add to common pitfalls) -- Update: `troubleshooting.md` if fix addresses known issue -- Add example of vulnerable pattern to avoid - -**5. Breaking Changes** -- Update: `README.md` (migration guide) -- Update: `onboarding.md` (new workflows) -- Update: `faq.md` (address upgrade questions) - ---- - -### Update if Needed - -**6. Bug Fixes** -- Update: `troubleshooting.md` if common issue -- Add: To FAQ if frequently asked - -**7. Performance Improvements** -- Update: `architecture.md` if architecture changed -- Update: Benchmarks in `README.md` - -**8. UI/UX Changes** -- Update: `ui-and-copy.md` (new patterns, copy) -- Update: `onboarding.md` (user journeys) - ---- - -### Don't Update - -**9. Code Refactoring (No Behavior Change)** -- No doc update unless internal architecture significantly changed - -**10. Minor Copy Changes** -- No doc update unless it changes UX patterns - -**11. Version Bumps** -- Update version number at top of each doc -- That's it (unless other changes) - ---- - -## How to Update Documentation - -### Step 1: Identify Affected Docs - -**Use this decision tree:** - -``` -Did you change... - -┌─ Database schema? -│ └─> Update: architecture.md, codebase-map.md -│ -┌─ REST API? -│ └─> Update: apis.md, ai-agent-guide.md (examples) -│ -┌─ AJAX handlers? -│ └─> Update: apis.md -│ -┌─ Hooks/filters? -│ └─> Update: apis.md, ai-agent-guide.md (patterns) -│ -┌─ User-facing UI? -│ └─> Update: ui-and-copy.md, onboarding.md -│ -┌─ Security patterns? -│ └─> Update: ai-agent-guide.md, coding-standards.md -│ -┌─ Build process? -│ └─> Update: README.md, onboarding.md -│ -└─ New file/folder? - └─> Update: codebase-map.md -``` - ---- - -### Step 2: Read Existing Doc - -**Before editing:** -1. Read entire document -2. Identify outdated sections -3. Check for consistency with your changes - -**Don't:** -- Blindly add new section without reading context -- Create duplicate information -- Use different terminology than existing docs - ---- - -### Step 3: Make Minimal Changes - -**Best practices:** - -**Do:** -- Update only what changed -- Keep existing structure -- Match existing tone and style -- Add code examples if helpful - -**Don't:** -- Rewrite entire document (unless necessary) -- Change unrelated sections -- Reorganize without discussion -- Remove information (mark deprecated instead) - ---- - -### Step 4: Verify Examples Still Work - -**All code examples must be tested:** - -**PHP examples:** -```bash -# Create test file -cat > /tmp/test-example.php << 'EOF' - [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='] - ], - 'limit' => 20 -]); -``` - -**❌ Bad:** -```php -// Vague, generic -$data = SomeClass::get_stuff($args); -``` - ---- - -### Cross-Referencing - -**Internal links:** -```markdown -See [Architecture Guide](architecture.md) for details. -See [Database Schema](architecture.md#database-schema) for table structure. -``` - -**External links:** -```markdown -Read [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/). -``` - -**Code references:** -```markdown -See `inc/form-submit.php:88-119` for nonce verification. -``` - ---- - -### Code Block Guidelines - -**Always specify language:** - -````markdown -```php -// PHP code here -``` - -```javascript -// JavaScript here -``` - -```bash -# Shell commands here -``` - -```sql --- SQL queries here -``` -```` - -**Include comments:** -```php -// Good: Explain what code does -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'status', 'value' => 'published', 'compare' => '='] - ] -]); -``` - -**Show output if helpful:** -```bash -wp plugin list --status=active -# Output: -# sureforms active -# sureforms-pro active -``` - ---- - -## Deprecation Process - -**When removing features:** - -### Step 1: Mark as Deprecated - -**In code:** -```php -/** - * Old function. - * - * @deprecated 2.5.0 Use new_function() instead. - */ -function old_function() { - _deprecated_function(__FUNCTION__, '2.5.0', 'new_function'); - return new_function(); -} -``` - -**In docs:** -```markdown -## ~~Old Feature~~ (Deprecated) - -**Deprecated in:** 2.5.0 -**Removed in:** 3.0.0 -**Replacement:** [New Feature](#new-feature) - -This feature is deprecated and will be removed in version 3.0.0. -Use [New Feature](#new-feature) instead. - -~~Old documentation here...~~ -``` - ---- - -### Step 2: Update Migration Guide - -**In README.md:** - -```markdown -## Upgrading from 2.4.x to 2.5.0 - -### Breaking Changes - -**Old Feature Deprecated:** -- **What changed:** `old_function()` is now deprecated -- **Action required:** Replace with `new_function()` -- **Code example:** - ```php - // Old (deprecated) - old_function($data); - - // New (recommended) - new_function($data); - ``` -``` - ---- - -### Step 3: Remove After Major Version - -**On next major version (3.0.0):** -1. Remove deprecated code -2. Remove ~~strikethrough~~ docs -3. Update changelog - ---- - -## Changelog Management - -**Keep CHANGELOG.md updated:** - -### Format - -```markdown -# Changelog - -## [2.5.1] - 2026-02-15 - -### Added -- New REST endpoint: `/sureforms/v1/forms/{id}/duplicate` -- Support for custom date formats in email notifications - -### Changed -- Improved performance of entry queries (20% faster) -- Updated Stripe API to v2024-01-01 - -### Deprecated -- `old_function()` - Use `new_function()` instead - -### Fixed -- Bug where conditional logic didn't work with checkboxes -- Memory leak in AI form generation - -### Security -- Fixed XSS vulnerability in admin settings (CVE-2026-0001) - -## [2.5.0] - 2026-02-01 - -... -``` - -**Categories:** -- **Added:** New features -- **Changed:** Changes to existing features -- **Deprecated:** Features marked for removal -- **Removed:** Features removed -- **Fixed:** Bug fixes -- **Security:** Security fixes - ---- - -## Documentation Review Checklist - -**Before committing doc changes:** - -### Content -- [ ] Information is accurate (tested code examples) -- [ ] No outdated information (double-checked) -- [ ] Cross-references updated (links work) -- [ ] Version number updated -- [ ] Terminology consistent with other docs - -### Style -- [ ] Clear and concise (no fluff) -- [ ] Code examples formatted correctly -- [ ] Spelling and grammar correct -- [ ] Tone matches existing docs - -### Technical -- [ ] All code examples tested -- [ ] All commands verified (on dev environment) -- [ ] All links work (no 404s) -- [ ] File paths correct (checked in codebase) - -### Accessibility -- [ ] Headings in logical order (H1 → H2 → H3) -- [ ] Code blocks have language specified -- [ ] Alt text for images (if any) -- [ ] Tables have headers - ---- - -## Automation & Tools - -### Linting Documentation - -**Check Markdown syntax:** - -```bash -# Install markdownlint -npm install -g markdownlint-cli - -# Lint all docs -cd internal-docs/ -markdownlint *.md -``` - -**Common issues caught:** -- Inconsistent heading levels -- Trailing whitespace -- Missing blank lines around code blocks - ---- - -### Link Checking - -**Verify all links work:** - -```bash -# Install markdown-link-check -npm install -g markdown-link-check - -# Check links -markdown-link-check internal-docs/*.md -``` - -**Fix broken links:** -- Update to correct URL -- Remove if resource no longer exists -- Use Internet Archive if critical resource - ---- - -### Spell Checking - -**Use spell checker:** - -```bash -# Install aspell -brew install aspell # macOS -apt install aspell # Linux - -# Check spelling -aspell check internal-docs/README.md -``` - -**Custom dictionary:** - -Create `.aspell.en.pws`: -``` -personal_ws-1.1 en 50 -SureForms -WordPress -Gutenberg -Mailchimp -PayPal -``` - ---- - -## Documentation Templates - -### New Feature Documentation Template - -**When adding new feature:** - -```markdown -## [Feature Name] - -**Since:** [version] -**Type:** [Free/Pro] - -### Overview - -[2-3 sentence description of what feature does and why it exists] - -### Use Cases - -- [Use case 1] -- [Use case 2] -- [Use case 3] - -### How to Use - -**Step 1: [Action]** - -[Description] - -```php -// Code example -``` - -**Step 2: [Action]** - -[Description] - -### Configuration - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `option_name` | string | 'default' | What this does | - -### API Reference - -**REST Endpoint:** -``` -POST /sureforms/v1/feature -``` - -**Parameters:** -- `param1` (string, required) - Description -- `param2` (integer, optional) - Description - -**Response:** -```json -{ - "success": true, - "data": { ... } -} -``` - -### Examples - -**Example 1: [Use case]** -```php -// Code here -``` - -**Example 2: [Use case]** -```php -// Code here -``` - -### Troubleshooting - -**Issue:** [Common problem] -**Solution:** [How to fix] - -### Related - -- [Related feature 1](link) -- [Related feature 2](link) -``` - ---- - -### Bug Fix Documentation Template - -**When fixing bug:** - -**In troubleshooting.md:** - -```markdown -### [Issue Description] - -**Symptom:** [What user sees] - -**Diagnosis:** - -[How to identify the issue] - -```bash -# Commands to diagnose -``` - -**Fixes:** - -**Option 1: [Fix description]** -```php -// Code or command -``` - -**Option 2: [Alternative fix]** -```php -// Alternative code -``` - -**Prevents:** [What this prevents] - -**See also:** [Related issues] -``` - ---- - -## Review & Approval Process - -### Before Merging Docs - -**Self-review:** -1. Read your changes out loud -2. Test all code examples -3. Run linters -4. Check cross-references - -**Peer review:** -1. Request review from team member -2. Address feedback -3. Update based on comments - -**Final checks:** -1. Rebase on main branch -2. Verify no conflicts -3. One last proofread - ---- - -### Documentation Pull Request Template - -**PR description:** - -```markdown -## Documentation Update - -**Plugin Version:** 2.5.1 -**Docs Changed:** [List files] - -### Changes Made - -- [ ] Added documentation for [feature] -- [ ] Updated [section] in [file] -- [ ] Fixed typos in [file] - -### Verification - -- [x] All code examples tested -- [x] All links checked -- [x] Spelling checked -- [x] Cross-references updated -- [x] Version numbers updated - -### Related - -- Related PR: #123 -- Related Issue: #456 -``` - ---- - -## Documentation Metrics - -**Track documentation quality:** - -### Metrics to Monitor - -**Coverage:** -- % of features documented -- % of APIs documented -- % of code examples tested - -**Freshness:** -- Days since last update -- Number of outdated sections -- Deprecated content count - -**Quality:** -- Broken link count -- Spelling error count -- User-reported doc issues - -**Engagement:** -- Doc views (if analytics enabled) -- Time spent reading -- Bounce rate (if high, docs unclear) - ---- - -## Long-Term Maintenance - -### Quarterly Review - -**Every 3 months:** - -1. **Audit all docs:** - - Read each document start to finish - - Test all code examples - - Verify all links - - Update outdated info - -2. **User feedback:** - - Review GitHub issues tagged "documentation" - - Survey users about doc clarity - - Identify gaps in coverage - -3. **Reorganize if needed:** - - Move sections to better fit structure - - Split large docs if too long - - Merge duplicate information - ---- - -### Annual Review - -**Every year:** - -1. **Major cleanup:** - - Remove deprecated content - - Archive old versions - - Rewrite outdated sections - -2. **Structure evaluation:** - - Does structure still make sense? - - Should we add/remove files? - - Are cross-references clear? - -3. **Voice & tone:** - - Is tone consistent? - - Is it still beginner-friendly? - - Does it match product evolution? - ---- - -## Questions? - -**For documentation questions:** -- Create GitHub issue: `[Docs] Your question` -- Tag: `documentation` -- Assign: Documentation maintainer - -**For urgent doc bugs:** -- Ping in Slack: `#sureforms-docs` -- Include: File name, line number, issue description - ---- - -## Conclusion - -**Documentation is code.** - -Treat it with same care: -- Test before committing -- Review changes -- Keep it DRY (Don't Repeat Yourself) -- Refactor when needed - -**Good documentation:** -- Saves support time -- Accelerates onboarding -- Prevents bugs -- Shows we care - -**Thank you for maintaining SureForms docs!** 📚 - ---- - -**Version:** 2.5.0 -**Last Updated:** 2026-02-12 -**Maintainer:** SureForms Team diff --git a/internal-docs/onboarding.md b/internal-docs/onboarding.md deleted file mode 100644 index ab5b83ad3..000000000 --- a/internal-docs/onboarding.md +++ /dev/null @@ -1,1086 +0,0 @@ -# Developer Onboarding - -**Version:** 2.5.0 - ---- - -## Welcome to SureForms - -This guide helps you become productive quickly, whether you have 1 hour, 1 day, or 1 week. - -**What you're working on:** -- **SureForms Free:** AI-powered WordPress form builder (219 PHP files, 50+ blocks) -- **SureForms Pro:** Premium extension (206 PHP files, payments, integrations, advanced features) - -**Architecture:** WordPress + Gutenberg blocks + React 18 + Custom database - ---- - -## Prerequisites - -Before starting: -- [ ] WordPress 6.4+ installed locally -- [ ] Node.js 18+ and npm 8+ -- [ ] PHP 7.4+ with Composer -- [ ] Git configured -- [ ] IDE with PHP/JavaScript support -- [ ] Both plugins cloned from GitHub - -**Test environment:** -```bash -# If using Local by Flywheel or similar -http://localhost:10003/wp-admin/ -Username: admin -Password: admin -``` - ---- - -## 1-Hour Quick Start - -**Goal:** Make your first successful change - -### Step 1: Get Code Running (15 min) - -```bash -# Clone repositories (if not already) -cd /path/to/wp-content/plugins/ -git clone https://github.com/brainstormforce/sureforms.git -git clone https://github.com/brainstormforce/sureforms-pro.git - -# Install dependencies - SureForms Free -cd sureforms/ -npm install -composer install - -# Build assets -npm run build - -# Install dependencies - SureForms Pro -cd ../sureforms-pro/ -npm install -composer install -npm run build -``` - -**Verify installation:** -```bash -# In WordPress admin -wp plugin activate sureforms sureforms-pro -wp plugin list | grep sureforms -``` - -### Step 2: Create Your First Form (15 min) - -**Via WordPress admin:** -1. Go to **SureForms → Add New** -2. Click **Create with AI** or **Start from Blank** -3. Add fields: Email, Name, Message -4. Click **Publish** -5. Add to a page with the SureForms block - -**Via WP-CLI:** -```bash -# Create form programmatically -wp post create \ - --post_type=sureforms_form \ - --post_title="Test Contact Form" \ - --post_status=publish \ - --post_content='' -``` - -### Step 3: Make a Simple Change (20 min) - -**Task:** Change the submit button text - -**Files to modify:** -``` -src/blocks/form/edit.js ← React component -inc/blocks/form/block.php ← Server-side rendering -``` - -**Change 1: Edit the React component** - -Open `src/blocks/form/edit.js`: - -```javascript -// Find around line 45-50 -const { submitButtonText } = attributes; - -// Change default value -const defaultSubmitText = __('Send Message', 'sureforms'); // ← Change this -``` - -**Change 2: Rebuild** - -```bash -npm run build -``` - -**Verify:** Create new form, check if default button text changed. - -### Step 4: Debug Your Change (10 min) - -**Enable debugging:** - -Edit `wp-config.php`: -```php -define('WP_DEBUG', true); -define('WP_DEBUG_LOG', true); -define('WP_DEBUG_DISPLAY', false); -define('SCRIPT_DEBUG', true); // Loads unminified JS -``` - -**Check debug log:** -```bash -tail -f wp-content/debug.log -``` - -**Browser console:** -- Open DevTools → Console -- Look for JavaScript errors -- Check Network tab for AJAX failures - ---- - -## 1-Day Deep Dive - -**Goal:** Understand architecture and make meaningful contributions - -### Morning: Core Concepts (4 hours) - -#### Hour 1: Architecture Overview - -**Read these docs first:** -1. [README.md](README.md) - Project overview -2. [architecture.md](architecture.md) - System design -3. [codebase-map.md](codebase-map.md) - File structure - -**Key concepts to understand:** -- **Gutenberg blocks:** React components that render forms -- **Custom post type:** `sureforms_form` stores form configurations -- **Custom database tables:** `wp_sureforms_entries`, `wp_sureforms_payments` -- **REST API:** Handles form submissions, AI generation -- **AJAX handlers:** Payment processing, file uploads - -**Test your understanding:** -- Where is form submission data saved? (Answer: `wp_sureforms_entries` table) -- How does AI form generation work? (Answer: REST endpoint → external middleware → GPT API) -- What's the difference between Free and Pro? (Answer: Pro adds payments, integrations, conditional logic) - -#### Hour 2: Data Flow - -**Trace a form submission from start to finish:** - -``` -User fills form → Frontend validation → AJAX request - ↓ -inc/form-submit.php (Line 88-119: Nonce verification) - ↓ -Field validation (inc/field-validation.php) - ↓ -Sanitization (inc/helper.php::sanitize_by_field_type()) - ↓ -Database insert (inc/database/tables/entries.php::insert()) - ↓ -Email notification (inc/email/email-handler.php) - ↓ -Confirmation message (or redirect) -``` - -**Hands-on exercise:** -1. Enable MySQL query logging -2. Submit a test form -3. Check `wp_sureforms_entries` table -4. Verify entry data matches submission - -```bash -# View recent entries -wp db query "SELECT * FROM wp_sureforms_entries ORDER BY id DESC LIMIT 5;" -``` - -#### Hour 3: Payment Processing - -**Understand Stripe payment flow:** - -``` -User clicks "Pay" → Frontend creates payment intent - ↓ -AJAX: wp_ajax_nopriv_srfm_create_payment_intent - ↓ -inc/payments/front-end.php (Line 77-83: Create Stripe PaymentIntent) - ↓ -Stripe API processes payment - ↓ -Webhook received: sureforms/webhook_test - ↓ -inc/payments/stripe/stripe-webhook.php (Line 220-637: Handle events) - ↓ -Update wp_sureforms_payments table - ↓ -Send confirmation email -``` - -**Hands-on exercise:** -1. Set up Stripe test keys -2. Create a payment form -3. Use Stripe test card: `4242 4242 4242 4242` -4. Check `wp_sureforms_payments` table for payment record - -#### Hour 4: Pro Features - -**Explore key Pro additions:** - -**1. User Registration:** -- File: `inc/business/user-registration/processor.php` -- Creates WordPress user accounts from form submissions -- Handles login/password reset - -**2. Native Integrations:** -- Folder: `inc/pro/native-integrations/integrations/` -- 24+ services: Mailchimp, Brevo, HubSpot, Salesforce, etc. -- OAuth authentication with encrypted token storage - -**3. Conditional Logic:** -- Frontend: `src/pro/conditional-logic/` -- Show/hide fields based on user selections -- Real-time UI updates - -**Hands-on exercise:** -1. Create form with registration block -2. Submit form, verify user created: `wp user list` -3. Enable Mailchimp integration (test mode) -4. Submit form, check integration logs - -### Afternoon: Build Your First Feature (4 hours) - -**Challenge:** Add a new field type - -**Example: "Star Rating" field** - -#### Step 1: Plan (30 min) - -**Requirements:** -- Display 5 stars (clickable) -- Save selected rating (1-5) -- Validate: Required if configured -- Display in entries table - -**Files to create/modify:** -``` -src/blocks/star-rating/ ← New React component -inc/blocks/star-rating/block.php ← Server-side rendering -inc/helper.php ← Add sanitization -inc/field-validation.php ← Add validation -``` - -#### Step 2: Create Block Structure (1 hour) - -**Create block files:** - -```bash -# Create directories -mkdir -p src/blocks/star-rating -mkdir -p inc/blocks/star-rating - -# Copy from existing field (Phone is a good template) -cp -r src/blocks/phone/* src/blocks/star-rating/ -cp inc/blocks/phone/block.php inc/blocks/star-rating/ -``` - -**Edit `src/blocks/star-rating/block.json`:** - -```json -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 3, - "name": "sureforms/star-rating", - "title": "Star Rating", - "category": "sureforms", - "icon": "star-filled", - "description": "Display a star rating input", - "attributes": { - "label": { - "type": "string", - "default": "Rate your experience" - }, - "required": { - "type": "boolean", - "default": false - }, - "maxRating": { - "type": "number", - "default": 5 - } - }, - "supports": { - "html": false - } -} -``` - -**Edit `src/blocks/star-rating/edit.js`:** - -```javascript -import { __ } from '@wordpress/i18n'; -import { useBlockProps } from '@wordpress/block-editor'; -import { TextControl, ToggleControl, RangeControl } from '@wordpress/components'; - -export default function Edit({ attributes, setAttributes }) { - const { label, required, maxRating } = attributes; - - return ( -
- setAttributes({ label: value })} - /> - setAttributes({ required: value })} - /> - setAttributes({ maxRating: value })} - min={1} - max={10} - /> -
- {[...Array(maxRating)].map((_, i) => ( - - ))} -
-
- ); -} -``` - -#### Step 3: Server-Side Rendering (45 min) - -**Edit `inc/blocks/star-rating/block.php`:** - -```php - [__CLASS__, 'render'], - ] - ); - } - - public static function render($attributes) { - $label = esc_html($attributes['label'] ?? 'Rate your experience'); - $required = !empty($attributes['required']); - $max_rating = absint($attributes['maxRating'] ?? 5); - $field_name = 'srfm-star-rating-' . uniqid(); - - ob_start(); - ?> -
- -
- - - ★ - - -
- > -
- { - const stars = container.querySelectorAll('.srfm-star'); - const input = container.nextElementSibling; - const maxRating = parseInt(container.dataset.max); - - stars.forEach(star => { - star.addEventListener('click', function() { - const value = parseInt(this.dataset.value); - input.value = value; - - // Visual update - stars.forEach((s, index) => { - if (index < value) { - s.classList.add('selected'); - } else { - s.classList.remove('selected'); - } - }); - }); - - // Hover effect - star.addEventListener('mouseenter', function() { - const value = parseInt(this.dataset.value); - stars.forEach((s, index) => { - if (index < value) { - s.classList.add('hover'); - } else { - s.classList.remove('hover'); - } - }); - }); - }); - - container.addEventListener('mouseleave', function() { - stars.forEach(s => s.classList.remove('hover')); - }); - }); -}); -``` - -#### Step 5: Register Block (15 min) - -**Edit `inc/blocks/register.php`:** - -```php -// Add to existing blocks registration -Star_Rating\Block::register(); -``` - -**Edit `src/blocks/star-rating/index.js`:** - -```javascript -import { registerBlockType } from '@wordpress/blocks'; -import edit from './edit'; -import metadata from './block.json'; - -registerBlockType(metadata.name, { - edit, - save: () => null, // Server-side rendering -}); -``` - -#### Step 6: Add Sanitization & Validation (30 min) - -**Edit `inc/helper.php`:** - -Find `sanitize_by_field_type()` method and add: - -```php -case 'star-rating': - $value = absint($value); - // Ensure value is between 1 and max rating - return ($value >= 1 && $value <= 10) ? $value : 0; -``` - -**Edit `inc/field-validation.php`:** - -Find validation logic and add: - -```php -if ($field_type === 'star-rating' && $is_required && empty($value)) { - $errors[] = __('Please select a rating', 'sureforms'); -} -``` - -#### Step 7: Build & Test (30 min) - -```bash -# Build assets -npm run build - -# Test in browser -# 1. Create new form -# 2. Add "Star Rating" block -# 3. Configure settings (label, required, max stars) -# 4. Publish form -# 5. Submit test entry -# 6. Verify data saved correctly - -# Check database -wp db query "SELECT * FROM wp_sureforms_entries ORDER BY id DESC LIMIT 1;" -``` - ---- - -## 1-Week Mastery - -**Goal:** Expert-level understanding and complex feature development - -### Day 1: Architecture Deep Dive - -**Morning:** -- Complete 1-day onboarding -- Read all internal docs thoroughly -- Map data flow for all major features - -**Afternoon:** -- Trace payment processing end-to-end -- Understand webhook verification -- Study integration architecture - -### Day 2: Security & Best Practices - -**Focus areas:** -1. **Input sanitization:** Understand all 15+ sanitization methods -2. **Output escaping:** When to use `esc_html()`, `esc_attr()`, `esc_url()` -3. **SQL injection prevention:** Always use `$wpdb->prepare()` -4. **CSRF protection:** Nonce verification patterns -5. **Authorization:** `current_user_can()` checks - -**Hands-on exercises:** -- Review `inc/form-submit.php` (Lines 88-119: Security checks) -- Study `inc/helper.php` sanitization methods -- Find and fix intentional security bugs in test code - -**Read:** -- [coding-standards.md](coding-standards.md) -- [ai-agent-guide.md](ai-agent-guide.md) - Security section - -### Day 3: React & Gutenberg Blocks - -**Topics:** -1. **Block architecture:** How Gutenberg blocks work -2. **Attributes:** Data storage and retrieval -3. **Inspector controls:** Settings sidebar -4. **Block variations:** Different field configurations -5. **Dynamic blocks:** Server-side rendering - -**Build complex block:** -- **Multi-step form block** (if not using Pro feature) -- Add step navigation -- Save progress between steps -- Implement validation per step - -**Resources:** -- Official Gutenberg docs: https://developer.wordpress.org/block-editor/ -- Example: Study `src/blocks/phone/` for complex field - -### Day 4: Database & REST API - -**Morning: Database operations** - -**Study these classes:** -- `inc/database/base.php` - Base table class -- `inc/database/tables/entries.php` - Entries CRUD -- `inc/database/tables/payments.php` - Payment records - -**Practice:** -```php -// Create custom query -use SRFM\Inc\Database\Tables\Entries; - -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='], - ['key' => 'status', 'value' => 'published', 'compare' => '='] - ], - 'orderby' => 'created_at', - 'order' => 'DESC', - 'limit' => 20 -]); -``` - -**Afternoon: REST API development** - -**Create custom endpoint:** - -```php -// In new file: inc/rest-api-custom.php -namespace SRFM\Inc; - -class Rest_API_Custom { - public function register_routes() { - register_rest_route('sureforms/v1', '/custom-stats', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_stats'], - 'permission_callback' => [$this, 'check_permissions'] - ]); - } - - public function check_permissions() { - return current_user_can('manage_options'); - } - - public function get_stats($request) { - // Your logic here - return ['success' => true, 'data' => []]; - } -} -``` - -### Day 5: Pro Features & Integrations - -**Morning: Payment processing** - -**Study both gateways:** -- Stripe: `inc/payments/stripe/` -- PayPal (Pro): `inc/business/payments/pay-pal/` - -**Understand webhook handling:** -- Signature verification -- Event processing -- Database updates -- Error handling - -**Afternoon: Build custom integration** - -**Example: Slack integration** - -1. Create integration folder: -``` -inc/pro/native-integrations/integrations/slack/ -├── config.json -└── actions/ - └── send-message.php -``` - -2. Define config: -```json -{ - "name": "Slack", - "slug": "slack", - "auth_method": "webhook_url", - "actions": [ - { - "name": "Send Message", - "slug": "send-message", - "endpoint": "chat.postMessage" - } - ] -} -``` - -3. Implement action: -```php -format_message($form_data); - - $response = wp_remote_post($webhook_url, [ - 'body' => json_encode(['text' => $message]), - 'headers' => ['Content-Type' => 'application/json'] - ]); - - if (is_wp_error($response)) { - return ['success' => false, 'error' => $response->get_error_message()]; - } - - return ['success' => true]; - } - - private function format_message($form_data) { - // Format form data as Slack message - return "New form submission:\n" . print_r($form_data, true); - } -} -``` - ---- - -## Testing Your Changes - -### Local Testing - -**PHP Unit Tests:** -```bash -# Run all tests -composer test - -# Run specific test file -vendor/bin/phpunit tests/unit/test-helper.php - -# Run with coverage -composer test-coverage -``` - -**JavaScript Tests:** -```bash -# Run all JS tests -npm run test:unit - -# Watch mode -npm run test:unit:watch - -# Coverage -npm run test:unit:coverage -``` - -### E2E Testing (Playwright) - -```bash -# Start test environment -npm run play:up - -# Run E2E tests -npm run play:run - -# Interactive mode (see browser) -npm run play:run:interactive - -# Stop environment -npm run play:down -``` - -### Manual Testing Checklist - -Before submitting PR: -- [ ] Form creation works -- [ ] Form submission saves entry -- [ ] Email notifications sent -- [ ] Payment processing works (if touched) -- [ ] No JavaScript console errors -- [ ] No PHP errors in debug.log -- [ ] Works in latest WordPress version -- [ ] Works in Firefox, Chrome, Safari -- [ ] Mobile responsive -- [ ] Accessibility: keyboard navigation works - ---- - -## Code Review Process - -### Before Creating PR - -**Self-review:** -1. Run linters: -```bash -composer lint # PHP_CodeSniffer -npm run lint-js # ESLint -``` - -2. Auto-fix minor issues: -```bash -composer format # phpcbf -npm run lint-js:fix # ESLint --fix -``` - -3. Check for common issues: -- [ ] All inputs sanitized -- [ ] All outputs escaped -- [ ] SQL queries use `prepare()` -- [ ] Nonces verified for AJAX/forms -- [ ] Capabilities checked for admin actions -- [ ] No hardcoded strings (use `__()` for i18n) - -### Creating PR - -**Branch naming:** -```bash -git checkout -b feat/star-rating-field -git checkout -b fix/payment-webhook-error -git checkout -b refactor/database-queries -``` - -**Commit message format:** -``` -type(scope): brief description - -Longer explanation if needed. - -Co-Authored-By: Your Name -``` - -**Types:** feat, fix, docs, style, refactor, test, chore - -**Example:** -``` -feat(blocks): add star rating field - -- Add new star-rating block -- Implement frontend JavaScript -- Add sanitization and validation -- Update docs with new field type - -Co-Authored-By: Claude Sonnet 4.5 -``` - -### PR Checklist - -- [ ] Tests pass: `composer test && npm run test:unit` -- [ ] Linting passes: `composer lint && npm run lint-js` -- [ ] No console errors -- [ ] Database migrations documented (if any) -- [ ] Breaking changes documented -- [ ] Changelog updated -- [ ] Screenshots attached (for UI changes) - ---- - -## Common Tasks & Patterns - -### Add New AJAX Handler - -**Pattern:** -```php -// In controller file -public function register_ajax_actions() { - add_action('wp_ajax_srfm_my_action', [$this, 'handle_my_action']); - add_action('wp_ajax_nopriv_srfm_my_action', [$this, 'handle_my_action']); -} - -public function handle_my_action() { - // CRITICAL: Always verify nonce - check_ajax_referer('srfm_my_action_nonce', 'security'); - - // Check permissions if needed - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Sanitize inputs - $data = Helper::sanitize_array_recursively($_POST['data'] ?? []); - - // Process... - - wp_send_json_success(['result' => $result]); -} -``` - -### Add New REST Endpoint - -**Pattern:** -```php -// In inc/rest-api.php -public function register_routes() { - register_rest_route('sureforms/v1', '/my-endpoint', [ - 'methods' => 'POST', - 'callback' => [$this, 'handle_request'], - 'permission_callback' => [$this, 'check_permissions'] - ]); -} - -public function check_permissions() { - return current_user_can('manage_options'); -} - -public function handle_request($request) { - // Always verify nonce - $nonce = $request->get_header('X-WP-Nonce'); - if (!wp_verify_nonce($nonce, 'wp_rest')) { - return new \WP_Error('invalid_nonce', 'Invalid nonce', ['status' => 403]); - } - - // Get and sanitize parameters - $params = $request->get_params(); - $clean_params = Helper::sanitize_array_recursively($params); - - // Process... - - return ['success' => true, 'data' => $result]; -} -``` - -### Add WordPress Hook - -**Action hook (notify about event):** -```php -// Trigger action after entry save -do_action('sureforms_after_entry_save', $entry_id, $form_id, $entry_data); - -// Other plugins/themes can listen: -add_action('sureforms_after_entry_save', function($entry_id, $form_id, $data) { - // Custom logic -}, 10, 3); -``` - -**Filter hook (modify data):** -```php -// Allow filtering of email subject -$subject = apply_filters('sureforms_email_subject', $subject, $form_id, $entry_data); - -// Other plugins/themes can modify: -add_filter('sureforms_email_subject', function($subject, $form_id, $data) { - return "[Form $form_id] $subject"; -}, 10, 3); -``` - -### Database Operations - -**Insert entry:** -```php -use SRFM\Inc\Database\Tables\Entries; - -$entry_id = Entries::insert([ - 'form_id' => 123, - 'user_id' => get_current_user_id(), - 'status' => 'published', - 'entry_data' => wp_json_encode($form_data) -]); -``` - -**Update entry:** -```php -Entries::update($entry_id, [ - 'status' => 'spam' -]); -``` - -**Delete entry:** -```php -Entries::delete($entry_id); -``` - -**Query entries:** -```php -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='] - ], - 'limit' => 20, - 'offset' => 0 -]); -``` - ---- - -## Debugging Tips - -### Enable Debug Mode - -**wp-config.php:** -```php -define('WP_DEBUG', true); -define('WP_DEBUG_LOG', true); -define('WP_DEBUG_DISPLAY', false); -define('SCRIPT_DEBUG', true); -define('SAVEQUERIES', true); -``` - -### Check Debug Log - -```bash -# Real-time log monitoring -tail -f wp-content/debug.log - -# Search for errors -grep "SureForms" wp-content/debug.log -``` - -### JavaScript Debugging - -**Browser console:** -```javascript -// Check global object -console.log(window.sureforms); - -// Check block attributes -wp.data.select('core/block-editor').getBlocks(); - -// Check form data -document.querySelector('form').addEventListener('submit', (e) => { - console.log('Form data:', new FormData(e.target)); -}); -``` - -### Database Queries - -**Install Query Monitor plugin:** -```bash -wp plugin install query-monitor --activate -``` - -**Check slow queries:** -- Visit any admin page -- Click "Query Monitor" in admin bar -- View "Queries" tab -- Sort by time -- Optimize slow queries - -**Manual query inspection:** -```php -// Add to code temporarily -global $wpdb; -$wpdb->show_errors(); -$wpdb->print_error(); -``` - -### AJAX Debugging - -**Check network tab:** -1. Open DevTools → Network -2. Filter: XHR -3. Submit form -4. Click request -5. Check Response tab for errors - -**Add debug output:** -```php -public function handle_ajax() { - error_log('AJAX data: ' . print_r($_POST, true)); - // ... rest of code -} -``` - ---- - -## Resources - -### Internal Documentation -- [README.md](README.md) - Quick start -- [architecture.md](architecture.md) - System design -- [codebase-map.md](codebase-map.md) - File structure -- [apis.md](apis.md) - API reference -- [coding-standards.md](coding-standards.md) - Code standards -- [ai-agent-guide.md](ai-agent-guide.md) - AI agent guidance -- [troubleshooting.md](troubleshooting.md) - Common problems - -### External Resources -- **WordPress Codex:** https://codex.wordpress.org/ -- **Block Editor Handbook:** https://developer.wordpress.org/block-editor/ -- **REST API Handbook:** https://developer.wordpress.org/rest-api/ -- **React Docs:** https://react.dev/ -- **WP-CLI:** https://wp-cli.org/ - -### Community -- **GitHub Issues:** Report bugs, request features -- **Facebook Group:** https://www.facebook.com/groups/surecart -- **Support:** https://support.brainstormforce.com/ - ---- - -## Next Steps - -After completing onboarding: - -1. **Pick a starter issue:** - - Look for "good first issue" label on GitHub - - Start with documentation or small bug fixes - - Work up to features - -2. **Read code:** - - Pick a feature you use - - Trace it from UI to database - - Understand every line - -3. **Write tests:** - - For code you touch - - Increases confidence - - Helps others understand your changes - -4. **Ask questions:** - - Don't hesitate to ask - - Better to ask than assume - - Document answers for others - -**Welcome to the team!** 🚀 diff --git a/internal-docs/product-vision.md b/internal-docs/product-vision.md deleted file mode 100644 index f41f1951c..000000000 --- a/internal-docs/product-vision.md +++ /dev/null @@ -1,688 +0,0 @@ -# Product Vision - -**Version:** 2.5.0 - ---- - -## Mission - -**Empower anyone to build beautiful, high-converting forms without code.** - -SureForms exists to solve a fundamental problem: creating forms on WordPress is unnecessarily complex, resulting in ugly forms that hurt conversion rates. - ---- - -## The Problem We Solve - -### Pain Points (Before SureForms) - -**1. Complexity** -- Learning curve: New interfaces, proprietary builders -- Time waste: Hours configuring simple contact forms -- Technical barriers: Requires developer for custom fields - -**2. Design Limitations** -- Generic templates that don't match site design -- Limited styling options without CSS knowledge -- Mobile responsiveness as afterthought - -**3. Low Engagement** -- Long, intimidating single-page forms -- No personalization or conditional logic -- High abandonment rates (avg 67% for long forms) - -**4. Spam & Security** -- Constant bot submissions -- Security vulnerabilities in popular plugins -- GDPR compliance challenges - -**5. Integration Friction** -- Payment processing requires multiple plugins -- Manual data export/import to CRMs -- Webhook setup requires developer knowledge - ---- - -## Our Solution - -### Core Differentiators - -**1. Native WordPress (Gutenberg)** -- **Why:** Users already know the interface -- **Benefit:** Zero learning curve, instant productivity -- **Technical:** React blocks, no proprietary builder - -**2. AI-Powered Form Building** -- **Innovation:** First AI form builder for WordPress -- **How:** Natural language → complete functional form in seconds -- **Examples:** - - "Create a job application form" → 12-field form with file upload - - "Simple contact form" → 3 fields, perfectly styled - - "Event RSVP with dietary restrictions" → Conditional logic auto-configured - -**3. Built-in Payments** -- **Why:** No WooCommerce, no add-ons required -- **Gateways:** Stripe (Free), PayPal (Pro) -- **Features:** One-time, subscriptions, custom amounts -- **Security:** PCI-compliant, encrypted credentials - -**4. Mobile-First Design** -- **Approach:** Responsive by default, not opt-in -- **Testing:** Every block tested on iOS/Android -- **Performance:** Fast load times on 3G networks - -**5. Engagement Features (Pro)** -- **Conversational Forms:** Chat-like, one question at a time -- **Multi-Step Forms:** Break long forms into digestible steps -- **Conditional Logic:** Show/hide based on answers -- **Result:** 3x higher completion rates vs single-page forms - ---- - -## Target Users - -### Primary Personas - -#### 1. Website Owner (40% of users) -**Profile:** -- Small business owner or solopreneur -- Limited technical skills -- DIY mentality -- Budget-conscious - -**Needs:** -- Contact forms, quote requests, bookings -- Easy setup (< 10 minutes) -- Professional appearance -- Spam protection - -**Pain Points:** -- Frustrated with complex form builders -- Can't afford developer -- Generic templates don't match brand - -**How SureForms Helps:** -- AI creates form in 30 seconds -- Instant Form feature (no embedding needed) -- Built-in anti-spam (reCAPTCHA, Honeypot) -- Modern, customizable design - ---- - -#### 2. WordPress Designer (30% of users) -**Profile:** -- Freelancer or agency designer -- Strong design skills, basic code knowledge -- Builds sites for clients -- Values aesthetics and UX - -**Needs:** -- Forms that match site design -- Custom styling without CSS -- Fast deployment -- Client-friendly interface - -**Pain Points:** -- Form plugins look "generic" -- CSS overrides are tedious -- Clients can't update forms themselves -- Other plugins don't use Gutenberg - -**How SureForms Helps:** -- Gutenberg-native (feels like page building) -- Extensive styling options in UI -- Client can edit without breaking design -- Block patterns for reusability - ---- - -#### 3. WordPress Developer (20% of users) -**Profile:** -- Full-stack developer -- Builds custom WordPress solutions -- Values clean code and extensibility -- Performance-conscious - -**Needs:** -- Developer-friendly APIs -- Hooks and filters -- Custom field types -- Database access -- Integration capabilities - -**Pain Points:** -- Other plugins have messy codebases -- Limited extensibility -- Poor documentation -- Performance issues (N+1 queries, bloat) - -**How SureForms Helps:** -- Clean, modern codebase (PSR-12-inspired) -- Extensive hooks: 50+ actions/filters -- Well-documented APIs -- Optimized queries (custom tables, not post meta) -- GitHub access for contributions - ---- - -#### 4. E-commerce Store Owner (10% of users) -**Profile:** -- Runs WooCommerce or custom store -- Needs payment forms (not full checkout) -- Sells services, memberships, donations -- Wants simplicity - -**Needs:** -- Accept payments without WooCommerce -- Subscription billing -- Custom pricing fields -- Receipt emails - -**Pain Points:** -- WooCommerce is overkill for simple payments -- Other payment forms require add-ons -- Can't customize payment flow -- Poor mobile checkout experience - -**How SureForms Helps:** -- Built-in Stripe/PayPal (no plugins) -- Subscription support included -- Custom amount fields (donations, tips) -- Mobile-optimized payment UI - ---- - -## Feature Philosophy - -### Design Principles - -**1. Simplicity Over Features** -- Don't add features just because competitors have them -- Every feature must solve real user problem -- Hide complexity behind smart defaults - -**Example:** -- ❌ Bad: 50 font options overwhelming users -- ✅ Good: 5 curated fonts + custom font option - -**2. Progressive Disclosure** -- Show basic options first -- Advanced settings collapsed by default -- Help text on hover, not always visible - -**Example:** -- Default form settings: 4 essential options -- Advanced panel: 15+ options (collapsed) - -**3. Convention Over Configuration** -- Smart defaults based on common use cases -- Zero config for 80% of users -- Power users can customize - -**Example:** -- Email notification auto-configured with sensible template -- User can override if needed - -**4. Performance First** -- Lazy load non-critical assets -- Minimize HTTP requests -- Database queries optimized (no N+1) -- CSS/JS minified and cached - -**Metrics:** -- Page load impact: < 50KB additional assets -- Time to Interactive: < 2 seconds on 3G -- Database queries: Max 5 per form render - -**5. Accessibility Built-In** -- WCAG 2.1 Level AA compliance -- Keyboard navigation -- Screen reader support -- Focus indicators - -**Testing:** -- Every block tested with NVDA/JAWS -- Keyboard-only testing required -- Color contrast checked - ---- - -## Free vs Pro Strategy - -### Free Plugin (Core Experience) - -**Philosophy:** Full-featured, not crippled trial - -**Included:** -- Unlimited forms -- Unlimited submissions -- All 15+ field types -- Email notifications -- Spam protection (reCAPTCHA, Honeypot) -- Stripe payments (one-time, subscriptions) -- Form analytics -- Export entries (CSV) -- GDPR compliance -- Instant Forms - -**Limitations:** -- No PayPal -- No native integrations (Mailchimp, etc.) -- No conditional logic -- No multi-step/conversational forms -- No user registration -- No PDF generation - -**Goal:** Provide genuine value, build trust, convert 5-10% to Pro - ---- - -### Pro Plugin (Power Features) - -**Philosophy:** Advanced features for serious users - -**Added Value:** -- **Payments:** PayPal (one-time, subscriptions) -- **Integrations:** 24+ native (Mailchimp, Brevo, HubSpot, Salesforce, etc.) -- **Logic:** Conditional show/hide, calculations -- **Forms:** Multi-step, conversational, save & resume -- **Users:** Registration, login, password reset -- **Output:** PDF generation from submissions -- **Advanced Fields:** Upload (images, files), signature, calculator -- **Priority Support:** < 24hr response time - -**Pricing Tiers:** -- **Essential:** $99/year (3 sites) -- **Plus:** $199/year (20 sites) -- **Agency:** $299/year (unlimited sites) - -**Conversion Strategy:** -- Free users see "Pro" badge on locked features -- No nag screens or popups -- Upgrade CTA in logical places (when user needs feature) -- 14-day money-back guarantee - ---- - -## Competitive Landscape - -### Direct Competitors - -**1. Gravity Forms** -- **Strengths:** Mature, extensive add-ons, trusted -- **Weaknesses:** Old UI, not Gutenberg-native, expensive -- **Our Advantage:** Modern interface, AI builder, lower cost - -**2. WPForms** -- **Strengths:** Beginner-friendly, drag-and-drop -- **Weaknesses:** Generic designs, limited styling -- **Our Advantage:** Better design flexibility, Gutenberg-native - -**3. Formidable Forms** -- **Strengths:** Advanced features, views/reporting -- **Weaknesses:** Steep learning curve, performance issues -- **Our Advantage:** Simpler, faster, AI-assisted - -**4. Fluent Forms** -- **Strengths:** Conversational forms, modern UI -- **Weaknesses:** Not Gutenberg-native, separate builder -- **Our Advantage:** Native Gutenberg, simpler UX - -**5. Contact Form 7** -- **Strengths:** Free, lightweight, popular -- **Weaknesses:** No UI, requires shortcodes, ugly default styles -- **Our Advantage:** Visual builder, beautiful defaults, AI - ---- - -### Market Positioning - -**SureForms:** The AI-powered Gutenberg form builder - -**Tagline:** "Beautiful forms without code" - -**Positioning Statement:** -> For WordPress users who want high-converting forms without complexity, SureForms is the AI-powered form builder that creates beautiful, mobile-first forms in seconds—unlike outdated plugins that require hours of configuration and result in generic-looking forms. - -**Why Users Choose SureForms:** -1. **Speed:** AI creates forms in 30 seconds vs 30 minutes -2. **Design:** Modern, mobile-first vs generic templates -3. **Ease:** Gutenberg-native vs proprietary builder -4. **Value:** Built-in payments vs add-ons required - ---- - -## Product Roadmap - -### Current Focus (2026 Q1) - -**Theme:** Stability & Performance - -**Priorities:** -1. Bug fixes from user reports -2. Performance optimization (lazy loading, query optimization) -3. Security hardening (code audit, penetration testing) -4. Documentation improvements -5. Accessibility compliance (WCAG 2.2 Level AA) - ---- - -### Near-Term (2026 Q2-Q3) - -**Theme:** Advanced Features & Integrations - -**Planned:** -1. **More Integrations:** - - Google Sheets (native, no Zapier) - - Airtable - - Notion - - Slack (native) - -2. **Field Types:** - - Star rating - - Matrix/grid (Pro) - - File upload enhancements (drag & drop, preview) - -3. **Conditional Logic Enhancements (Pro):** - - Show/hide blocks (not just fields) - - Calculation fields (price quotes, BMI calculators) - - Conditional email notifications - -4. **Analytics:** - - Conversion funnel visualization - - A/B testing (form variations) - - Heatmaps (where users drop off) - -5. **Templates:** - - 100+ pre-built form templates - - Industry-specific (real estate, healthcare, education) - - Import/export custom templates - ---- - -### Long-Term Vision (2027+) - -**Theme:** AI-Driven Personalization - -**Research Areas:** -1. **AI Form Optimization:** - - Auto-suggest field improvements based on completion rates - - Predictive text for common fields - - Smart field ordering (ML-driven) - -2. **Advanced Conversational Forms:** - - Voice input support - - Natural language processing for responses - - Branching logic based on sentiment - -3. **Global Expansion:** - - Multi-language support (WPML, Polylang) - - Currency localization - - Regional compliance (CCPA, PIPEDA, etc.) - -4. **Enterprise Features:** - - Team collaboration (roles, permissions) - - Advanced approval workflows - - Audit logs - - White-label options - -5. **Mobile App:** - - iOS/Android app for managing entries - - Push notifications for new submissions - - Offline form viewing - ---- - -## Success Metrics - -### North Star Metric -**Active Forms:** Number of forms receiving at least 1 submission per month - -**Why:** Indicates genuine usage, not just installs - -**Target:** 100,000 active forms by end of 2026 - ---- - -### Secondary Metrics - -**Growth:** -- New installations per month: 50,000+ -- Activation rate: 60% (user creates first form within 7 days) -- Retention: 80% still active after 30 days - -**Engagement:** -- Forms created per user: 3.5 average -- Submissions per form: 25/month average -- Feature adoption (Pro): 40% use conditional logic - -**Conversion:** -- Free → Pro conversion: 5-7% -- Trial → Paid: 25% -- Annual renewal rate: 85% - -**Quality:** -- Support ticket volume: < 2% of active users -- Bug reports: < 0.5% of installations -- 4.5+ star rating on WordPress.org - -**Performance:** -- Average page load impact: < 40KB -- Time to first form submission: < 5 minutes (from install) -- Support response time: < 12 hours - ---- - -## User Feedback Integration - -### How We Listen - -**1. Support Tickets** -- Every ticket tagged by topic -- Monthly review of common issues -- Feature requests tracked in GitHub - -**2. User Surveys** -- Annual user satisfaction survey -- Post-purchase survey (Pro users) -- Exit survey (churned Pro users) - -**3. Analytics** -- Feature usage tracking (opt-in) -- Error logging (anonymized) -- Performance metrics - -**4. Community** -- Facebook group discussions -- GitHub issues and discussions -- WordPress.org support forum - -**5. Direct Outreach** -- User interviews (quarterly) -- Beta tester program -- Power user advisory board - ---- - -### Decision Framework - -**Feature Requests Evaluation:** - -**Criteria:** -1. **Impact:** How many users need this? (1-10) -2. **Effort:** Development complexity? (1-10) -3. **Strategic Fit:** Aligns with vision? (Yes/No) -4. **Competitive:** Do competitors have it? (Yes/No) -5. **Revenue:** Drives conversions? (Yes/No) - -**Scoring:** -- Impact / Effort = Priority score -- Strategic Fit = multiplier (2x if yes) -- Build if score > 5 - -**Example:** -- Feature: "Drag & drop file upload" -- Impact: 8 (many requests) -- Effort: 4 (moderate complexity) -- Strategic Fit: Yes (better UX) -- Score: (8/4) × 2 = 4 → **Build later** - ---- - -## Brand & Voice - -### Brand Personality - -**Adjectives:** -- Approachable (not intimidating) -- Modern (not trendy) -- Reliable (not boring) -- Empowering (not condescending) - -**Tone:** -- Friendly but professional -- Clear over clever -- Helpful without being pushy -- Honest about limitations - ---- - -### Writing Guidelines - -**Do:** -- Use "you" (conversational) -- Short sentences -- Active voice -- Explain "why" not just "how" - -**Don't:** -- Jargon without explanation -- Marketing fluff ("revolutionary", "game-changing") -- Passive voice ("the form was created") -- Unnecessary exclamation marks!!!! - -**Examples:** - -❌ Bad: "SureForms revolutionizes form building with cutting-edge AI technology!" - -✅ Good: "SureForms uses AI to create forms in seconds. Describe what you need, and we'll build it." - ---- - -❌ Bad: "The form submission process has been optimized for maximum conversion potential." - -✅ Good: "We designed our forms to load fast and look great on mobile, so more people complete them." - ---- - -## Technical Vision - -### Architecture Goals - -**1. Performance** -- Custom database tables (not post meta) -- Lazy loading for non-critical assets -- Query optimization (no N+1) -- CDN-friendly (static assets versioned) - -**2. Scalability** -- Handle 100,000+ submissions per form -- Efficient database queries (indexed columns) -- Background processing for heavy tasks (webhooks, PDFs) -- Caching strategy (transients, object cache) - -**3. Security** -- Input sanitization (all user data) -- Output escaping (all rendered content) -- Nonce verification (all AJAX/REST) -- SQL injection prevention (prepared statements) -- Regular security audits - -**4. Extensibility** -- 50+ hooks (actions and filters) -- Clean, documented APIs -- Developer-friendly codebase -- Backward compatibility promise - -**5. Maintainability** -- WordPress coding standards (PHP_CodeSniffer) -- ESLint for JavaScript -- Automated testing (PHPUnit, Playwright) -- Clear documentation (inline and external) - ---- - -## Values & Principles - -**1. User Privacy** -- No tracking without consent -- GDPR compliance built-in -- Data portability (easy export) -- Clear privacy policy - -**2. Open Source (Free Plugin)** -- Public GitHub repository -- Accept community contributions -- Transparent roadmap -- Active maintenance - -**3. Quality Over Speed** -- Test thoroughly before release -- Fix bugs before adding features -- Code review required for all changes -- No "move fast and break things" - -**4. Accessibility** -- WCAG 2.1 Level AA minimum -- Keyboard navigation always -- Screen reader testing required -- Color contrast compliance - -**5. Sustainability** -- Reasonable pricing (not subscription trap) -- Long-term support commitment -- No vendor lock-in (data always exportable) -- Transparent upgrade policies - ---- - -## What We Won't Do - -**Out of Scope:** - -**1. Full CRM System** -- Why: Bloat, complexity, competing with specialists -- Alternative: Integrate with existing CRMs (HubSpot, Salesforce) - -**2. Email Marketing Platform** -- Why: Already solved by Mailchimp, Brevo, etc. -- Alternative: Native integrations with 24+ email platforms - -**3. Complete E-commerce Solution** -- Why: WooCommerce exists, does it well -- Alternative: Simple payment forms (our niche) - -**4. Website Builder** -- Why: Outside core competency (forms) -- Alternative: Work seamlessly with any page builder - -**5. Survey & Quiz Builder** -- Why: Different use case, different UX requirements -- Alternative: Forms work for simple surveys - ---- - -## Conclusion - -**Core Belief:** -Forms are critical touchpoints between businesses and customers. They should be beautiful, fast, and easy to create. - -**Our Promise:** -We'll keep building tools that make form creation accessible to everyone, without sacrificing power or flexibility. - -**For Developers:** -Your feedback shapes this product. Keep the issues and PRs coming. We're listening. - ---- - -**Next:** [UI & Copy Guidelines](ui-and-copy.md) diff --git a/internal-docs/troubleshooting.md b/internal-docs/troubleshooting.md deleted file mode 100644 index b7f256af5..000000000 --- a/internal-docs/troubleshooting.md +++ /dev/null @@ -1,1031 +0,0 @@ -# Troubleshooting Guide - -**Version:** 2.5.0 - ---- - -## Quick Diagnostic - -**Before diving deep, check these common issues:** - -```bash -# 1. WordPress & PHP versions -wp core version -php -v - -# 2. Plugin status -wp plugin list | grep sureforms - -# 3. Theme compatibility -wp theme list --status=active - -# 4. Recent errors -tail -50 wp-content/debug.log | grep -i "sureforms\|fatal\|error" - -# 5. Database tables exist -wp db query "SHOW TABLES LIKE '%sureforms%';" -``` - ---- - -## Installation & Activation Issues - -### Plugin Won't Activate - -**Symptom:** "Plugin activation failed" or white screen - -**Common Causes:** - -**1. PHP Version Too Old** -```bash -php -v -# Required: PHP 7.4+ -``` - -**Fix:** -```bash -# Update PHP (contact host if shared hosting) -# Or add to wp-config.php temporarily to see error: -define('WP_DEBUG', true); -define('WP_DEBUG_DISPLAY', true); -``` - -**2. WordPress Version Too Old** -```bash -wp core version -# Required: WordPress 6.4+ -``` - -**Fix:** -```bash -wp core update -``` - -**3. Conflicting Plugin** - -**Check for conflicts:** -```bash -# Deactivate all other plugins -wp plugin deactivate --all --exclude=sureforms,sureforms-pro - -# Try activating SureForms -wp plugin activate sureforms - -# Reactivate plugins one by one -wp plugin activate plugin-name -``` - -**Common conflicts:** -- Old caching plugins (W3 Total Cache < 2.0) -- Security plugins with aggressive rules -- Other form builders (namespace collisions) - -**4. Memory Limit Too Low** - -**Check current limit:** -```bash -wp eval 'echo WP_MEMORY_LIMIT;' -``` - -**Fix (wp-config.php):** -```php -define('WP_MEMORY_LIMIT', '256M'); -``` - ---- - -### Pro Plugin Shows "Base Plugin Required" - -**Symptom:** SureForms Pro won't activate - -**Diagnosis:** -```bash -wp plugin list | grep sureforms -# Ensure both sureforms AND sureforms-pro are installed -``` - -**Fix:** -1. Install SureForms Free first -2. Activate SureForms Free -3. Then activate SureForms Pro - -**Check version compatibility:** -```bash -# Free and Pro versions should match -# Both should be 2.5.0 (or same major.minor) -``` - ---- - -### Database Tables Not Created - -**Symptom:** Form submissions fail, entries not showing - -**Check tables exist:** -```bash -wp db query "SHOW TABLES LIKE '%sureforms%';" -``` - -**Expected output:** -``` -wp_sureforms_entries -wp_sureforms_payments -wp_sureforms_integrations -wp_sureforms_save_resume -``` - -**Fix: Force database creation** -```bash -# Deactivate plugin -wp plugin deactivate sureforms sureforms-pro - -# Delete plugin (backup first!) -wp plugin delete sureforms - -# Reinstall -wp plugin install sureforms --activate - -# Tables should be created on activation -``` - -**Manual creation (last resort):** - -Read schema from: -- `inc/database/tables/entries.php` -- `inc/database/tables/payments.php` - -Run CREATE TABLE statements manually. - ---- - -## Form Builder (Editor) Issues - -### Forms Won't Load in Editor - -**Symptom:** Blank screen or infinite loading spinner - -**Diagnosis:** - -**1. Check browser console:** -``` -Open DevTools → Console -Look for JavaScript errors -``` - -**Common errors:** - -**a) "wp.blockEditor is undefined"** -- **Cause:** Outdated WordPress version -- **Fix:** `wp core update` - -**b) "React version mismatch"** -- **Cause:** Another plugin using old React version -- **Fix:** Deactivate other plugins one by one to find conflict - -**c) "Uncaught SyntaxError"** -- **Cause:** JavaScript file corrupted or not minified correctly -- **Fix:** Rebuild assets: `npm run build` in plugin directory - -**2. Check network tab:** -``` -DevTools → Network -Filter: JS -Look for failed requests (red, 404, 500) -``` - -**Common failures:** -- `form-editor.js` (404) → Rebuild assets -- `chunk-vendors.js` (500) → Server misconfiguration - -**3. Enable SCRIPT_DEBUG:** - -In `wp-config.php`: -```php -define('SCRIPT_DEBUG', true); -``` - -This loads unminified JS, easier to debug. - ---- - -### Blocks Not Appearing in Inserter - -**Symptom:** SureForms blocks missing from block library - -**Diagnosis:** -```bash -# Check if blocks are registered -wp eval "print_r(get_option('srfm_blocks_registration'));" -``` - -**Fixes:** - -**1. Re-register blocks:** -```bash -wp plugin deactivate sureforms -wp plugin activate sureforms -``` - -**2. Clear block cache:** -```bash -wp cache flush -wp transient delete --all -``` - -**3. Check block.json files exist:** -```bash -find wp-content/plugins/sureforms/inc/blocks -name "block.json" -# Should return multiple files -``` - ---- - -### Form Preview Shows "Invalid Block" - -**Symptom:** Form renders as "This block contains unexpected or invalid content" - -**Cause:** Block HTML structure changed, saved form has old structure - -**Fix:** - -**Option 1: Update block (preserves data):** -1. Click "Attempt Block Recovery" -2. Verify content looks correct -3. Update form - -**Option 2: Clear and rebuild:** -1. Delete the invalid block -2. Add fresh block -3. Reconfigure settings - -**Prevention:** -- Always test after plugin updates -- Keep staging environment for testing - ---- - -## Form Submission Issues - -### Form Submissions Not Saving - -**Symptom:** User submits form, success message shows, but no entry in database - -**Diagnosis:** - -**1. Check debug log:** -```bash -tail -f wp-content/debug.log -# Submit form while watching log -``` - -**Common errors:** -- "Database insert failed" → Check table permissions -- "Nonce verification failed" → Caching issue (see below) -- "Call to undefined method" → PHP version or missing dependency - -**2. Check database directly:** -```bash -# Get latest entry -wp db query "SELECT * FROM wp_sureforms_entries ORDER BY id DESC LIMIT 1;" -``` - -**3. Test with minimal form:** -Create form with ONLY: -- Email field -- Submit button - -If this works, issue is with specific field type. - -**Fixes:** - -**Database Permissions:** -```bash -# Check MySQL user has INSERT privilege -wp db query "SHOW GRANTS;" -``` - -**Fix permissions (MySQL):** -```sql -GRANT INSERT, UPDATE, DELETE ON database_name.* TO 'wp_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -**Caching Interference:** - -Caching plugins cache nonce values, causing verification failures. - -**Fix:** -1. Exclude `/wp-admin/admin-ajax.php` from cache -2. Exclude REST API `/wp-json/` from cache -3. Or disable caching temporarily to test - ---- - -### Email Notifications Not Sending - -**Symptom:** Form submitted successfully, but no email received - -**Diagnosis:** - -**1. Check if emails are being sent at all:** -```bash -# Install WP Mail SMTP or similar -wp plugin install wp-mail-smtp --activate - -# Or test with simple command: -wp eval "wp_mail('your@email.com', 'Test', 'Testing SureForms');" -``` - -**2. Check SureForms email settings:** -```bash -# View form meta -wp post meta list | grep email -``` - -**3. Check spam folder** -- WordPress default `from` address: `wordpress@yourdomain.com` -- Often flagged as spam - -**Fixes:** - -**Configure SMTP:** - -Install WP Mail SMTP plugin: -```bash -wp plugin install wp-mail-smtp --activate -``` - -Configure with: -- Gmail -- SendGrid -- Mailgun -- Amazon SES - -**Check email template:** - -In form settings → Email Notification: -- Verify "To" address is correct -- Check "From" address is valid domain -- Test with simple subject/message - -**Server mail() function:** - -Some hosts disable PHP `mail()` function. - -Test: -```bash -php -r "mail('test@example.com', 'Test', 'Test message');" -``` - -If no email received, `mail()` is disabled. Use SMTP. - ---- - -### Form Validation Not Working - -**Symptom:** Form submits even with empty required fields - -**Causes:** - -**1. JavaScript disabled** (frontend validation skipped) -**2. Theme CSS hiding error messages** -**3. Custom JavaScript conflict** - -**Diagnosis:** - -**Check browser console:** -``` -Look for JavaScript errors -Check if srfm-validation.js loaded -``` - -**Test with default theme:** -```bash -wp theme activate twentytwentythree -# Submit form again -``` - -If works with default theme → Theme conflict. - -**Fixes:** - -**Theme conflict:** - -Add to theme's `functions.php`: -```php -add_action('wp_enqueue_scripts', function() { - // Ensure SureForms scripts load - wp_enqueue_script('srfm-frontend'); -}, 20); -``` - -**CSS hiding errors:** - -Check if theme has: -```css -.srfm-error { display: none !important; } -``` - -Remove or override. - ---- - -## Payment Processing Issues - -### Stripe Payments Failing - -**Symptom:** "Payment failed" error after entering card details - -**Diagnosis:** - -**1. Check Stripe API keys:** -```bash -# In WordPress admin: SureForms → Settings → Payments → Stripe -# Verify: -# - Using correct keys (test vs live) -# - Keys match Stripe dashboard -``` - -**2. Check Stripe webhook:** -```bash -# In Stripe Dashboard → Developers → Webhooks -# Verify webhook URL is: -https://yoursite.com/wp-json/sureforms/webhook_test - -# Check recent webhook deliveries for errors -``` - -**3. Check browser console:** -``` -DevTools → Console -Look for Stripe.js errors -``` - -**Common Errors:** - -**"Invalid API Key"** -- Using test key in live mode (or vice versa) -- API key revoked in Stripe dashboard -- Fix: Copy fresh keys from Stripe - -**"Payment Intent creation failed"** -- Amount is $0 or negative -- Currency mismatch -- Fix: Check form configuration, amount field - -**"Webhook signature verification failed"** -- Webhook secret incorrect -- Middleware issue -- Fix: Copy webhook signing secret from Stripe, update settings - -**Fixes:** - -**Test mode checklist:** -```bash -# 1. Use test API keys (starts with pk_test_ / sk_test_) -# 2. Test card: 4242 4242 4242 4242 -# 3. Any future expiry date -# 4. Any 3-digit CVC -``` - -**Live mode checklist:** -```bash -# 1. Use live API keys (starts with pk_live_ / sk_live_) -# 2. SSL certificate valid (https://) -# 3. Webhook verified in Stripe dashboard -# 4. Test with real card (refund immediately) -``` - ---- - -### PayPal Payments Failing (Pro) - -**Symptom:** Redirected to PayPal but payment doesn't process - -**Diagnosis:** - -**Check PayPal credentials:** -```bash -# SureForms → Settings → Payments → PayPal -# Verify: -# - Client ID matches PayPal dashboard -# - Secret matches -# - Using sandbox for testing, live for production -``` - -**Check webhook endpoint:** -```bash -# PayPal Dashboard → Apps & Credentials → Webhooks -# Webhook URL should be: -https://yoursite.com/wp-json/sureforms-pro/paypal-live-webhook -``` - -**Fixes:** - -**Test in sandbox mode first:** -1. Create PayPal sandbox account: https://developer.paypal.com/ -2. Use sandbox credentials in SureForms -3. Test with sandbox buyer account - -**Common issues:** -- **Wrong environment:** Using sandbox credentials in live mode -- **Webhook not subscribed:** Must subscribe to payment events in PayPal dashboard -- **SSL certificate:** PayPal requires valid HTTPS - ---- - -## Integration Issues (Pro) - -### Native Integration Not Connecting - -**Symptom:** "Connection failed" when adding integration (Mailchimp, HubSpot, etc.) - -**Diagnosis:** - -**1. Check OAuth redirect URL:** - -For OAuth integrations (Mailchimp, HubSpot, Salesforce): -``` -Redirect URL must be: -https://yoursite.com/wp-json/sureforms-pro/v1/oauth/callback -``` - -**2. Test API credentials:** - -For API key integrations (Brevo, etc.): -```bash -# Test API key directly -curl -X GET "https://api.brevo.com/v3/account" \ - -H "api-key: YOUR_API_KEY" -``` - -Should return account info, not error. - -**3. Check error logs:** -```bash -tail -f wp-content/debug.log | grep -i "integration\|oauth" -``` - -**Fixes:** - -**OAuth issues:** -- Ensure site uses HTTPS (required for OAuth) -- Whitelist redirect URL in service's developer console -- Check OAuth app has correct permissions/scopes - -**API key issues:** -- Regenerate key in service dashboard -- Copy entire key (no spaces, no quotes) -- Check key has required permissions (read/write) - ---- - -### Webhook Not Firing - -**Symptom:** Form submitted, but data not sent to integrated service - -**Diagnosis:** - -**Check webhook logs:** - -Install Query Monitor plugin: -```bash -wp plugin install query-monitor --activate -``` - -Submit form, check "HTTP API Calls" panel for webhook requests. - -**Manually trigger webhook:** -```bash -# Find integration ID -wp db query "SELECT * FROM wp_sureforms_integrations;" - -# Trigger webhook manually (developer test) -wp eval "do_action('sureforms_after_entry_save', 123, 456, []);" -``` - -**Fixes:** - -**Webhook URL validation:** - -In `inc/pro/integrations/webhooks.php`, ensure URL is valid: -- HTTPS only (no HTTP) -- No localhost (unless testing) -- Responds with 200 OK - -**Timeout issues:** - -Increase timeout in `wp-config.php`: -```php -define('WP_HTTP_BLOCK_EXTERNAL', false); -define('WP_ACCESSIBLE_HOSTS', 'api.mailchimp.com,api.hubspot.com'); -``` - ---- - -## Performance Issues - -### Form Editor Slow to Load - -**Symptom:** Takes 10+ seconds to load form in editor - -**Diagnosis:** - -**1. Check database size:** -```bash -wp db query "SELECT COUNT(*) FROM wp_sureforms_entries;" -# If > 100,000 entries, database may be slow -``` - -**2. Check server resources:** -```bash -# Memory usage -free -h - -# CPU usage -top - -# Disk I/O -iostat -``` - -**3. Profile with Query Monitor:** -```bash -wp plugin install query-monitor --activate -# Open form editor, check QM panel for slow queries -``` - -**Fixes:** - -**Optimize database:** -```bash -# Clean old entries (backup first!) -wp db query "DELETE FROM wp_sureforms_entries WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);" - -# Optimize tables -wp db optimize -``` - -**Increase PHP limits:** - -In `php.ini` or `.htaccess`: -``` -max_execution_time = 300 -memory_limit = 256M -``` - -**Enable object caching:** - -Install Redis or Memcached: -```bash -wp plugin install redis-cache --activate -wp redis enable -``` - ---- - -### Frontend Form Loads Slowly - -**Symptom:** Form takes 5+ seconds to appear on page - -**Diagnosis:** - -**Check asset loading:** -``` -DevTools → Network → Slow 3G simulation -Watch which assets are slow -``` - -**Common bottlenecks:** -- Google Fonts loading slowly -- Large CSS/JS files -- Unoptimized images in form - -**Fixes:** - -**Lazy load non-critical assets:** - -In `functions.php`: -```php -add_filter('script_loader_tag', function($tag, $handle) { - if ($handle === 'srfm-frontend') { - return str_replace(' src', ' defer src', $tag); - } - return $tag; -}, 10, 2); -``` - -**Use CDN for Google Fonts:** - -In form settings → Design → Typography: -- Limit to 1-2 font families -- Use system fonts for faster load (e.g., -apple-system) - -**Minify and combine assets:** -```bash -npm run build # Ensures assets are minified -``` - -**Enable caching:** - -Install caching plugin: -```bash -wp plugin install wp-super-cache --activate -``` - -Configure to cache pages with forms. - ---- - -## Block Compatibility Issues - -### Form Breaks After Theme Update - -**Symptom:** Form displays incorrectly or not at all after theme update - -**Diagnosis:** - -**Compare theme CSS:** - -Check if new theme has conflicting styles: -```css -/* Common conflicts */ -.srfm-form input { /* Theme override */ } -.srfm-field { /* Theme override */ } -``` - -**Test with default theme:** -```bash -wp theme activate twentytwentythree -``` - -If works → Theme issue. - -**Fixes:** - -**Add theme compatibility CSS:** - -In child theme `style.css`: -```css -/* Reset SureForms blocks */ -.srfm-form, -.srfm-field, -.srfm-form input, -.srfm-form textarea { - all: revert; -} - -/* Then apply minimal SureForms styles */ -``` - -**Use !important (last resort):** - -In SureForms settings → Custom CSS: -```css -.srfm-form input { - border: 1px solid #ccc !important; - padding: 10px !important; -} -``` - ---- - -### Conflicts with Page Builders - -**Symptom:** Forms don't work inside Elementor/Divi/Beaver Builder - -**Common Issues:** - -**1. JavaScript conflicts:** -- Page builder loads own jQuery version -- Conflicts with SureForms scripts - -**Fix:** -```php -// In functions.php -add_action('wp_enqueue_scripts', function() { - if (class_exists('Elementor\Plugin')) { - wp_dequeue_script('jquery'); - wp_enqueue_script('jquery'); - } -}, 100); -``` - -**2. CSS specificity:** -- Page builder CSS overrides SureForms - -**Fix:** - -Use SureForms settings → Custom CSS with higher specificity: -```css -.elementor-widget-container .srfm-form input { - /* Your styles */ -} -``` - ---- - -## Database & Query Issues - -### "Too many connections" Error - -**Symptom:** Site crashes during high form submission volume - -**Diagnosis:** -```bash -wp db query "SHOW STATUS LIKE 'max_used_connections';" -wp db query "SHOW VARIABLES LIKE 'max_connections';" -``` - -**Fixes:** - -**Increase max connections (MySQL):** - -In `my.cnf`: -``` -[mysqld] -max_connections = 500 -``` - -**Use persistent connections:** - -In `wp-config.php`: -```php -define('DB_CHARSET', 'utf8mb4'); -define('DB_COLLATE', ''); -define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_PERSISTENT); -``` - -**Add connection pooling:** - -Use ProxySQL or PgBouncer for connection pooling. - ---- - -### Slow Query: "SELECT * FROM wp_sureforms_entries" - -**Symptom:** Admin page very slow when viewing entries - -**Diagnosis:** -```bash -wp plugin install query-monitor --activate -# View entries page, check QM for slow queries -``` - -**Fixes:** - -**Add database indexes:** -```sql -ALTER TABLE wp_sureforms_entries -ADD INDEX idx_form_id (form_id), -ADD INDEX idx_created_at (created_at), -ADD INDEX idx_status (status); -``` - -**Limit entries displayed:** - -In admin, reduce entries per page from 100 to 20. - -**Paginate large result sets:** - -Ensure code uses `LIMIT` and `OFFSET`: -```php -$entries = Entries::get_all([ - 'limit' => 20, - 'offset' => ($page - 1) * 20 -]); -``` - ---- - -## WordPress.org Review Issues - -### Plugin Rejected for Security - -**Common reasons:** - -**1. Direct database calls without prepare():** -```php -// ❌ Bad -$wpdb->query("DELETE FROM table WHERE id = $id"); - -// ✅ Good -$wpdb->query($wpdb->prepare("DELETE FROM table WHERE id = %d", $id)); -``` - -**2. Unsanitized user input:** -```php -// ❌ Bad -echo $_POST['name']; - -// ✅ Good -echo esc_html(sanitize_text_field($_POST['name'] ?? '')); -``` - -**3. Missing nonce verification:** -```php -// ❌ Bad -if (isset($_POST['action'])) { do_action(); } - -// ✅ Good -if (isset($_POST['action']) && check_ajax_referer('my_action_nonce')) { - do_action(); -} -``` - -**Fix:** - -Run WordPress Coding Standards checker: -```bash -composer require --dev wp-coding-standards/wpcs -vendor/bin/phpcs --standard=WordPress inc/ -``` - -Fix all errors before resubmitting. - ---- - -## Getting Help - -### Before Asking for Help - -**Gather this information:** - -```bash -# 1. WordPress & PHP versions -wp core version -php -v - -# 2. Plugin version -wp plugin list | grep sureforms - -# 3. Active theme -wp theme list --status=active - -# 4. Other active plugins -wp plugin list --status=active - -# 5. Recent errors -tail -50 wp-content/debug.log - -# 6. Browser/OS (if frontend issue) -# Example: Chrome 120 on macOS 14 -``` - -### Where to Get Help - -**1. Documentation** (check first) -- This troubleshooting guide -- [FAQ](faq.md) -- [Architecture](architecture.md) - -**2. GitHub Issues** (bugs & feature requests) -- Search existing issues first -- Provide minimal reproduction steps -- Include system info from above - -**3. Support Portal** (Pro users) -- https://support.brainstormforce.com/ -- < 24hr response time for Pro users - -**4. Community** -- Facebook group: https://www.facebook.com/groups/surecart -- WordPress.org forum (Free only) - ---- - -## Still Stuck? - -If none of the above solutions work: - -**Create detailed bug report:** - -```markdown -**Environment:** -- WordPress: 6.4.2 -- PHP: 8.1 -- SureForms: 2.5.0 (Free/Pro) -- Theme: Astra 4.5.0 -- Browser: Chrome 120 - -**Steps to Reproduce:** -1. Create form with email field -2. Mark field as required -3. Submit form with empty email -4. [Describe unexpected behavior] - -**Expected:** Validation error shows -**Actual:** Form submits anyway - -**Debug Log:** -[Paste relevant errors from debug.log] - -**Screenshots:** -[Attach if visual issue] -``` - -Submit to: https://github.com/brainstormforce/sureforms/issues - ---- - -**Next:** [FAQ](faq.md) diff --git a/internal-docs/ui-and-copy.md b/internal-docs/ui-and-copy.md deleted file mode 100644 index e12a9a45a..000000000 --- a/internal-docs/ui-and-copy.md +++ /dev/null @@ -1,1077 +0,0 @@ -# UI & Copy Guidelines - -**Version:** 2.5.0 - ---- - -## User Experience Philosophy - -**Principle:** Every word and interaction should make the user feel capable, not confused. - -**User Mindset:** -- "I just want to create a form quickly" -- "I don't have time to read documentation" -- "I'm not a developer" - -**Our Responsibility:** -- Make obvious what to do next -- Explain why, not just how -- Never blame the user - ---- - -## User Journeys - -### Journey 1: First-Time User Creates Contact Form - -**Goal:** Create and publish first form in < 5 minutes - -**Steps:** - -**1. Plugin Activation (30 seconds)** - -``` -User activates plugin - ↓ -Redirect to welcome screen - ↓ -Show 2 options: - [Create with AI] [Start from Blank] -``` - -**Copy:** -``` -Welcome to SureForms! - -Let's create your first form. - -[Create with AI] ← Recommended for beginners - Let AI build your form from a simple description - -[Start from Blank] - Build your form block-by-block -``` - -**Design notes:** -- Large, friendly buttons -- "Recommended" badge on AI option -- No walls of text - ---- - -**2. AI Form Creation (2 minutes)** - -``` -User clicks "Create with AI" - ↓ -Modal appears with text input - ↓ -User types: "simple contact form" - ↓ -AI generates form (3-5 seconds) - ↓ -Form opens in editor, ready to publish -``` - -**Copy:** - -``` -Create Form with AI - -Describe the form you need: -┌─────────────────────────────────────┐ -│ Example: "job application with │ -│ file upload" or "event RSVP" │ -└─────────────────────────────────────┘ - -[Cancel] [Generate Form →] -``` - -**Loading state:** -``` -✨ Creating your form... - -AI is adding fields based on your description. -This usually takes 3-5 seconds. -``` - -**Success state:** -``` -✅ Your form is ready! - -We added: -• Name field -• Email field -• Message field -• Submit button - -You can add, remove, or rearrange fields below. - -[Publish Form] -``` - -**Design notes:** -- Loading spinner + encouraging message -- Success message lists what was created -- Clear next action (Publish) - ---- - -**3. Form Publishing (1 minute)** - -``` -User clicks "Publish" - ↓ -Gutenberg publish panel opens - ↓ -SureForms shows additional options -``` - -**Copy in publish panel:** - -``` -📋 Form Settings - -Where should submissions be sent? - -Email: [admin@yoursite.com ▼] - -After submission, show: -○ Confirmation message -○ Redirect to page -● Confirmation message (selected by default) - -Message: -┌─────────────────────────────────────┐ -│ Thank you! We'll respond within │ -│ 24 hours. │ -└─────────────────────────────────────┘ - -[Publish Form] -``` - -**Design notes:** -- Sensible defaults (confirmation message, admin email) -- Inline editing (no separate settings page) -- Jargon-free labels - ---- - -**4. Embedding Form (30 seconds)** - -``` -User wants to add form to page - ↓ -Edit page in Gutenberg - ↓ -Add SureForms block - ↓ -Select form from dropdown -``` - -**Copy in block inserter:** - -``` -SureForms - -Display a form on your page. - -📝 No form yet? Create one in SureForms > Add New -``` - -**Copy in block settings:** - -``` -Form - -Select a form: -[Contact Form ▼] [Create New] - -Display settings: -☑ Show form title -☑ Show form description -☐ Hide labels (show placeholders only) -``` - -**Design notes:** -- Help text guides to form creation if needed -- Checkbox labels explain what they do -- Preview updates in real-time - ---- - -### Journey 2: Pro User Sets Up Payment Form - -**Goal:** Create payment form and collect first payment - -**Complexity:** Higher (involves Stripe setup) - -**Steps:** - -**1. Payment Gateway Setup (5 minutes)** - -``` -User navigates to: -SureForms → Settings → Payments → Stripe -``` - -**Copy:** - -``` -Stripe Payments - -Accept credit card payments with Stripe. - -🔒 Secure: We never store card details. Payments are processed directly by Stripe. - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Test Mode (for testing) - -☑ Enable test mode - -Use these while testing: - Test card: 4242 4242 4242 4242 - Any future expiry, any CVC - -Publishable Key (starts with pk_test_) -┌─────────────────────────────────────┐ -│ │ -└─────────────────────────────────────┘ - -Secret Key (starts with sk_test_) -┌─────────────────────────────────────┐ -│ •••••••••••••••••••••••••••••••• │ -└─────────────────────────────────────┘ - -Where to get keys: -→ Stripe Dashboard > Developers > API Keys - https://dashboard.stripe.com/apikeys - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Live Mode (for real payments) - -⚠️ Only use after testing - -Publishable Key (starts with pk_live_) -┌─────────────────────────────────────┐ -│ │ -└─────────────────────────────────────┘ - -Secret Key (starts with sk_live_) -┌─────────────────────────────────────┐ -│ │ -└─────────────────────────────────────┘ - -[Save Settings] -``` - -**Design notes:** -- Test mode emphasized first -- Clear instructions where to get keys -- Security reassurance ("We never store") -- Direct link to Stripe dashboard - ---- - -**2. Creating Payment Form (3 minutes)** - -``` -User creates new form - ↓ -Adds "Payment" block - ↓ -Configures amount and currency -``` - -**Copy in Payment block settings:** - -``` -Payment Details - -Amount -○ Fixed amount - ┌──────┐ - │ 50 │ USD ▼ - └──────┘ - -● Let user choose amount - Min: [10] Max: [1000] USD ▼ - Default: [50] - -Payment Type -● One-time payment -○ Subscription - Interval: [Monthly ▼] - -Button Text -┌─────────────────────────────────────┐ -│ Pay Now │ -└─────────────────────────────────────┘ -``` - -**Design notes:** -- Radio buttons for mutually exclusive options -- Currency dropdown next to amount -- Clear labels (no "recurring" jargon, use "subscription") - ---- - -**3. Testing Payment (2 minutes)** - -**Copy on frontend form:** - -``` -Payment Information - -Amount: $50.00 - -Card Number -┌─────────────────────────────────────┐ -│ 1234 5678 9012 3456 │ [Card icon] -└─────────────────────────────────────┘ - -Expiry CVC -┌──────────┐ ┌─────┐ -│ MM / YY │ │ 123 │ -└──────────┘ └─────┘ - -🔒 Secure payment powered by Stripe - Your card details are encrypted and never stored. - -[Pay $50.00] -``` - -**Success message after payment:** - -``` -✅ Payment Successful! - -Receipt sent to: john@example.com - -Transaction ID: ch_1A2B3C4D5E6F - -[View Receipt] [Back to Home] -``` - -**Error message if payment fails:** - -``` -❌ Payment Failed - -Your card was declined. - -Common reasons: -• Insufficient funds -• Incorrect card number or expiry date -• Card requires 3D Secure verification - -Please try again or use a different card. - -[Try Again] -``` - -**Design notes:** -- Security reassurance prominent -- Success message includes receipt email and transaction ID -- Error message explains why and how to fix - ---- - -### Journey 3: Power User Builds Conditional Logic Form (Pro) - -**Goal:** Show/hide fields based on user selection - -**Example:** Event RSVP with dietary restrictions (only show if attending) - -**Steps:** - -**1. Creating Base Form (2 minutes)** - -``` -User adds fields: - - "Will you attend?" (Multiple Choice: Yes/No) - - "Dietary restrictions" (Dropdown) -``` - -**2. Adding Conditional Logic (3 minutes)** - -``` -User clicks "Dietary restrictions" block - ↓ -Opens block settings panel - ↓ -Enables conditional logic -``` - -**Copy in block settings:** - -``` -Conditional Logic - -Show this field only when certain conditions are met. - -☑ Enable conditional logic - -Show this field when: - -[Will you attend? ▼] [is ▼] [Yes ▼] - -[+ Add Condition] - -Logic: -● Show if ALL conditions match (AND) -○ Show if ANY condition matches (OR) -``` - -**Design notes:** -- Toggle to enable -- Dropdown-based condition builder (no code) -- Visual feedback: field grays out in editor preview when hidden - ---- - -**3. Testing Logic (1 minute)** - -**Copy in editor preview mode:** - -``` -💡 Preview Mode - -This is how your form will appear to users. - -Try selecting different options to see conditional logic in action. - -[Exit Preview] -``` - -**Behavior:** -- User selects "Yes" → Dietary field appears -- User selects "No" → Dietary field disappears - -**Design notes:** -- Clear indication of preview mode -- Real-time updates (no reload) - ---- - -## Microcopy Guidelines - -### Field Labels - -**Be concise and conversational** - -❌ Bad: "Please enter your electronic mail address" -✅ Good: "Email" - -❌ Bad: "Input telephone number (optional)" -✅ Good: "Phone (optional)" - -**Use sentence case, not title case** - -❌ Bad: "First Name" -✅ Good: "First name" - -❌ Bad: "Company Name (If Applicable)" -✅ Good: "Company name (if applicable)" - ---- - -### Help Text - -**Explain why or provide examples** - -**Email field:** -❌ Bad: "Enter email" -✅ Good: "We'll send a confirmation to this email" - -**Phone field:** -❌ Bad: "Phone number" -✅ Good: "We'll only call if there's an issue with your order" - -**File upload:** -❌ Bad: "Upload file" -✅ Good: "Upload your resume (PDF or DOCX, max 10MB)" - ---- - -### Error Messages - -**Be specific and actionable** - -**Email validation:** -❌ Bad: "Invalid email" -✅ Good: "Please enter a valid email (e.g., name@example.com)" - -**Required field:** -❌ Bad: "This field is required" -✅ Good: "Please enter your name" - -**File size:** -❌ Bad: "File too large" -✅ Good: "File must be under 10MB. Yours is 15MB. Try compressing it." - -**Payment failed:** -❌ Bad: "Transaction error" -✅ Good: "Your card was declined. Please check your card details or try a different card." - ---- - -### Success Messages - -**Be enthusiastic but not over-the-top** - -**Form submission:** -❌ Bad: "Form submitted" -✅ Good: "Thanks! We'll get back to you within 24 hours." - -**Payment successful:** -❌ Bad: "Payment processed successfully. Transaction ID: 1234." -✅ Good: "✅ Payment received! Receipt sent to your email." - -**Registration:** -❌ Bad: "Account created. Please log in." -✅ Good: "Welcome! Check your email to verify your account." - ---- - -### Button Labels - -**Use verbs that describe the action** - -❌ Bad: "Submit" -✅ Good: "Send Message" - -❌ Bad: "Click Here" -✅ Good: "Download Receipt" - -❌ Bad: "Proceed" -✅ Good: "Continue to Payment" - -**For payment buttons, include amount** - -❌ Bad: "Pay" -✅ Good: "Pay $50.00" - -❌ Bad: "Subscribe" -✅ Good: "Subscribe for $9.99/month" - ---- - -### Settings & Options - -**Explain consequences, not just features** - -**Email notification:** -❌ Bad: "Send email notification" -✅ Good: "Email me when someone submits this form" - -**Required field:** -❌ Bad: "Required" -✅ Good: "Make this field required" (checkbox label) - -**Conditional logic:** -❌ Bad: "Enable conditional logic" -✅ Good: "Show or hide this field based on other answers" - ---- - -## Visual Design Patterns - -### Empty States - -**When user has no forms yet:** - -``` -┌─────────────────────────────────────────┐ -│ │ -│ 📝 │ -│ │ -│ No forms yet │ -│ │ -│ Forms help you collect information │ -│ from your website visitors. │ -│ │ -│ [Create Your First Form] │ -│ │ -└─────────────────────────────────────────┘ -``` - -**Copy principles:** -- Icon relevant to context -- 1-sentence explanation -- Clear call-to-action button - ---- - -**When form has no submissions:** - -``` -┌─────────────────────────────────────────┐ -│ │ -│ 📭 │ -│ │ -│ No submissions yet │ -│ │ -│ Share this form to start collecting │ -│ responses. │ -│ │ -│ [Copy Form Link] [Embed on Page] │ -│ │ -└─────────────────────────────────────────┘ -``` - -**Design notes:** -- Actionable next steps -- Multiple options (link vs embed) - ---- - -### Loading States - -**Form submission in progress:** - -``` -┌─────────────────────────────────────┐ -│ ⏳ Sending... │ -│ │ -│ Please don't close this page. │ -└─────────────────────────────────────┘ -``` - -**AI form generation:** - -``` -┌─────────────────────────────────────┐ -│ ✨ Creating your form... │ -│ │ -│ [████████░░] 80% │ -│ │ -│ Adding email validation... │ -└─────────────────────────────────────┘ -``` - -**Design notes:** -- Spinner or progress indicator -- Explain what's happening -- Show progress if possible - ---- - -### Confirmation Dialogs - -**Deleting a form:** - -``` -┌─────────────────────────────────────┐ -│ Delete "Contact Form"? │ -│ │ -│ This will permanently delete: │ -│ • The form │ -│ • 47 submissions │ -│ • All settings │ -│ │ -│ This cannot be undone. │ -│ │ -│ [Cancel] [Delete Form] │ -└─────────────────────────────────────┘ -``` - -**Design notes:** -- Specific about what will be deleted -- Use red/destructive style for delete button -- Cancel button should be default (easier to accidentally click) - ---- - -### Tooltips & Hints - -**When to use:** -- Explaining technical terms -- Providing examples -- Showing keyboard shortcuts - -**Format:** - -``` -Field Name [?] - ↓ (on hover) -┌─────────────────────────────────────┐ -│ This is the internal name used in │ -│ the database. │ -│ │ -│ Example: "first_name" │ -└─────────────────────────────────────┘ -``` - -**Guidelines:** -- Keep under 2 sentences -- Provide example if helpful -- Don't repeat the label - ---- - -## Accessibility - -### Screen Reader Text - -**Form structure:** - -```html -
-
-

Personal Information

- -
-
-``` - -**Error announcements:** - -```html -
- Please fix 2 errors before submitting. -
- - - - Please enter a valid email address - -``` - ---- - -### Keyboard Navigation - -**Required interactions:** -- Tab through all fields -- Space to toggle checkboxes/radio buttons -- Enter to submit form -- Escape to close modals - -**Visual focus indicators:** - -```css -.srfm-field input:focus { - outline: 2px solid #0073aa; - outline-offset: 2px; -} -``` - -**Never:** -```css -:focus { - outline: none; /* ❌ Never remove focus outline */ -} -``` - ---- - -### Color Contrast - -**WCAG 2.1 Level AA Requirements:** -- Normal text: 4.5:1 minimum -- Large text (18pt+): 3:1 minimum -- UI components: 3:1 minimum - -**Test tools:** -- WebAIM Contrast Checker -- Chrome DevTools (Lighthouse) - -**Safe color combinations:** -- White text on dark backgrounds (#333 or darker) -- Dark text on light backgrounds (#F0F0F0 or lighter) -- Avoid gray text on gray backgrounds - ---- - -## Tone & Voice - -### Voice Characteristics - -**Friendly but professional** -- Use "we" and "you" -- Conversational, not formal -- Helpful, not condescending - -**Example:** -❌ Formal: "The system has detected an error in your input." -✅ Friendly: "Oops! We couldn't save that. Check for any errors above." - ---- - -**Clear over clever** -- Avoid puns and wordplay -- Be direct -- Technical accuracy over marketing fluff - -**Example:** -❌ Clever: "Houston, we have a problem!" -✅ Clear: "Something went wrong. Please try again." - ---- - -**Empowering, not blaming** -- Focus on solutions, not problems -- Use "we" for errors, "you" for successes - -**Example:** -❌ Blaming: "You entered an invalid email." -✅ Empowering: "Hmm, that email doesn't look quite right. Mind double-checking it?" - ---- - -### Writing for Different Contexts - -**First-time users:** -- More explanation -- Examples provided -- Encouraging tone - -**Example:** -``` -Welcome to SureForms! - -Creating your first form is easy. We'll walk you through each step. - -Let's start by choosing what kind of form you need. -``` - ---- - -**Power users:** -- Less explanation -- Assume knowledge -- Efficiency-focused - -**Example:** -``` -Advanced Settings - -Show field if: [condition builder] -Custom CSS class: [input] -``` - ---- - -**Error states:** -- Apologetic but not dramatic -- Specific about what went wrong -- Clear next steps - -**Example:** -``` -We couldn't save your changes. - -The connection to the server timed out. - -Please try again. If this keeps happening, check your internet connection. - -[Try Again] -``` - ---- - -**Success states:** -- Positive reinforcement -- What happens next -- Optional next action - -**Example:** -``` -✅ Form published! - -Your form is now live at: -https://yoursite.com/contact - -[View Form] [Create Another] -``` - ---- - -## Form Field Best Practices - -### Required vs Optional - -**Default:** Make fields optional - -**Require only when absolutely necessary:** -- Name (if sending personalized response) -- Email (if you need to contact them) -- Payment details (if collecting payment) - -**Label optional fields:** -``` -Phone (optional) -Company name (optional) -``` - -**Don't label required fields:** -❌ Bad: "Email (required)" -✅ Good: "Email" (with * indicator) - ---- - -### Field Order - -**Logical flow:** -1. Personal info (Name, Email) -2. Specific questions -3. Payment details (if applicable) -4. Submit button - -**Example contact form:** -``` -1. Name -2. Email -3. Subject -4. Message -5. [Submit] -``` - -**Example order form:** -``` -1. Name -2. Email -3. Product selection -4. Quantity -5. Payment details -6. [Complete Purchase] -``` - ---- - -### Placeholder Text - -**Use sparingly** - -Only for examples, not as labels: - -❌ Bad: -``` -Label: [empty] -Placeholder: "Enter your email" -``` - -✅ Good: -``` -Label: Email -Placeholder: "name@example.com" -``` - -**Never use placeholders as only label** (accessibility issue) - ---- - -### Multi-Step Forms (Pro) - -**Step indicators:** - -``` -Step 1 of 3: Your Information -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -●━━━━━━━━━━━━○━━━━━━━━━━━━━○ - -[Fields here] - -[Continue to Step 2 →] -``` - -**Navigation:** -- Show progress (Step 1 of 3) -- Visual progress bar -- Allow back navigation (not just forward) - -**Button labels:** -- Step 1: "Continue" (not "Next") -- Step 2: "Continue" -- Step 3: "Submit Form" (or "Complete Order") - ---- - -## Internationalization (i18n) - -### Text Translation - -**All user-facing strings must be translatable:** - -```php -// ✅ Good -__('Email', 'sureforms'); -_e('Submit Form', 'sureforms'); - -// ❌ Bad -echo 'Email'; // Hardcoded English -``` - -**Placeholders for dynamic content:** - -```php -// ✅ Good -sprintf(__('You have %d new submissions', 'sureforms'), $count); - -// ❌ Bad -echo "You have $count new submissions"; -``` - ---- - -### Date & Number Formatting - -**Use WordPress functions:** - -```php -// Dates -echo date_i18n(get_option('date_format'), $timestamp); - -// Numbers -echo number_format_i18n($number); - -// Currency -echo '$' . number_format_i18n($amount, 2); -``` - ---- - -### RTL Support - -**CSS for right-to-left languages:** - -```css -/* Use logical properties */ -.srfm-field { - margin-inline-start: 10px; /* Not margin-left */ - padding-inline-end: 20px; /* Not padding-right */ -} -``` - -**Test with RTL languages:** -- Arabic -- Hebrew -- Persian - ---- - -## Quality Checklist - -Before shipping UI copy: - -- [ ] Spell check passed -- [ ] Grammar correct -- [ ] Tone matches guidelines -- [ ] Actionable error messages -- [ ] Examples provided where helpful -- [ ] Accessibility: screen reader friendly -- [ ] Accessibility: color contrast ≥ 4.5:1 -- [ ] Keyboard navigation works -- [ ] i18n: All strings translatable -- [ ] RTL languages supported -- [ ] Tested with real users (if major UI) - ---- - -**Next:** [FAQ](faq.md) diff --git a/tests/play/specs/CLAUDE.md b/tests/play/specs/CLAUDE.md deleted file mode 100644 index 113b91abd..000000000 --- a/tests/play/specs/CLAUDE.md +++ /dev/null @@ -1,324 +0,0 @@ -# Playwright E2E Specs — Writing Guide - -Rules and patterns for writing reliable SureForms E2E tests. Each section is a lesson learned from real failures. - ---- - -## Auth: Never do a full form-login inside tests - -**Pattern:** `globalSetup` logs in once and saves `storageState.json`. All workers restore it. - -**Why:** Multiple workers calling the WordPress login form simultaneously causes nonce collisions — WordPress redirects to `?reauth=1`, breaking every test in those workers. - -**Correct:** Let `storageState` handle auth. The `loginAsAdmin` util now just navigates to `/wp-admin` and relies on the pre-loaded cookies. - -```js -// DO — storageState is already loaded by playwright.config.js -test.beforeEach(async ({ page }) => { - await loginAsAdmin(page); // navigates to /wp-admin, skips login form -}); - -// DON'T — never re-do a full form login inside specs -await page.goto('/wp-login.php'); -await page.fill('#user_login', 'admin'); -``` - ---- - -## Adding blocks: sidebar vs programmatic insertion - -**Only these blocks appear in the quick-action sidebar by default:** -`input`, `email`, `textarea`, `checkbox`, `number`, `inline-button`, `advanced-heading`, `payment` - -**All other blocks** (`phone`, `url`, `multi-choice`, `dropdown`, `gdpr`, `rating`, `date`, etc.) **must be inserted programmatically.** - -Use `addFieldBlock(page, 'slug')` from `formHelpers.js` — it automatically routes sidebar blocks via click and all other blocks via `insertBlock`. - -```js -// This handles routing automatically -await addFieldBlock(page, 'gdpr'); // programmatic -await addFieldBlock(page, 'input'); // sidebar click -await addFieldBlock(page, 'dropdown'); // programmatic -``` - -**Never** try to open the WP block inserter toolbar to add non-sidebar blocks — it is fragile and frequently breaks tests. - ---- - -## WP 6.9+ iframe editor: no `data-type`, use data store - -WP 6.9 renders the block editor inside an `iframe[name="editor-canvas"]` and **removed the `data-type` attribute** from block wrapper elements. - -**Never** use `page.locator('.wp-block[data-type="srfm/..."]')` — it will find nothing. - -To **select a block** (e.g. to open its settings panel), use `selectBlock(page, 'slug')` from `formHelpers.js`. It dispatches `selectBlock` via the Gutenberg data store: - -```js -const { selectBlock } = require('../utils/formHelpers'); - -await addFieldBlock(page, 'email'); -await selectBlock(page, 'email'); // selects the block via data store -await openBlockSettingsTab(page); // now the sidebar shows this block's settings -``` - -To **verify a block exists**, `addFieldBlock` already checks via `wp.data.select('core/block-editor').getBlocks()` — no need for a separate DOM assertion. - ---- - -## Field CSS selectors - -Always use the most specific class. Do not rely on element order (`.nth(N)`) unless you are certain only N elements exist. - -| Field | Frontend input selector | -|---|---| -| Single line input | `input.srfm-input-input` | -| Email | `input.srfm-input-email` | -| Email confirm | `input.srfm-input-email-confirm` — **NOT** `input.srfm-input-email` | -| Phone | `input.srfm-input-phone` | -| Number | `input.srfm-input-number` | -| URL | `input.srfm-input-url` | -| Textarea | `textarea.srfm-textarea-text` | -| Checkbox | `input[type="checkbox"]` inside `.srfm-checkbox-input-wrap` | -| Submit button | `#srfm-submit-btn` | -| Success box | `.srfm-success-box` | -| Error message | `.srfm-error-message` | - ---- - -## TomSelect dropdowns - -The SureForms dropdown field uses TomSelect. The native ``. Filling it simulates a bot: - -```js -await page.evaluate(() => { - const f = document.querySelector('[name="srfm-honeypot-field"]'); - if (f) f.value = 'spambot'; -}); -``` - -Server returns `wp_send_json_error` → `.srfm-success-box` must NOT appear. - -## Helpful utilities - -All utilities live in `tests/play/utils/`. - -| Utility | Purpose | -|---|---| -| `loginAsAdmin(page)` | Navigates to `/wp-admin`, restores auth from storageState | -| `createBlankForm(page)` | Creates a new blank SureForms form in the editor | -| `addFieldBlock(page, slug)` | Adds a field block (sidebar or programmatic) | -| `selectBlock(page, slug)` | Selects a block in the editor via the data store (iframe-safe) | -| `publishFormAndGetURL(page)` | Publishes the current form and returns its front-end URL | -| `openFormSettingsDialog(page, navItem)` | Opens Form Behavior dialog to a specific nav item | -| `setFormTitle(page, title)` | Sets the editor post title via `editPost` (avoids fragile UI title input) | -| `createWPPage(page, title, content)` | Creates a published WP page via REST API | - -When a new utility is needed, add it here rather than inlining complex selectors in spec files. diff --git a/tests/play/specs/TODO.md b/tests/play/specs/TODO.md deleted file mode 100644 index fd594df9a..000000000 --- a/tests/play/specs/TODO.md +++ /dev/null @@ -1,244 +0,0 @@ -# E2E Test Plan — SureForms Free Plugin - -Track progress for all Playwright test cases. -**Update this file every time a test is added or a test ID changes.** - -Last updated: 2026-03-16 -Branch convention: `test/e2e-p{N}-test-cases` - ---- - -## How to update this file - -- When you **add a test**, tick its checkbox and fill in the `Spec file` column. -- When you **add a new test ID** that wasn't listed here, add a row in the correct section. -- Fix the `Last updated` date above. - ---- - -## Coverage status key - -| Symbol | Meaning | -|---|---| -| ✅ | Implemented and passing | -| ⏭️ | Implemented but skipped (requires external keys / infra) | -| ❌ | Not yet implemented | -| 🔢 | Implemented but test ID needs renumbering | - ---- - -## Group 1 — Field types (basic submit) - -All tests live in `field-types.spec.js`. -Single-line input and email are covered in `form-creation-submission.spec.js` (not repeated here). - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 1.1 | Textarea — submit multi-line text | ✅ | field-types.spec.js | -| 1.2 | Number — submit a valid number | ✅ | field-types.spec.js | -| 1.3 | Phone — submit a valid phone number | ✅ | field-types.spec.js | -| 1.4 | URL — submit a valid URL | ✅ | field-types.spec.js | -| 1.5 | Checkbox — check and submit | ✅ | field-types.spec.js | -| 1.6 | Multi-choice (radio) — pick one option and submit | ✅ | field-types.spec.js | -| 1.7 | Multi-choice (multi-select) — pick multiple options and submit | ✅ | field-types.spec.js | -| 1.8 | Dropdown — select one option and submit | ✅ | field-types.spec.js | -| 1.9 | *(reserved — inline-button / advanced-heading behaviour TBD)* | ❌ | — | -| 1.10 | GDPR — check consent and submit | ✅ | field-types.spec.js | - ---- - -## Group 2 — Field validation - -All tests live in `field-validation.spec.js`. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 2.1 | Email — invalid format shows error | ✅ | field-validation.spec.js | -| 2.2 | Email confirmation — mismatch shows error | ✅ | field-validation.spec.js | -| 2.3 | Email confirmation — matching emails submit successfully | ✅ | field-validation.spec.js | -| 2.4 | URL — invalid format shows error | ✅ | field-validation.spec.js | -| 2.5 | Number — value below minimum shows error | ✅ | field-validation.spec.js | -| 2.6 | Number — value above maximum shows error | ✅ | field-validation.spec.js | -| 2.7 | Required single-line input — submit empty shows error | ✅ | field-validation.spec.js | -| 2.8 | Required email — submit empty shows error | ✅ | field-validation.spec.js | -| 2.9 | Required phone — submit empty shows error | ✅ | field-validation.spec.js | -| 2.10 | Required textarea — submit empty shows error | ✅ | field-validation.spec.js | -| 2.11 | Required checkbox — submit unchecked shows error | ✅ | field-validation.spec.js | -| 2.12 | Required GDPR — submit without consent shows error | ✅ | field-validation.spec.js | -| 2.13 | Required dropdown — submit without selection shows error | ✅ | field-validation.spec.js | -| 2.14 | Required multi-choice — submit without selection shows error | ✅ | field-validation.spec.js | -| 2.15 | Character limit — input/textarea rejects text over max-chars | ❌ | — | - ---- - -## Group 3 — Form settings (confirmation behaviour) - -All tests live in `form-settings.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 3.1 | Redirect on submission — user lands on configured URL | ✅ | form-settings.spec.js | -| 3.2 | Custom success message — configured text shown after submit | ✅ | form-settings.spec.js | -| 3.3 | Store entries disabled — submission succeeds but no entry stored | ✅ | form-settings.spec.js | -| 3.4 | Page confirmation type — redirect to a configured WordPress page | ✅ | form-settings.spec.js | -| 3.5 | Store entries enabled (default) — entry appears in admin after submit | ✅ | form-settings.spec.js | -| 3.6 | Per-form GDPR compliance: do-not-store-entries enabled → no entry created | ✅ | form-settings.spec.js (covered by 3.3) | -| 3.7 | Per-form compliance: auto-delete setting saves and is visible in editor | ❌ | — | - ---- - -## Group 4 — Entries management - -All tests live in `entries.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 4.1 | Submitted entry appears in the admin entries list | ✅ | entries.spec.js | -| 4.2 | Entry detail contains the correct submitted field values | ✅ | entries.spec.js | -| 4.3 | Bulk delete removes selected entries | ✅ | entries.spec.js | -| 4.4 | CSV export downloads a file | ✅ | entries.spec.js | -| 4.5 | Mark entry as read / unread — status updates in the admin list | ✅ | entries.spec.js | - ---- - -## Group 5 — Form embedding - -All tests live in `form-embedding.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 5.1 | Shortcode-embedded form renders on a page | ✅ | form-embedding.spec.js | -| 5.2 | Shortcode-embedded form submits successfully | ✅ | form-embedding.spec.js | -| 5.3 | Gutenberg block-embedded form renders on a page | ✅ | form-embedding.spec.js | -| 5.4 | Gutenberg block-embedded form submits successfully | ✅ | form-embedding.spec.js | - ---- - -## Group 6 — Email notifications - -All tests live in `email-notifications.spec.js`. -All tests are currently skipped — require MailHog wired into the wp-env Docker setup. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 6.1 | Admin notification email is sent after form submission | ⏭️ | email-notifications.spec.js | -| 6.2 | Notification email body contains submitted field values | ⏭️ | email-notifications.spec.js | -| 6.3 | User confirmation email is sent to submitter's address | ❌ | — | - ---- - -## Group 7 — CAPTCHA - -All tests live in `captcha.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 7.2 | reCAPTCHA v2 — submit without solving is blocked | ⏭️ | captcha.spec.js | -| 7.3 | Cloudflare Turnstile — submit without solving is blocked | ❌ | — | -| 7.4 | hCaptcha — submit without solving is blocked | ❌ | — | -| 7.5 | reCAPTCHA v3 — submit with low score is blocked | ❌ | — | -| 7.7 | CAPTCHA disabled — form submits normally without CAPTCHA widget | ✅ | captcha.spec.js | - -> **Note:** 7.3–7.5 require external API keys; add them with `test.skip` pattern matching 7.2. - ---- - -## Group 8 — Payments (Stripe) - -All tests live in `payments.spec.js`. -All tests are currently skipped — require Stripe test keys. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 8.1 | Payment field renders the Stripe card widget | ⏭️ | payments.spec.js | -| 8.2 | Valid Stripe test card — payment succeeds and success message shown | ⏭️ | payments.spec.js | -| 8.3 | Declined Stripe test card — error message shown | ⏭️ | payments.spec.js | -| 8.6 | Successful payment creates an entry in the admin entries list | ⏭️ | payments.spec.js | - ---- - -## Group 9 — Global settings - -All tests live in `global-settings.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 9.1 | Global settings page loads without a JS error | ✅ | global-settings.spec.js | -| 9.2 | All main settings tabs are clickable and stay on settings page | ✅ | global-settings.spec.js | - ---- - -## Group 10 — Spam protection - -All tests live in `spam-protection.spec.js`. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 10.1 | Honeypot — bot fills hidden field → submission is blocked | ✅ | spam-protection.spec.js | -| 10.2 | Double-submit prevention — submit button disabled after first click | ✅ | spam-protection.spec.js | - ---- - -## Group 11 — Form restrictions - -All tests live in `form-restrictions.spec.js`. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 11.1 | Entry limit — form shows restriction message after limit reached | ✅ | form-restrictions.spec.js | -| 11.2 | Scheduling — restriction message shown when schedule has ended | ✅ | form-restrictions.spec.js | -| 11.3 | Scheduling — restriction message shown when schedule has not started yet | ✅ | form-restrictions.spec.js | - ---- - -## Group 12 — Form lifecycle - -All tests live in `form-lifecycle.spec.js`. - -> **✅ Renaming done:** Comments in `form-lifecycle.spec.js` updated from `8.x` to `12.x`. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 12.1 | Duplicate form — copy appears in forms list with "(Copy)" suffix | ✅ | form-lifecycle.spec.js | -| 12.2 | Trash form — form disappears from All Forms view | ✅ | form-lifecycle.spec.js | -| 12.3 | Restore form — form reappears in All Forms view after restore from Trash | ✅ | form-lifecycle.spec.js | -| 12.4 | Delete permanently — form is fully removed and not restorable | ✅ | form-lifecycle.spec.js | - ---- - -## Group 13 — AI form generation - -All tests will live in `ai-form-builder.spec.js`. -Tests that require an API key should follow the `test.skip` pattern used in payments and CAPTCHA. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 13.1 | AI generation panel opens from the "Create Form" flow | ❌ | — | -| 13.2 | Generating a form with a prompt produces at least one block | ❌ | — | - ---- - -## Suggested implementation order - -Work these in priority order — top of each section first: - -### Next up (P1 — no external dependencies) - -- [x] **11.3** Scheduling not-started-yet *(done — form-restrictions.spec.js)* -- [x] **12.4** Delete permanently from trash *(done — form-lifecycle.spec.js)* -- [x] **2.7–2.10** Required validation for remaining field types *(done — field-validation.spec.js)* -- [x] **4.5** Entry read/unread status toggle *(done — entries.spec.js)* -- [x] **3.6** Per-form do-not-store-entries compliance *(already covered by test 3.3)* -- [ ] **2.15** Character limit — low E2E value (browser `maxlength` attribute, no server-side check); defer unless a custom error message is confirmed - -### Later (P2 — needs infra or API keys, use skip pattern) - -- [ ] **6.3** User confirmation email (needs MailHog) -- [ ] **7.3–7.5** Additional CAPTCHA types (need external keys) -- [ ] **13.1–13.2** AI form generation (needs AI API key) - -### Nice to have - -- [ ] **3.7** Auto-delete entries compliance setting -- [ ] **9.x** Global settings save (email From Name/From Email) -- [ ] **1.9** Inline button / advanced heading block behaviour