From ec153415298860c002abfc9332e3beb7360d6267 Mon Sep 17 00:00:00 2001
From: Avinash Kumar Sharma
Date: Mon, 28 Sep 2026 16:06:23 +0530
Subject: [PATCH 001/116] fix(multi-choice): prevent shared options reference
when block is duplicated
deleteOption() was calling options.splice() which mutated the original
array in-place. When a block is duplicated in Gutenberg the new block's
attributes initially share the same array reference, so the mutation
affected both blocks simultaneously.
Additionally, setAttributes received `deleteOptions` as the key instead
of `options`, making the update a no-op and leaving the shared reference
intact.
Fixes by replacing splice/map with filter (immutable) and correcting
the setAttributes key to `options`.
Fixes #3174
Co-Authored-By: Claude Sonnet 4.6
---
src/blocks/multi-choice/edit.js | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/src/blocks/multi-choice/edit.js b/src/blocks/multi-choice/edit.js
index 6ca3740f4..5600bc66d 100644
--- a/src/blocks/multi-choice/edit.js
+++ b/src/blocks/multi-choice/edit.js
@@ -71,13 +71,9 @@ const Edit = ( props ) => {
const prevSingleSelection = useRef( singleSelection );
const deleteOption = ( index ) => {
- const deleteOptions = options.map( ( item, thisIndex ) => {
- if ( index === thisIndex ) {
- options.splice( index, 1 );
- item = { options };
- }
- return item;
- } );
+ const newOptions = options.filter(
+ ( _, thisIndex ) => thisIndex !== index
+ );
// Update preselected options when an option is deleted
const newPreselected = preselectedOptions
@@ -85,7 +81,7 @@ const Edit = ( props ) => {
.map( ( i ) => ( i > index ? i - 1 : i ) ); // Adjust indices
setAttributes( {
- deleteOptions,
+ options: newOptions,
preselectedOptions: newPreselected,
} );
};
From e638cc3896e6aa5a7b982ace14dc17423dadb7bc Mon Sep 17 00:00:00 2001
From: Vansh Kapoor
Date: Mon, 28 Sep 2026 16:18:13 +0530
Subject: [PATCH 002/116] fix: keep the submit button disabled while a redirect
confirmation navigates
handleFormSubmission() re-enabled the submit button and hid the loader
right after redirectToUrl(), while the old page was still on screen. A
second tap in that window resubmitted into a request the unload then
cancelled (Safari: "TypeError: Load failed"). submitFormData() logged it
as type 'error', which is_fault() counts, so one tap sent the site owner
the "form submissions are failing" alert, and the visitor saw the raw
browser text.
- After a redirect that leaves the page, keep the button disabled and the
loader on. Release them on a back/forward cache restore, or after 15s
for a redirect that never unloads the page (e.g. a file download). A
hash-only URL on the same page does not unload, so it releases at once.
- Track navigatingAway (set by redirectToUrl() and pagehide, cleared on a
bfcache restore). A request that fails while it is set is logged as
'network' with no status, which is_fault() does not count.
- Do not pass native TypeError / SyntaxError text to showErrorMessage();
the empty message falls back to the translated generic error, as Pro's
login handler already does.
- Test: pin that a status-less 'network' entry is not a fault.
Fixes #3173
---
assets/js/unminified/form-submit.js | 101 +++++++++++++++++++++++---
tests/unit/inc/test-client-logger.php | 10 +++
2 files changed, 102 insertions(+), 9 deletions(-)
diff --git a/assets/js/unminified/form-submit.js b/assets/js/unminified/form-submit.js
index 10c514aa4..e1bae6521 100644
--- a/assets/js/unminified/form-submit.js
+++ b/assets/js/unminified/form-submit.js
@@ -760,14 +760,29 @@ async function submitFormData( form ) {
// "TypeError: Failed to fetch" with a short duration is itself the
// diagnosis: the request never reached PHP -- blocked, offline or CORS.
+ //
+ // While the page is leaving, the browser cancels the request itself
+ // (Safari: "TypeError: Load failed"). That is logged as 'network' with no
+ // status, which Client_Logger::is_fault() does not count, so it cannot
+ // raise the "form submissions are failing" alert.
+ const message = `${ e?.name ?? 'Error' }: ${
+ typeof e?.message === 'string' ? e.message : ''
+ }${ e?.code ? ` (${ e.code })` : '' }`;
srfmLog.add( {
- type: 'error',
+ type: navigatingAway ? 'network' : 'error',
duration_ms: Math.round( performance.now() - startedAt ),
- message: `${ e?.name ?? 'Error' }: ${
- typeof e?.message === 'string' ? e.message : ''
- }${ e?.code ? ` (${ e.code })` : '' }`,
+ message: navigatingAway
+ ? `Submission cancelled as the page was unloading: ${ message }`
+ : message,
} );
+ // showErrorMessage() writes the message with innerHTML, and a native fetch
+ // or JSON error is raw browser text ("Load failed", or a SyntaxError
+ // quoting an HTML challenge page). An empty message shows the translated
+ // generic error instead, as the Pro login handler does.
+ const isRawBrowserError =
+ e instanceof TypeError || e instanceof SyntaxError;
+
/**
* Surface the server's own message (e.g. an expired submission token, a
* failed CAPTCHA, a REST route blocked by a firewall) instead of returning
@@ -778,7 +793,10 @@ async function submitFormData( form ) {
return {
success: false,
data: {
- message: typeof e?.message === 'string' ? e.message : '',
+ message:
+ ! isRawBrowserError && typeof e?.message === 'string'
+ ? e.message
+ : '',
log_message: e?.code
? `SureForms submission failed (${ e.code }).`
: null,
@@ -914,8 +932,49 @@ function showSuccessMessage(
}
}
+/**
+ * True once this page has started to leave: a redirect confirmation or any
+ * other navigation. The browser cancels in-flight requests as the page unloads,
+ * so a request that fails in this window is not a sign the site is broken.
+ * Reset when the page comes back from the back/forward cache.
+ */
+let navigatingAway = false;
+
+window.addEventListener( 'pagehide', () => {
+ navigatingAway = true;
+} );
+
+window.addEventListener( 'pageshow', ( event ) => {
+ if ( event.persisted ) {
+ navigatingAway = false;
+ }
+} );
+
+/**
+ * Navigate to the confirmation URL.
+ *
+ * @param {string} url Redirect URL.
+ * @return {boolean} True when the page will unload. False for a URL that only
+ * changes the hash of the current page, which keeps it on screen.
+ */
function redirectToUrl( url ) {
+ let leavesPage = true;
+ try {
+ const target = new URL( url, window.location.href );
+ const here = new URL( window.location.href );
+ leavesPage = ! (
+ target.hash &&
+ target.origin === here.origin &&
+ target.pathname === here.pathname &&
+ target.search === here.search
+ );
+ } catch ( e ) {
+ // Not a parseable URL. The browser decides; assume it navigates.
+ }
+
+ navigatingAway = leavesPage;
window.location.assign( url );
+ return leavesPage;
}
/**
@@ -1216,13 +1275,37 @@ async function handleFormSubmission(
submitType,
loader
);
+ } else if (
+ formStatus?.redirect_url &&
+ redirectToUrl( formStatus.redirect_url )
+ ) {
+ // Keep the button disabled and the loader on while the browser
+ // navigates. The old page can stay on screen for seconds, and a
+ // second tap would resubmit into a request the unload then cancels.
+ let fallbackTimer = 0;
+ const release = () => {
+ window.clearTimeout( fallbackTimer );
+ loader.classList.remove( 'srfm-active' );
+ enableSubmitButton( form );
+ };
+ // Back/forward cache restore: the page is back, so is the form.
+ window.addEventListener(
+ 'pageshow',
+ ( pageEvent ) => {
+ if ( pageEvent.persisted ) {
+ release();
+ }
+ },
+ { once: true }
+ );
+ // ponytail: fixed 15s fallback for a redirect that never unloads the
+ // page (e.g. a URL served as a file download). Make it configurable
+ // if a real slow redirect ever takes longer.
+ fallbackTimer = window.setTimeout( release, 15000 );
} else {
- if ( formStatus?.redirect_url ) {
- redirectToUrl( formStatus?.redirect_url );
- }
loader.classList.remove( 'srfm-active' );
- // Re-enable submit button after redirect.
+ // Re-enable submit button: no redirect left the page.
enableSubmitButton( form );
}
} else {
diff --git a/tests/unit/inc/test-client-logger.php b/tests/unit/inc/test-client-logger.php
index 675ac4405..eef24ec9d 100644
--- a/tests/unit/inc/test-client-logger.php
+++ b/tests/unit/inc/test-client-logger.php
@@ -814,6 +814,16 @@ public function test_is_fault_ignores_an_ordinary_rejection() {
$this->assertFalse( Client_Logger::is_fault( [ 'type' => 'network', 'status' => 200 ] ) );
}
+ /**
+ * A 'network' entry with no status means no response came back. form-submit.js
+ * logs a submission the browser cancelled while the page was unloading this
+ * way, and that says nothing about whether the site can take submissions.
+ */
+ public function test_is_fault_ignores_a_network_entry_without_a_status() {
+ $this->assertFalse( Client_Logger::is_fault( [ 'type' => 'network' ] ) );
+ $this->assertFalse( Client_Logger::is_fault( Client_Logger::sanitize_entry( [ 'type' => 'network', 'message' => 'TypeError: Load failed' ] ) ) );
+ }
+
/**
* The notice appears only after a run of faults, so one bad request on an
* otherwise healthy site says nothing.
From e9771eca3e3df09f14d8b75646de30cf0bf5ea3f Mon Sep 17 00:00:00 2001
From: Vansh Kapoor
Date: Mon, 28 Sep 2026 16:23:11 +0530
Subject: [PATCH 003/116] fix: release non-unloading redirects at once and
clear the flag on fallback
From the self-review of #3175:
- mailto:, tel: and other non-http(s) URLs hand off to another app and
never unload the page, so they release the button at once instead of
after 15s.
- A URL ending in an empty "#" is a same-page fragment navigation too.
- The 15s fallback proves the page did not leave, so it clears
navigatingAway. Otherwise a later real failure on that page would be
logged as a non-fault. If the navigation commits later, pagehide sets
the flag again before the unload.
- release() removes its pageshow listener, so repeated non-unloading
redirects do not stack listeners.
- Reword the raw-error comment as the rule it enforces.
---
assets/js/unminified/form-submit.js | 53 ++++++++++++++++-------------
1 file changed, 30 insertions(+), 23 deletions(-)
diff --git a/assets/js/unminified/form-submit.js b/assets/js/unminified/form-submit.js
index e1bae6521..bf0c6e8f4 100644
--- a/assets/js/unminified/form-submit.js
+++ b/assets/js/unminified/form-submit.js
@@ -776,10 +776,10 @@ async function submitFormData( form ) {
: message,
} );
- // showErrorMessage() writes the message with innerHTML, and a native fetch
- // or JSON error is raw browser text ("Load failed", or a SyntaxError
- // quoting an HTML challenge page). An empty message shows the translated
- // generic error instead, as the Pro login handler does.
+ // Only server-authored messages are shown to the visitor. A native fetch or
+ // JSON error is raw browser text ("Load failed", or a SyntaxError about an
+ // HTML challenge page), so an empty message shows the translated generic
+ // error instead, as the Pro login handler does.
const isRawBrowserError =
e instanceof TypeError || e instanceof SyntaxError;
@@ -954,20 +954,23 @@ window.addEventListener( 'pageshow', ( event ) => {
* Navigate to the confirmation URL.
*
* @param {string} url Redirect URL.
- * @return {boolean} True when the page will unload. False for a URL that only
- * changes the hash of the current page, which keeps it on screen.
+ * @return {boolean} True when the page will unload. False when it stays on
+ * screen: a URL that only changes the fragment of the current
+ * page (including an empty `#`), or a non-http(s) URL such as
+ * mailto: or tel:, which hands off to another app.
*/
function redirectToUrl( url ) {
let leavesPage = true;
try {
const target = new URL( url, window.location.href );
const here = new URL( window.location.href );
- leavesPage = ! (
- target.hash &&
+ const isHttp = [ 'http:', 'https:' ].includes( target.protocol );
+ const isFragmentOnly =
+ String( url ).includes( '#' ) &&
target.origin === here.origin &&
target.pathname === here.pathname &&
- target.search === here.search
- );
+ target.search === here.search;
+ leavesPage = isHttp && ! isFragmentOnly;
} catch ( e ) {
// Not a parseable URL. The browser decides; assume it navigates.
}
@@ -1283,25 +1286,29 @@ async function handleFormSubmission(
// navigates. The old page can stay on screen for seconds, and a
// second tap would resubmit into a request the unload then cancels.
let fallbackTimer = 0;
+ // Back/forward cache restore: the page is back, so is the form.
+ const onPageShow = ( pageEvent ) => {
+ if ( pageEvent.persisted ) {
+ release();
+ }
+ };
const release = () => {
window.clearTimeout( fallbackTimer );
+ window.removeEventListener( 'pageshow', onPageShow );
loader.classList.remove( 'srfm-active' );
enableSubmitButton( form );
};
- // Back/forward cache restore: the page is back, so is the form.
- window.addEventListener(
- 'pageshow',
- ( pageEvent ) => {
- if ( pageEvent.persisted ) {
- release();
- }
- },
- { once: true }
- );
+ window.addEventListener( 'pageshow', onPageShow );
// ponytail: fixed 15s fallback for a redirect that never unloads the
- // page (e.g. a URL served as a file download). Make it configurable
- // if a real slow redirect ever takes longer.
- fallbackTimer = window.setTimeout( release, 15000 );
+ // page (e.g. a URL served as a file download, or a beforeunload
+ // prompt the visitor cancelled). Make it configurable if a real slow
+ // redirect ever takes longer. The page is still here, so failures
+ // count again; if the navigation does commit later, pagehide sets
+ // navigatingAway back before the unload.
+ fallbackTimer = window.setTimeout( () => {
+ navigatingAway = false;
+ release();
+ }, 15000 );
} else {
loader.classList.remove( 'srfm-active' );
From 65952e4809faf33872e4d4fd7fea5de3acde437b Mon Sep 17 00:00:00 2001
From: Aditya Jain
Date: Mon, 28 Sep 2026 19:24:31 +0530
Subject: [PATCH 004/116] feat(ai-builder): add starter prompt cards and guided
steps to the Generate with AI screen
- Four free-safe starter cards (Contact, Quote request, Event registration,
Feedback) that prefill the prompt; clicking again clears an untouched prompt.
- Pick / Customize / Generate progress steps and event-driven Learn tips.
- Generate stays aria-disabled until the prompt has text, with a hint.
- Inline Undo when a card replaces the user's own prompt.
- 0/2000 character counter; voice input adds a separator and respects the limit.
- Layout uses max-width and scrolls on short or narrow screens.
- Replace the resume-upload example prompt with free-supported wording.
- New srfm.aiFormScreen.starterPrompts filter with jest coverage.
---
.../AiFormBuilderForm.js | 519 ++++++++++++------
.../StarterPrompts.js | 104 ++++
.../template-picker/utils/starter-prompts.js | 87 +++
.../utils/test/starter-prompts.test.js | 80 +++
4 files changed, 629 insertions(+), 161 deletions(-)
create mode 100644 src/admin/components/template-picker/ai-form-builder-components/StarterPrompts.js
create mode 100644 src/admin/components/template-picker/utils/starter-prompts.js
create mode 100644 src/admin/components/template-picker/utils/test/starter-prompts.test.js
diff --git a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
index 114a3fdc8..46d496a4b 100644
--- a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
+++ b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
@@ -1,11 +1,24 @@
-import { __ } from '@wordpress/i18n';
-import { useState, useRef, useEffect, memo } from '@wordpress/element';
-import { Button, Container, TextArea, toast, Tooltip } from '@bsf/force-ui';
+import { __, sprintf } from '@wordpress/i18n';
+import { useState, useRef, useEffect, useMemo, memo } from '@wordpress/element';
+import {
+ Button,
+ Container,
+ ProgressSteps,
+ TextArea,
+ toast,
+ Tooltip,
+} from '@bsf/force-ui';
import { ArrowRight, Sparkles, MicOff, Mic } from 'lucide-react';
import { applyFilters } from '@wordpress/hooks';
+import { speak } from '@wordpress/a11y';
import { cn, srfmClassNames } from '@Utils/Helpers';
import FormTypeSelector from '../components/FormTypeSelector.js';
import CreditDetailsPopup from './CreditDetailsPopup.js';
+import StarterPrompts from './StarterPrompts.js';
+import {
+ getStarterPrompts,
+ MAX_PROMPT_LENGTH,
+} from '../utils/starter-prompts.js';
import { useLocation } from 'react-router-dom';
const VoiceToggleButton = memo( ( { isListening, toggleListening } ) => {
@@ -75,11 +88,28 @@ export default ( props ) => {
// ๐ added new state for banner visibility
const [ isFocused, setIsFocused ] = useState( false );
- const handlePromptClick = ( prompt ) => {
+ const starters = useMemo( () => getStarterPrompts(), [] );
+ const [ selectedStarterId, setSelectedStarterId ] = useState( null );
+ // Prompt replaced by a starter card, kept so the user can undo.
+ const [ replacedPrompt, setReplacedPrompt ] = useState( null );
+ const [ showEmptyPromptHint, setShowEmptyPromptHint ] = useState( false );
+
+ const canGenerate = Boolean( text.trim() );
+
+ const setPrompt = ( prompt ) => {
setText( prompt );
setCharacterCount( prompt.length );
};
+ // Handles prompt edits made by typing or pasting.
+ const handlePromptClick = ( prompt ) => {
+ setPrompt( prompt );
+ setReplacedPrompt( null );
+ if ( ! prompt.trim() ) {
+ setSelectedStarterId( null );
+ }
+ };
+
function useQuery() {
return new URLSearchParams( useLocation().search );
}
@@ -144,8 +174,15 @@ export default ( props ) => {
// keep on appending the result to the textarea
const speechResult =
event.results[ event.results.length - 1 ][ 0 ].transcript;
+ setReplacedPrompt( null );
setText( ( prevText ) => {
- const updatedText = prevText + speechResult;
+ const separator =
+ prevText && ! /\s$/.test( prevText ) ? ' ' : '';
+ const updatedText = (
+ prevText +
+ separator +
+ speechResult.trim()
+ ).slice( 0, MAX_PROMPT_LENGTH );
setCharacterCount( updatedText.length );
return updatedText;
} );
@@ -192,7 +229,7 @@ export default ( props ) => {
},
{
title: __(
- 'Job application form for "Marketing Manager" with resume upload',
+ 'Job application form for "Marketing Manager" with a link to their resume or LinkedIn profile',
'sureforms'
),
},
@@ -284,18 +321,27 @@ export default ( props ) => {
return () => clearInterval( interval );
}, [ textAreaPlaceholder ] );
- // Show learn tip tooltips sequentially when redirected from Learn section.
+ // Show the first learn tip when redirected from Learn section.
useEffect( () => {
if ( source === 'learn' ) {
setShowLearnTip( true );
- const hideTimer = setTimeout( () => {
- setShowLearnTip( false );
- setShowGenerateTip( true );
- }, 5000 );
- return () => clearTimeout( hideTimer );
}
}, [ source ] );
+ // Move to the generate tip once the prompt has text.
+ useEffect( () => {
+ if ( showLearnTip && canGenerate ) {
+ setShowLearnTip( false );
+ setShowGenerateTip( true );
+ }
+ }, [ showLearnTip, canGenerate ] );
+
+ useEffect( () => {
+ if ( canGenerate ) {
+ setShowEmptyPromptHint( false );
+ }
+ }, [ canGenerate ] );
+
// Auto-dismiss generate tip after 5 seconds.
useEffect( () => {
if ( showGenerateTip ) {
@@ -320,183 +366,334 @@ export default ( props ) => {
const finalFormCreationCountRemaining =
isRegistered && formCreationleft > 20 ? 20 : formCreationleft;
- const is_pro_active =
- srfm_admin?.is_pro_active && srfm_admin?.is_pro_license_active;
+ const handleStarterToggle = ( starter ) => {
+ // Stop voice input so it doesn't append to the starter prompt.
+ handleTyping();
+
+ if ( selectedStarterId === starter.id ) {
+ setSelectedStarterId( null );
+ setReplacedPrompt( null );
+ // Only clear the prompt if the user hasn't edited it.
+ if ( text === starter.prompt ) {
+ setPrompt( '' );
+ }
+ speak( __( 'Example removed.', 'sureforms' ) );
+ return;
+ }
+
+ const isStarterText =
+ ! text.trim() || starters.some( ( item ) => item.prompt === text );
+
+ setReplacedPrompt( isStarterText ? null : text );
+ setPrompt( starter.prompt );
+ setSelectedStarterId( starter.id );
+ speak(
+ sprintf(
+ /* translators: %s: starter example title, e.g. "Contact form". */
+ __(
+ '%s example added to the prompt. Edit it or press Generate.',
+ 'sureforms'
+ ),
+ starter.title
+ )
+ );
+ };
+
+ const handleUndoReplace = () => {
+ setPrompt( replacedPrompt ?? '' );
+ setSelectedStarterId( null );
+ setReplacedPrompt( null );
+ document.getElementById( 'textarea' )?.focus();
+ };
+
+ // Guided steps: 1 Pick, 2 Customize, 3 Generate.
+ const isUntouchedStarter =
+ !! selectedStarterId &&
+ starters.some(
+ ( item ) => item.id === selectedStarterId && item.prompt === text
+ );
+ let currentStep = 3;
+ if ( ! canGenerate ) {
+ currentStep = 1;
+ } else if ( isUntouchedStarter ) {
+ currentStep = 2;
+ }
+
+ let counterColorClass = 'text-field-helper';
+ if ( characterCount >= MAX_PROMPT_LENGTH ) {
+ counterColorClass = 'text-support-error';
+ } else if ( characterCount >= MAX_PROMPT_LENGTH * 0.9 ) {
+ counterColorClass = 'text-badge-color-yellow';
+ }
return (
-
+
-
-
+
+
+ { __(
+ 'What form should we build?',
+ 'sureforms'
+ ) }
+
+
{ __(
- 'Describe the form that you want',
+ 'Start with an example, or describe your own form below.',
'sureforms'
) }
-
+
-
- { showLearnTip && (
-
-
-
- { __(
- 'Describe what kind of form you want',
- 'sureforms'
- ) }
+
+
+
+
+
+
+
+
+
+
+ { showLearnTip && (
+
+
+
+ { __(
+ 'Pick an example or describe your form',
+ 'sureforms'
+ ) }
+
-
- ) }
-
-
+ { showEmptyPromptHint && ! canGenerate && (
+
+ { __(
+ 'Describe your form or pick an example to continue.',
+ 'sureforms'
+ ) }
+
+ ) }
+ { replacedPrompt !== null && (
+
+
+ { __(
+ 'Your previous prompt was replaced.',
+ 'sureforms'
+ ) }
+
+
+ { __( 'Undo', 'sureforms' ) }
+
+
+ ) }
diff --git a/src/admin/components/template-picker/ai-form-builder-components/StarterPrompts.js b/src/admin/components/template-picker/ai-form-builder-components/StarterPrompts.js
new file mode 100644
index 000000000..95c49b475
--- /dev/null
+++ b/src/admin/components/template-picker/ai-form-builder-components/StarterPrompts.js
@@ -0,0 +1,104 @@
+import { __ } from '@wordpress/i18n';
+import { Text } from '@bsf/force-ui';
+import { Plus, X } from 'lucide-react';
+import { cn } from '@Utils/Helpers';
+
+/**
+ * Starter prompt cards shown above the AI prompt box.
+ *
+ * @since x.x.x
+ * @param {Object} props
+ * @param {Array} props.starters Starter prompts to render.
+ * @param {string} props.selectedStarterId Currently selected starter id, or null.
+ * @param {Function} props.onToggle Called with a starter when its card is clicked.
+ */
+const StarterPrompts = ( { starters, selectedStarterId, onToggle } ) => {
+ if ( ! starters.length ) {
+ return null;
+ }
+
+ return (
+
+
+ { __( 'Start with an example', 'sureforms' ) }
+
+
+ { starters.map( ( starter ) => {
+ const isSelected = selectedStarterId === starter.id;
+ const Icon = starter.icon;
+ const TrailingIcon = isSelected ? X : Plus;
+
+ return (
+ onToggle( starter ) }
+ className={ cn(
+ 'group flex w-full items-center gap-3 rounded-lg border border-solid p-3 text-left cursor-pointer transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-1',
+ isSelected
+ ? 'border-border-ai-button bg-brand-background-50'
+ : 'border-border-subtle bg-background-primary shadow-sm hover:border-border-strong'
+ ) }
+ >
+ { Icon && (
+
+
+
+ ) }
+
+
+ { starter.title }
+
+ { starter.description && (
+
+ { starter.description }
+
+ ) }
+
+
+
+ );
+ } ) }
+
+
+ );
+};
+
+export default StarterPrompts;
diff --git a/src/admin/components/template-picker/utils/starter-prompts.js b/src/admin/components/template-picker/utils/starter-prompts.js
new file mode 100644
index 000000000..64c93a5b0
--- /dev/null
+++ b/src/admin/components/template-picker/utils/starter-prompts.js
@@ -0,0 +1,87 @@
+import { __ } from '@wordpress/i18n';
+import { applyFilters } from '@wordpress/hooks';
+import { Mail, FileText, CalendarCheck, MessageSquare } from 'lucide-react';
+
+// Maximum prompt length accepted by the AI form builder.
+export const MAX_PROMPT_LENGTH = 2000;
+
+// Maximum number of starter cards rendered in the grid.
+const MAX_STARTERS = 4;
+
+/**
+ * Get the starter prompts shown above the AI prompt box.
+ *
+ * Default prompts only ask for fields available in the free plugin.
+ *
+ * @since x.x.x
+ * @return {Array<{id: string, title: string, description: string, icon: Function, prompt: string}>} Starter prompts.
+ */
+export const getStarterPrompts = () => {
+ const starters = applyFilters( 'srfm.aiFormScreen.starterPrompts', [
+ {
+ id: 'contact',
+ title: __( 'Contact form', 'sureforms' ),
+ description: __( 'Name, email and message', 'sureforms' ),
+ icon: Mail,
+ prompt: __(
+ 'Contact form with full name, email address, phone number (optional), subject and message. Add a consent checkbox asking permission to reply by email.',
+ 'sureforms'
+ ),
+ },
+ {
+ id: 'quote-request',
+ title: __( 'Quote request', 'sureforms' ),
+ description: __( 'Enquiries with project details', 'sureforms' ),
+ icon: FileText,
+ prompt: __(
+ 'Quote request form for a service business with full name, email, phone number, company name (optional), a dropdown to choose the service needed, a dropdown for budget range (Under $500, $500โ$2,000, $2,000โ$5,000, Over $5,000), a dropdown for timeline (As soon as possible, Within a month, 1โ3 months, Just exploring), a field describing the project, and preferred contact method (Email, Phone).',
+ 'sureforms'
+ ),
+ },
+ {
+ id: 'event-registration',
+ title: __( 'Event registration', 'sureforms' ),
+ description: __( 'Sign-ups with attendee details', 'sureforms' ),
+ icon: CalendarCheck,
+ prompt: __(
+ 'Event registration form with full name, email address, phone number, number of attendees, a dropdown to choose a session (Morning, Afternoon, Evening), checkboxes for dietary needs (Vegetarian, Vegan, Gluten-free, None), and a field for questions or special requests.',
+ 'sureforms'
+ ),
+ },
+ {
+ id: 'feedback',
+ title: __( 'Feedback form', 'sureforms' ),
+ description: __( 'Ratings and suggestions', 'sureforms' ),
+ icon: MessageSquare,
+ prompt: __(
+ 'Customer feedback form with name (optional), email (optional), a multiple-choice question "How satisfied are you with our service?" from Very satisfied to Very dissatisfied, a field for what we did well, a field for what we could improve, and a Yes/No question "Would you recommend us to a friend?"',
+ 'sureforms'
+ ),
+ },
+ ] );
+
+ if ( ! Array.isArray( starters ) ) {
+ return [];
+ }
+
+ const seenIds = new Set();
+
+ return starters
+ .filter( ( starter ) => {
+ if (
+ ! starter?.id ||
+ ! starter?.title ||
+ ! starter?.prompt ||
+ seenIds.has( starter.id )
+ ) {
+ return false;
+ }
+ seenIds.add( starter.id );
+ return true;
+ } )
+ .slice( 0, MAX_STARTERS )
+ .map( ( starter ) => ( {
+ ...starter,
+ prompt: starter.prompt.slice( 0, MAX_PROMPT_LENGTH ),
+ } ) );
+};
diff --git a/src/admin/components/template-picker/utils/test/starter-prompts.test.js b/src/admin/components/template-picker/utils/test/starter-prompts.test.js
new file mode 100644
index 000000000..e1754cccd
--- /dev/null
+++ b/src/admin/components/template-picker/utils/test/starter-prompts.test.js
@@ -0,0 +1,80 @@
+/* eslint-env jest */
+/**
+ * getStarterPrompts() feeds the starter cards on the "Generate with AI" screen.
+ * Default prompts must only ask for fields the free plugin can generate, since
+ * the field mapper silently drops pro-only fields (upload, date, rating, ...)
+ * when SureForms Pro is not active.
+ */
+// lucide-react ships ESM that jest does not transform; icons are irrelevant here.
+jest.mock( 'lucide-react', () => ( {
+ Mail: () => null,
+ FileText: () => null,
+ CalendarCheck: () => null,
+ MessageSquare: () => null,
+} ) );
+
+import { addFilter, removeAllFilters } from '@wordpress/hooks';
+import { getStarterPrompts, MAX_PROMPT_LENGTH } from '../starter-prompts';
+
+const FILTER = 'srfm.aiFormScreen.starterPrompts';
+
+afterEach( () => {
+ removeAllFilters( FILTER );
+} );
+
+describe( 'getStarterPrompts', () => {
+ it( 'returns the four default starters with unique ids', () => {
+ const starters = getStarterPrompts();
+ expect( starters.map( ( starter ) => starter.id ) ).toEqual( [
+ 'contact',
+ 'quote-request',
+ 'event-registration',
+ 'feedback',
+ ] );
+ } );
+
+ it( 'keeps default prompts within the prompt length limit', () => {
+ getStarterPrompts().forEach( ( starter ) => {
+ expect( starter.prompt.length ).toBeLessThanOrEqual(
+ MAX_PROMPT_LENGTH
+ );
+ } );
+ } );
+
+ it( 'does not ask for pro-only fields in default prompts', () => {
+ const proOnly =
+ /\b(upload|attachment|date picker|time picker|signature|slider|star rating|nps)\b/i;
+ getStarterPrompts().forEach( ( starter ) => {
+ expect( starter.prompt ).not.toMatch( proOnly );
+ } );
+ } );
+
+ it( 'drops invalid and duplicate starters from filters', () => {
+ addFilter( FILTER, 'test', ( starters ) => [
+ ...starters,
+ { id: 'contact', title: 'Duplicate', prompt: 'Duplicate' },
+ { id: 'no-prompt', title: 'Missing prompt' },
+ ] );
+ const ids = getStarterPrompts().map( ( starter ) => starter.id );
+ expect( ids.filter( ( id ) => id === 'contact' ) ).toHaveLength( 1 );
+ expect( ids ).not.toContain( 'no-prompt' );
+ } );
+
+ it( 'caps the number of starters and trims long prompts', () => {
+ addFilter( FILTER, 'test', () => [
+ { id: 'a', title: 'A', prompt: 'x'.repeat( 2500 ) },
+ { id: 'b', title: 'B', prompt: 'b' },
+ { id: 'c', title: 'C', prompt: 'c' },
+ { id: 'd', title: 'D', prompt: 'd' },
+ { id: 'e', title: 'E', prompt: 'e' },
+ ] );
+ const starters = getStarterPrompts();
+ expect( starters ).toHaveLength( 4 );
+ expect( starters[ 0 ].prompt ).toHaveLength( MAX_PROMPT_LENGTH );
+ } );
+
+ it( 'returns an empty list when a filter returns a non-array', () => {
+ addFilter( FILTER, 'test', () => null );
+ expect( getStarterPrompts() ).toEqual( [] );
+ } );
+} );
From fe0e9af84cd9d22921dbea31eb9837abaaffbe91 Mon Sep 17 00:00:00 2001
From: Avinash Kumar Sharma
Date: Mon, 28 Sep 2026 19:58:20 +0530
Subject: [PATCH 005/116] chore: update plugin version to 2.12.8
Co-Authored-By: Claude Sonnet 4.6
---
package.json | 2 +-
sureforms.php | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index 701ad5377..efa67980c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sureforms",
- "version": "2.8.1",
+ "version": "2.12.8",
"description": "A simple yet powerful way to add forms to your website.",
"main": "index.js",
"scripts": {
diff --git a/sureforms.php b/sureforms.php
index 6681276cc..55150eaff 100644
--- a/sureforms.php
+++ b/sureforms.php
@@ -7,7 +7,7 @@
* Requires PHP: 7.4
* Author: SureForms
* Author URI: https://sureforms.com/
- * Version: 2.8.1
+ * Version: 2.12.8
* 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.8.1' );
+define( 'SRFM_VER', '2.12.8' );
define( 'SRFM_SLUG', 'srfm' );
// ------ ADDITIONAL CONSTANTS ------- //
define( 'SRFM_FORMS_POST_TYPE', 'sureforms_form' );
From e367429cca8af1f0bcc3d7aa4d6d0b5f868b7f5b Mon Sep 17 00:00:00 2001
From: Avinash Kumar Sharma
Date: Mon, 28 Sep 2026 20:18:32 +0530
Subject: [PATCH 006/116] fix(addInitialAttr): deep-clone array attributes when
block is duplicated
Gutenberg's cloneBlock() does a shallow spread of block attributes, so
duplicated blocks share the same array references as the original (e.g.
the options array in multi-choice). Any in-place mutation on one block's
array would silently affect the other.
On duplicate detection, iterate all attributes and shallow-clone any
array values (one level deep is sufficient since option objects contain
only primitives) so each block immediately owns its own independent copy.
This is the root-cause fix. The deleteOption immutability fix in
multi-choice/edit.js is still correct and prevents future regressions,
but without this change the shared reference persists until the first
setAttributes({ options }) call.
Co-Authored-By: Claude Sonnet 4.6
---
src/srfm-controls/addInitialAttr.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/src/srfm-controls/addInitialAttr.js b/src/srfm-controls/addInitialAttr.js
index 8dbdef846..fd90d5d03 100644
--- a/src/srfm-controls/addInitialAttr.js
+++ b/src/srfm-controls/addInitialAttr.js
@@ -65,6 +65,20 @@ const addInitialAttr = ( ChildComponent ) => {
attributeObject.slug = '';
}
+ // Deep-clone array attributes so the duplicated block does not
+ // share the same array references as the original. Without this,
+ // mutations on one block's arrays (e.g. options in multi-choice)
+ // would silently affect the other block.
+ Object.entries( attributes ).forEach( ( [ key, value ] ) => {
+ if ( Array.isArray( value ) ) {
+ attributeObject[ key ] = value.map( ( item ) =>
+ typeof item === 'object' && item !== null
+ ? { ...item }
+ : item
+ );
+ }
+ } );
+
// Only copy conditional logic if pro is active (action is registered).
if ( hasAction( 'srfm.duplicateBlock' ) ) {
doAction( 'srfm.duplicateBlock', block_id, newBlockId );
From 243c48fbc4989996c6b7a1adee18f065a0d51bfc Mon Sep 17 00:00:00 2001
From: Vansh Kapoor
Date: Mon, 28 Sep 2026 22:23:47 +0530
Subject: [PATCH 007/116] feat: fire srfm_submission_accepted as soon as a
submission is accepted
Adds a narrow lifecycle hook right after Entries::add() succeeds, before
notification mail is sent, and on the compliance "do not store entries"
accepted path (entry id 0). It carries the parsed request data, so a
listener can correlate the submission with non-field keys the client sent.
Pro's Form Abandonment & Recovery (sureforms-pro#1519) uses it to mark an
unfinished capture submitted and stop its pending reminder without waiting
behind a slow mail transport. srfm_form_submit is unchanged.
---
inc/form-submit.php | 23 +++++++
tests/unit/inc/test-form-submit.php | 103 ++++++++++++++++++++++++++++
2 files changed, 126 insertions(+)
diff --git a/inc/form-submit.php b/inc/form-submit.php
index d5848be57..e1ad60379 100644
--- a/inc/form-submit.php
+++ b/inc/form-submit.php
@@ -684,6 +684,9 @@ public function handle_form_entry( $form_data ) {
// Check if GDPR is enabled and do not store entries is enabled.
// If so, send email and do not store entries.
if ( $gdpr && $do_not_store_entries ) {
+ /** This action is documented below, where the stored-entry path fires it. */
+ do_action( 'srfm_submission_accepted', 0, Helper::get_integer_value( $id ), $form_data );
+
// Send email before early return. No entry is created in this path so {entry_id} will be empty โ that is expected.
$send_email = $this->send_email( $id, $submission_data, $form_data );
if ( $send_email ) {
@@ -799,6 +802,26 @@ public function handle_form_entry( $form_data ) {
// Inject entry_id so {entry_id} smart tag resolves in confirmation message, redirect URL, email notifications, and downstream integrations.
$form_data['entry_id'] = intval( $entry_id );
+ /**
+ * Fires once a submission has been accepted, before notification email is sent.
+ *
+ * Runs as early as the accepted state is known, so listeners that must react to
+ * "this form was completed" (for example, stopping follow-up work queued against
+ * an unfinished copy of it) are not held behind a slow mail transport. The
+ * `srfm_form_submit` action still fires after mail, unchanged.
+ *
+ * Also fires when the form's compliance settings accept a submission without
+ * storing it; `$entry_id` is 0 on that path.
+ *
+ * @param int $entry_id Stored entry ID, or 0 when the entry is not stored.
+ * @param int $form_id Form ID.
+ * @param array $form_data Parsed request data for the submission (JSON or
+ * multipart), including any non-field keys a client
+ * sent alongside the fields.
+ * @since x.x.x
+ */
+ do_action( 'srfm_submission_accepted', intval( $entry_id ), Helper::get_integer_value( $id ), $form_data );
+
// Switch the multilingual provider to the entry's language so the
// confirmation message, redirect URL, and email notifications render
// in the language the visitor saw at submit time. The REST submit
diff --git a/tests/unit/inc/test-form-submit.php b/tests/unit/inc/test-form-submit.php
index 99d52eda7..d342147d2 100644
--- a/tests/unit/inc/test-form-submit.php
+++ b/tests/unit/inc/test-form-submit.php
@@ -825,6 +825,109 @@ static function ( $log ) {
wp_delete_post( $form_id, true );
}
+ /**
+ * srfm_submission_accepted fires with the stored entry id, before any mail is sent.
+ *
+ * Listeners use it to stop follow-up work for an unfinished copy of the form, so it
+ * must not wait behind a slow mail transport, and it must carry the non-field keys the
+ * client sent with the submission.
+ */
+ public function test_handle_form_entry_fires_submission_accepted_before_mail() {
+ $form_id = wp_insert_post(
+ [
+ 'post_type' => 'sureforms_form',
+ 'post_title' => 'Accepted Hook Form',
+ 'post_status' => 'publish',
+ ]
+ );
+ update_post_meta( $form_id, '_srfm_email_notification', [ [ 'id' => 1, 'status' => true ] ] );
+
+ $calls = [];
+ $mailed = false;
+ $listen = static function ( $entry_id, $hook_form_id, $form_data ) use ( &$calls, &$mailed ) {
+ $calls[] = [ $entry_id, $hook_form_id, $form_data, $mailed ];
+ };
+ $parsed = static function () {
+ return [
+ 'to' => 'owner@example.com',
+ 'subject' => 'New entry',
+ 'message' => 'An entry arrived.',
+ 'headers' => [],
+ ];
+ };
+ $sending = static function () use ( &$mailed ) {
+ $mailed = true;
+ return true;
+ };
+
+ add_action( 'srfm_submission_accepted', $listen, 10, 3 );
+ add_filter( 'srfm_email_notification', $parsed, 99 );
+ add_filter( 'pre_wp_mail', $sending, 99 );
+
+ $response = $this->form_submit->handle_form_entry(
+ [
+ 'form-id' => (string) $form_id,
+ 'srfm_partial_session_id' => 'abc-123',
+ ]
+ );
+
+ remove_filter( 'pre_wp_mail', $sending, 99 );
+ remove_filter( 'srfm_email_notification', $parsed, 99 );
+ remove_action( 'srfm_submission_accepted', $listen, 10 );
+
+ $entry_id = Helper::get_integer_value( $response['data']['submission_id'] );
+
+ $this->assertTrue( $mailed, 'The notification must still be sent.' );
+ $this->assertCount( 1, $calls );
+ $this->assertSame( $entry_id, $calls[0][0] );
+ $this->assertSame( $form_id, $calls[0][1] );
+ $this->assertSame( 'abc-123', $calls[0][2]['srfm_partial_session_id'] );
+ $this->assertFalse( $calls[0][3], 'The hook must fire before mail is handed to the transport.' );
+
+ EntriesTable::delete( $entry_id );
+ wp_delete_post( $form_id, true );
+ }
+
+ /**
+ * A submission accepted without storing an entry still fires the hook, with entry id 0.
+ *
+ * Otherwise a completed no-store form would look unfinished to every listener.
+ */
+ public function test_handle_form_entry_fires_submission_accepted_when_not_stored() {
+ $form_id = wp_insert_post(
+ [
+ 'post_type' => 'sureforms_form',
+ 'post_title' => 'No Store Form',
+ 'post_status' => 'publish',
+ ]
+ );
+ update_post_meta(
+ $form_id,
+ '_srfm_compliance',
+ [
+ [
+ 'gdpr' => true,
+ 'do_not_store_entries' => true,
+ ],
+ ]
+ );
+
+ $calls = [];
+ $listen = static function ( $entry_id, $hook_form_id ) use ( &$calls ) {
+ $calls[] = [ $entry_id, $hook_form_id ];
+ };
+ add_action( 'srfm_submission_accepted', $listen, 10, 2 );
+
+ $response = $this->form_submit->handle_form_entry( [ 'form-id' => (string) $form_id ] );
+
+ remove_action( 'srfm_submission_accepted', $listen, 10 );
+
+ $this->assertTrue( $response['success'] );
+ $this->assertSame( [ [ 0, $form_id ] ], $calls );
+
+ wp_delete_post( $form_id, true );
+ }
+
/**
* One recipient succeeding while another fails is still a failure.
*
From fd48972b4566f05df1d5516b862e69569a8eb0c1 Mon Sep 17 00:00:00 2001
From: Aditya Jain
Date: Tue, 29 Sep 2026 06:50:38 +0530
Subject: [PATCH 008/116] feat(analytics): track AI forms created from starter
cards
- Send the selected starter card id and an edited flag with create-new-form.
- Store the card id on the form as _srfm_ai_starter.
- New ai_starter_usage event (forced, cumulative): total AI forms, forms per
starter card and edited count, so card share is queryable in ClickHouse.
- Add ai_starter to the first_form_published event properties.
- handleAddNewPost() accepts optional extra request data.
- PHPUnit coverage for the counters, key cap and pending-event overwrite.
---
admin/analytics.php | 45 +++++++++
inc/create-new-form.php | 11 +++
.../AiFormBuilderForm.js | 9 +-
.../components/AiFormBuilder.js | 9 +-
src/utils/Helpers.js | 4 +-
tests/php/stubs/srfm-stubs.php | 11 +++
.../unit/admin/test-analytics-ai-starter.php | 94 +++++++++++++++++++
7 files changed, 179 insertions(+), 4 deletions(-)
create mode 100644 tests/unit/admin/test-analytics-ai-starter.php
diff --git a/admin/analytics.php b/admin/analytics.php
index c652395f4..98463c3df 100644
--- a/admin/analytics.php
+++ b/admin/analytics.php
@@ -732,10 +732,55 @@ public function track_first_form_published( $new_status, $old_status, $post ) {
'is_ai_generated' => (string) (int) $is_ai,
'block_count' => (string) $block_count,
'days_since_install' => (string) $days_since_install,
+ 'ai_starter' => Helper::get_string_value( get_post_meta( $post->ID, '_srfm_ai_starter', true ) ),
]
);
}
+ /**
+ * Track AI form creation, broken down by the starter card used (if any).
+ *
+ * Keeps cumulative counters in srfm_options and re-sends the latest totals
+ * as the `ai_starter_usage` event, so the share of AI forms started from
+ * each card can be read from the warehouse.
+ *
+ * @param string $starter_id Starter card id, or empty string for a free-text prompt.
+ * @param bool $edited Whether the starter prompt was edited before generating.
+ * @since x.x.x
+ * @return void
+ */
+ public static function track_ai_starter_usage( $starter_id, $edited ) {
+ $usage = Helper::get_srfm_option( 'ai_starter_usage', [] );
+ $usage = is_array( $usage ) ? $usage : [];
+
+ $counts = isset( $usage['starters'] ) && is_array( $usage['starters'] ) ? $usage['starters'] : [];
+
+ $usage['total_ai_forms'] = absint( $usage['total_ai_forms'] ?? 0 ) + 1;
+
+ // Cap distinct keys so a filtered starter list can't grow the option unbounded.
+ if ( '' !== $starter_id && ( isset( $counts[ $starter_id ] ) || count( $counts ) < 20 ) ) {
+ $counts[ $starter_id ] = absint( $counts[ $starter_id ] ?? 0 ) + 1;
+
+ if ( $edited ) {
+ $usage['edited'] = absint( $usage['edited'] ?? 0 ) + 1;
+ }
+ }
+
+ $usage['starters'] = $counts;
+ Helper::update_srfm_option( 'ai_starter_usage', $usage );
+
+ $properties = [
+ 'total_ai_forms' => (string) $usage['total_ai_forms'],
+ 'edited' => (string) absint( $usage['edited'] ?? 0 ),
+ ];
+ foreach ( $counts as $id => $count ) {
+ $properties[ 'starter_' . $id ] = (string) $count;
+ }
+
+ // $force = true: cumulative counter, must re-send the latest totals each cycle.
+ self::events()->track( 'ai_starter_usage', (string) array_sum( $counts ), $properties, true );
+ }
+
/**
* Track embed styling configuration when a post/page is saved.
*
diff --git a/inc/create-new-form.php b/inc/create-new-form.php
index b662aa5cd..457220789 100644
--- a/inc/create-new-form.php
+++ b/inc/create-new-form.php
@@ -8,6 +8,7 @@
namespace SRFM\Inc;
+use SRFM\Admin\Analytics;
use SRFM\Inc\AI_Form_Builder\AI_Helper;
use SRFM\Inc\Traits\Get_Instance;
use WP_Error;
@@ -182,6 +183,16 @@ public static function create_form( $data ) {
*/
update_post_meta( $post_id, '_srfm_is_ai_generated', true );
+ // Record which starter card (if any) the AI prompt came from.
+ $starter_id = isset( $form_info_obj->ai_starter ) && is_string( $form_info_obj->ai_starter ) ? substr( sanitize_key( $form_info_obj->ai_starter ), 0, 40 ) : '';
+ $edited = ! empty( $form_info_obj->ai_starter_edited );
+
+ if ( '' !== $starter_id ) {
+ update_post_meta( $post_id, '_srfm_ai_starter', $starter_id );
+ }
+
+ Analytics::track_ai_starter_usage( $starter_id, $edited );
+
return new WP_REST_Response(
[
'message' => __( 'SureForms Form created successfully.', 'sureforms' ),
diff --git a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
index 46d496a4b..3d9d94c92 100644
--- a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
+++ b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
@@ -648,7 +648,14 @@ export default ( props ) => {
handleCreateAiForm(
text,
[],
- true
+ true,
+ {
+ starterId:
+ selectedStarterId,
+ starterEdited:
+ !! selectedStarterId &&
+ ! isUntouchedStarter,
+ }
);
} }
>
diff --git a/src/admin/components/template-picker/components/AiFormBuilder.js b/src/admin/components/template-picker/components/AiFormBuilder.js
index 2c21e1f78..e7a2694a6 100644
--- a/src/admin/components/template-picker/components/AiFormBuilder.js
+++ b/src/admin/components/template-picker/components/AiFormBuilder.js
@@ -62,7 +62,8 @@ const AiFormBuilder = () => {
const handleCreateAiForm = async (
userCommand,
previousMessages,
- useSystemMessage
+ useSystemMessage,
+ starterInfo = {}
) => {
// Check if the user has permission to create posts.
if ( '1' !== srfm_admin.capability ) {
@@ -209,7 +210,11 @@ const AiFormBuilder = () => {
metasToUpdate,
formTypeObj?.isConversationalForm,
formType,
- handleFormCreationError
+ handleFormCreationError,
+ {
+ ai_starter: starterInfo?.starterId || '',
+ ai_starter_edited: Boolean( starterInfo?.starterEdited ),
+ }
);
};
diff --git a/src/utils/Helpers.js b/src/utils/Helpers.js
index 7a66d73ed..1d939e097 100644
--- a/src/utils/Helpers.js
+++ b/src/utils/Helpers.js
@@ -68,7 +68,8 @@ export const handleAddNewPost = async (
templateMetas,
isConversational = false,
formType = '',
- onError
+ onError,
+ extraData = {}
) => {
const reportError = ( message ) => {
if ( typeof onError === 'function' ) {
@@ -100,6 +101,7 @@ export const handleAddNewPost = async (
template_metas: templateMetas,
is_conversational: isConversational,
form_type: formType,
+ ...extraData,
},
} );
diff --git a/tests/php/stubs/srfm-stubs.php b/tests/php/stubs/srfm-stubs.php
index 7be3c8ee0..97f3190f2 100644
--- a/tests/php/stubs/srfm-stubs.php
+++ b/tests/php/stubs/srfm-stubs.php
@@ -189,6 +189,17 @@ public function __construct()
public function add_srfm_analytics_data($stats_data)
{
}
+ /**
+ * Track AI form creation, broken down by the starter card used (if any).
+ *
+ * @param string $starter_id Starter card id, or empty string for a free-text prompt.
+ * @param bool $edited Whether the starter prompt was edited before generating.
+ * @since x.x.x
+ * @return void
+ */
+ public static function track_ai_starter_usage($starter_id, $edited)
+ {
+ }
/**
* Return total number of forms using instant forms.
*
diff --git a/tests/unit/admin/test-analytics-ai-starter.php b/tests/unit/admin/test-analytics-ai-starter.php
new file mode 100644
index 000000000..32521b5a7
--- /dev/null
+++ b/tests/unit/admin/test-analytics-ai-starter.php
@@ -0,0 +1,94 @@
+get_pending_event();
+ $this->assertNotNull( $event );
+ $this->assertSame( '0', $event['event_value'] );
+ $this->assertSame(
+ [
+ 'total_ai_forms' => '1',
+ 'edited' => '0',
+ ],
+ $event['properties']
+ );
+ }
+
+ public function test_starter_usage_is_counted_per_card_with_edits() {
+ Analytics::track_ai_starter_usage( 'contact', false );
+ Analytics::track_ai_starter_usage( 'contact', true );
+ Analytics::track_ai_starter_usage( 'feedback', false );
+ Analytics::track_ai_starter_usage( '', false );
+
+ $event = $this->get_pending_event();
+ $this->assertSame( '3', $event['event_value'] );
+ $this->assertSame( '4', $event['properties']['total_ai_forms'] );
+ $this->assertSame( '1', $event['properties']['edited'] );
+ $this->assertSame( '2', $event['properties']['starter_contact'] );
+ $this->assertSame( '1', $event['properties']['starter_feedback'] );
+ }
+
+ public function test_latest_totals_replace_the_pending_event() {
+ Analytics::track_ai_starter_usage( 'contact', false );
+ Analytics::track_ai_starter_usage( 'contact', false );
+
+ $pending = Helper::get_srfm_option( 'usage_events_pending', [] );
+ $names = array_column( (array) $pending, 'event_name' );
+ $this->assertSame( 1, count( array_keys( $names, 'ai_starter_usage', true ) ) );
+ $this->assertSame( '2', $this->get_pending_event()['event_value'] );
+ }
+
+ public function test_distinct_starter_keys_are_capped() {
+ for ( $i = 0; $i < 25; $i++ ) {
+ Analytics::track_ai_starter_usage( 'starter-' . $i, false );
+ }
+
+ $usage = Helper::get_srfm_option( 'ai_starter_usage', [] );
+ $this->assertCount( 20, $usage['starters'] );
+ $this->assertSame( 25, $usage['total_ai_forms'] );
+ }
+}
From 0f4f59791be0b6f6bc9d3fea667508e3a7d2a729 Mon Sep 17 00:00:00 2001
From: Aditya Jain
Date: Tue, 29 Sep 2026 06:55:59 +0530
Subject: [PATCH 009/116] test(analytics): move ai_starter_usage tests into
test-analytics.php for the coverage check
---
.../unit/admin/test-analytics-ai-starter.php | 94 -------------------
tests/unit/admin/test-analytics.php | 72 ++++++++++++++
2 files changed, 72 insertions(+), 94 deletions(-)
delete mode 100644 tests/unit/admin/test-analytics-ai-starter.php
diff --git a/tests/unit/admin/test-analytics-ai-starter.php b/tests/unit/admin/test-analytics-ai-starter.php
deleted file mode 100644
index 32521b5a7..000000000
--- a/tests/unit/admin/test-analytics-ai-starter.php
+++ /dev/null
@@ -1,94 +0,0 @@
-get_pending_event();
- $this->assertNotNull( $event );
- $this->assertSame( '0', $event['event_value'] );
- $this->assertSame(
- [
- 'total_ai_forms' => '1',
- 'edited' => '0',
- ],
- $event['properties']
- );
- }
-
- public function test_starter_usage_is_counted_per_card_with_edits() {
- Analytics::track_ai_starter_usage( 'contact', false );
- Analytics::track_ai_starter_usage( 'contact', true );
- Analytics::track_ai_starter_usage( 'feedback', false );
- Analytics::track_ai_starter_usage( '', false );
-
- $event = $this->get_pending_event();
- $this->assertSame( '3', $event['event_value'] );
- $this->assertSame( '4', $event['properties']['total_ai_forms'] );
- $this->assertSame( '1', $event['properties']['edited'] );
- $this->assertSame( '2', $event['properties']['starter_contact'] );
- $this->assertSame( '1', $event['properties']['starter_feedback'] );
- }
-
- public function test_latest_totals_replace_the_pending_event() {
- Analytics::track_ai_starter_usage( 'contact', false );
- Analytics::track_ai_starter_usage( 'contact', false );
-
- $pending = Helper::get_srfm_option( 'usage_events_pending', [] );
- $names = array_column( (array) $pending, 'event_name' );
- $this->assertSame( 1, count( array_keys( $names, 'ai_starter_usage', true ) ) );
- $this->assertSame( '2', $this->get_pending_event()['event_value'] );
- }
-
- public function test_distinct_starter_keys_are_capped() {
- for ( $i = 0; $i < 25; $i++ ) {
- Analytics::track_ai_starter_usage( 'starter-' . $i, false );
- }
-
- $usage = Helper::get_srfm_option( 'ai_starter_usage', [] );
- $this->assertCount( 20, $usage['starters'] );
- $this->assertSame( 25, $usage['total_ai_forms'] );
- }
-}
diff --git a/tests/unit/admin/test-analytics.php b/tests/unit/admin/test-analytics.php
index fcdf1558c..4d19dccf2 100644
--- a/tests/unit/admin/test-analytics.php
+++ b/tests/unit/admin/test-analytics.php
@@ -1524,4 +1524,76 @@ private function reset_table_cache() {
$memo->setAccessible( true );
$memo->setValue( null, null );
}
+
+ /**
+ * Get the pending ai_starter_usage event, if any.
+ *
+ * @return array|null
+ */
+ private function get_pending_ai_starter_event() {
+ $pending = Helper::get_srfm_option( 'usage_events_pending', [] );
+ foreach ( (array) $pending as $event ) {
+ if ( isset( $event['event_name'] ) && 'ai_starter_usage' === $event['event_name'] ) {
+ return $event;
+ }
+ }
+ return null;
+ }
+
+ public function test_track_ai_starter_usage_counts_free_text_toward_total_only() {
+ Helper::update_srfm_option( 'ai_starter_usage', [] );
+
+ Analytics::track_ai_starter_usage( '', false );
+
+ $event = $this->get_pending_ai_starter_event();
+ $this->assertNotNull( $event );
+ $this->assertSame( '0', $event['event_value'] );
+ $this->assertSame(
+ [
+ 'total_ai_forms' => '1',
+ 'edited' => '0',
+ ],
+ $event['properties']
+ );
+ }
+
+ public function test_track_ai_starter_usage_counts_per_card_with_edits() {
+ Helper::update_srfm_option( 'ai_starter_usage', [] );
+
+ Analytics::track_ai_starter_usage( 'contact', false );
+ Analytics::track_ai_starter_usage( 'contact', true );
+ Analytics::track_ai_starter_usage( 'feedback', false );
+ Analytics::track_ai_starter_usage( '', false );
+
+ $event = $this->get_pending_ai_starter_event();
+ $this->assertSame( '3', $event['event_value'] );
+ $this->assertSame( '4', $event['properties']['total_ai_forms'] );
+ $this->assertSame( '1', $event['properties']['edited'] );
+ $this->assertSame( '2', $event['properties']['starter_contact'] );
+ $this->assertSame( '1', $event['properties']['starter_feedback'] );
+ }
+
+ public function test_track_ai_starter_usage_replaces_the_pending_event() {
+ Helper::update_srfm_option( 'ai_starter_usage', [] );
+
+ Analytics::track_ai_starter_usage( 'contact', false );
+ Analytics::track_ai_starter_usage( 'contact', false );
+
+ $pending = Helper::get_srfm_option( 'usage_events_pending', [] );
+ $names = array_column( (array) $pending, 'event_name' );
+ $this->assertSame( 1, count( array_keys( $names, 'ai_starter_usage', true ) ) );
+ $this->assertSame( '2', $this->get_pending_ai_starter_event()['event_value'] );
+ }
+
+ public function test_track_ai_starter_usage_caps_distinct_starter_keys() {
+ Helper::update_srfm_option( 'ai_starter_usage', [] );
+
+ for ( $i = 0; $i < 25; $i++ ) {
+ Analytics::track_ai_starter_usage( 'starter-' . $i, false );
+ }
+
+ $usage = Helper::get_srfm_option( 'ai_starter_usage', [] );
+ $this->assertCount( 20, $usage['starters'] );
+ $this->assertSame( 25, $usage['total_ai_forms'] );
+ }
}
From 25962dcc0da1235306d7886ab7c0a186a618af86 Mon Sep 17 00:00:00 2001
From: Aditya Jain
Date: Tue, 29 Sep 2026 07:44:16 +0530
Subject: [PATCH 010/116] test(e2e): cover starter cards on the Generate with
AI screen
Forces healthy AI usage details before boot (local/CI domains fail middleware
verification) and stubs generate-form, then checks each card sends its prompt,
the inactive Generate hint, deselect and Undo.
---
tests/play/specs/ai-form-builder.spec.js | 181 +++++++++++++++++++++++
1 file changed, 181 insertions(+)
create mode 100644 tests/play/specs/ai-form-builder.spec.js
diff --git a/tests/play/specs/ai-form-builder.spec.js b/tests/play/specs/ai-form-builder.spec.js
new file mode 100644
index 000000000..173cadf4c
--- /dev/null
+++ b/tests/play/specs/ai-form-builder.spec.js
@@ -0,0 +1,181 @@
+/**
+ * Generate with AI screen โ starter cards.
+ *
+ * The AI middleware can't verify local/CI domains, so the page would normally
+ * open behind a "limit reached / domain verification" popup. Each test forces
+ * the localized usage details to a healthy state before the app boots, and
+ * stubs the generate-form request so no AI credits are used.
+ */
+const { test, expect } = require( '@playwright/test' );
+const { loginAsAdmin } = require( '../utils/loginAsAdmin' );
+
+const AI_SCREEN = '/wp-admin/admin.php?page=add-new-form';
+const GENERATE_FORM_ROUTE = /sureforms(\/|%2F)v1(\/|%2F)generate-form/;
+
+const STARTERS = [
+ { title: 'Contact form', keyword: 'Contact form' },
+ { title: 'Quote request', keyword: 'Quote request form' },
+ { title: 'Event registration', keyword: 'Event registration form' },
+ { title: 'Feedback form', keyword: 'Customer feedback form' },
+];
+
+/**
+ * Open the AI screen with usable AI credits and a stubbed generate endpoint.
+ *
+ * @param {import('@playwright/test').Page} page
+ * @return {Promise>} Request bodies sent to generate-form.
+ */
+async function openAiScreen( page ) {
+ await page.addInitScript( () => {
+ // srfm_admin is printed as `var srfm_admin = {...}`; intercept the
+ // assignment so the screen boots without the limit/verification popup.
+ let srfmAdmin;
+ Object.defineProperty( window, 'srfm_admin', {
+ configurable: true,
+ get: () => srfmAdmin,
+ set: ( value ) => {
+ srfmAdmin = {
+ ...value,
+ srfm_ai_usage_details: {
+ type: 'registered',
+ remaining: 5,
+ limit: 10,
+ },
+ };
+ },
+ } );
+ } );
+
+ const generateRequests = [];
+ await page.route( GENERATE_FORM_ROUTE, async ( route ) => {
+ generateRequests.push( route.request().postDataJSON() );
+ await route.fulfill( {
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify( {
+ success: false,
+ message: 'Stubbed in tests.',
+ } ),
+ } );
+ } );
+
+ await loginAsAdmin( page );
+ await page.goto( AI_SCREEN );
+ await expect(
+ page.getByRole( 'heading', { name: 'What form should we build?' } )
+ ).toBeVisible();
+
+ return generateRequests;
+}
+
+const starterCard = ( page, title ) =>
+ page.getByRole( 'button', { name: new RegExp( `^${ title }` ) } );
+
+const promptBox = ( page ) => page.locator( '#textarea' );
+
+const generateButton = ( page ) =>
+ page.getByRole( 'button', { name: 'Generate', exact: true } );
+
+test.describe( 'Generate with AI โ starter cards', () => {
+ test( 'shows the starter cards and keeps Generate inactive while empty', async ( {
+ page,
+ } ) => {
+ const generateRequests = await openAiScreen( page );
+
+ for ( const { title } of STARTERS ) {
+ await expect( starterCard( page, title ) ).toHaveAttribute(
+ 'aria-pressed',
+ 'false'
+ );
+ }
+ await expect( page.locator( '#srfm-ai-prompt-counter' ) ).toHaveText(
+ '0/2000'
+ );
+ await expect( generateButton( page ) ).toHaveAttribute(
+ 'aria-disabled',
+ 'true'
+ );
+
+ // Generate uses aria-disabled (not the disabled attribute) so it stays
+ // clickable and can explain itself; Playwright treats aria-disabled as
+ // not actionable, hence force.
+ await generateButton( page ).click( { force: true } );
+
+ // The same text is also announced via wp.a11y.speak(), so target the hint by id.
+ await expect( page.locator( '#srfm-ai-empty-prompt-hint' ) ).toHaveText(
+ 'Describe your form or pick an example to continue.'
+ );
+ await expect( promptBox( page ) ).toBeFocused();
+ expect( generateRequests ).toHaveLength( 0 );
+ } );
+
+ for ( const { title, keyword } of STARTERS ) {
+ test( `"${ title }" card sends its prompt to the AI`, async ( {
+ page,
+ } ) => {
+ const generateRequests = await openAiScreen( page );
+
+ await starterCard( page, title ).click();
+
+ await expect( starterCard( page, title ) ).toHaveAttribute(
+ 'aria-pressed',
+ 'true'
+ );
+ const prompt = await promptBox( page ).inputValue();
+ expect( prompt.startsWith( keyword ) ).toBe( true );
+ await expect(
+ page.locator( '#srfm-ai-prompt-counter' )
+ ).toHaveText( `${ prompt.length }/2000` );
+ await expect( generateButton( page ) ).toHaveAttribute(
+ 'aria-disabled',
+ 'false'
+ );
+
+ await generateButton( page ).click();
+
+ await expect.poll( () => generateRequests.length ).toBe( 1 );
+ const body = generateRequests[ 0 ];
+ const lastMessage =
+ body.message_array[ body.message_array.length - 1 ];
+ expect( lastMessage ).toEqual( {
+ role: 'user',
+ content: prompt,
+ } );
+ expect( body.form_type ).toBe( 'simple' );
+ expect( body.is_conversional ).toBeFalsy();
+ } );
+ }
+
+ test( 'clicking a selected card again clears its untouched prompt', async ( {
+ page,
+ } ) => {
+ await openAiScreen( page );
+
+ await starterCard( page, 'Feedback form' ).click();
+ await expect( promptBox( page ) ).not.toHaveValue( '' );
+
+ await starterCard( page, 'Feedback form' ).click();
+
+ await expect( promptBox( page ) ).toHaveValue( '' );
+ await expect(
+ starterCard( page, 'Feedback form' )
+ ).toHaveAttribute( 'aria-pressed', 'false' );
+ } );
+
+ test( 'a card replacing typed text can be undone', async ( { page } ) => {
+ await openAiScreen( page );
+
+ await promptBox( page ).fill( 'Booking form for my yoga studio' );
+ await starterCard( page, 'Contact form' ).click();
+
+ await expect( promptBox( page ) ).toHaveValue( /^Contact form/ );
+ await page.getByRole( 'button', { name: 'Undo' } ).click();
+
+ await expect( promptBox( page ) ).toHaveValue(
+ 'Booking form for my yoga studio'
+ );
+ await expect(
+ starterCard( page, 'Contact form' )
+ ).toHaveAttribute( 'aria-pressed', 'false' );
+ } );
+} );
From 1fdf4c71b413bacdbaaf960ffe5fad531f94f182 Mon Sep 17 00:00:00 2001
From: Aditya Jain
Date: Tue, 29 Sep 2026 07:51:24 +0530
Subject: [PATCH 011/116] docs(e2e): note AI screen testing patterns in the
spec guide
---
tests/play/specs/CLAUDE.md | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/tests/play/specs/CLAUDE.md b/tests/play/specs/CLAUDE.md
index 113b91abd..954d37009 100644
--- a/tests/play/specs/CLAUDE.md
+++ b/tests/play/specs/CLAUDE.md
@@ -306,6 +306,13 @@ await page.evaluate(() => {
Server returns `wp_send_json_error` โ `.srfm-success-box` must NOT appear.
+## Generate with AI screen (`admin.php?page=add-new-form`)
+
+- The AI middleware can't verify local/CI domains, so the screen boots behind a limit/"Domain Verification Failed" popup. Before `page.goto`, use `page.addInitScript` to intercept the `srfm_admin` assignment and force `srfm_ai_usage_details` to `{ type: 'registered', remaining: 5 }`. See `ai-form-builder.spec.js`.
+- Stub `generate-form` with `page.route( /sureforms(\/|%2F)v1(\/|%2F)generate-form/ )`, then assert on `route.request().postDataJSON()`. Never call the real AI.
+- Generate uses `aria-disabled`, not `disabled`. Playwright treats it as not actionable, so an intentional click on the empty state needs `{ force: true }`.
+- Texts passed to `wp.a11y.speak()` also exist in a live region, so `getByText` hits two elements. Target the visible element by id.
+
## Helpful utilities
All utilities live in `tests/play/utils/`.
From 585345d1be90fb8bf84d6511eb756927264b28d8 Mon Sep 17 00:00:00 2001
From: Aditya Jain
Date: Tue, 29 Sep 2026 11:08:00 +0530
Subject: [PATCH 012/116] feat(ai-builder): remove the Pick / Customize /
Generate step indicator
The starter cards and Learn tips already guide the flow; the indicator added
visual noise without semantics for assistive tech.
---
.../AiFormBuilderForm.js | 40 -------------------
1 file changed, 40 deletions(-)
diff --git a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
index 46d496a4b..89fa8f39d 100644
--- a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
+++ b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
@@ -3,7 +3,6 @@ import { useState, useRef, useEffect, useMemo, memo } from '@wordpress/element';
import {
Button,
Container,
- ProgressSteps,
TextArea,
toast,
Tooltip,
@@ -406,19 +405,6 @@ export default ( props ) => {
document.getElementById( 'textarea' )?.focus();
};
- // Guided steps: 1 Pick, 2 Customize, 3 Generate.
- const isUntouchedStarter =
- !! selectedStarterId &&
- starters.some(
- ( item ) => item.id === selectedStarterId && item.prompt === text
- );
- let currentStep = 3;
- if ( ! canGenerate ) {
- currentStep = 1;
- } else if ( isUntouchedStarter ) {
- currentStep = 2;
- }
-
let counterColorClass = 'text-field-helper';
if ( characterCount >= MAX_PROMPT_LENGTH ) {
counterColorClass = 'text-support-error';
@@ -459,32 +445,6 @@ export default ( props ) => {
) }
-
-
-
-
-
-
-
Date: Tue, 29 Sep 2026 11:10:47 +0530
Subject: [PATCH 013/116] fix(analytics): keep isUntouchedStarter for the
starter edited flag after the step indicator removal
---
.../ai-form-builder-components/AiFormBuilderForm.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
index c767f841a..aabe7b984 100644
--- a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
+++ b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
@@ -405,6 +405,13 @@ export default ( props ) => {
document.getElementById( 'textarea' )?.focus();
};
+ // Whether the prompt is still exactly the selected starter's text (for tracking edits).
+ const isUntouchedStarter =
+ !! selectedStarterId &&
+ starters.some(
+ ( item ) => item.id === selectedStarterId && item.prompt === text
+ );
+
let counterColorClass = 'text-field-helper';
if ( characterCount >= MAX_PROMPT_LENGTH ) {
counterColorClass = 'text-support-error';
From 5cb4d860ffbb68e5dfa1a3a7f372ee5dd1c28803 Mon Sep 17 00:00:00 2001
From: Aditya Jain
Date: Tue, 29 Sep 2026 11:23:33 +0530
Subject: [PATCH 014/116] feat(ai-builder): collapse starter examples once the
site has an AI form
- New AI_Helper::has_ai_generated_forms() (one-row lookup), localized as
srfm_admin.has_ai_generated_forms on the Generate with AI screen only.
- After the first AI form the cards collapse to "Need ideas? Show examples";
expanding moves focus to the first card. The Learn flow always shows them.
- PHPUnit + Playwright coverage.
---
admin/admin.php | 1 +
inc/ai-form-builder/ai-helper.php | 30 +++++++++
.../AiFormBuilderForm.js | 61 +++++++++++++++++--
tests/play/specs/ai-form-builder.spec.js | 35 +++++++++--
.../inc/ai-form-builder/test-ai-helper.php | 38 ++++++++++++
5 files changed, 156 insertions(+), 9 deletions(-)
diff --git a/admin/admin.php b/admin/admin.php
index a445eca49..df6f3da9f 100644
--- a/admin/admin.php
+++ b/admin/admin.php
@@ -2187,6 +2187,7 @@ public function enqueue_scripts() {
'template_picker_nonce' => Helper::current_user_can() ? wp_create_nonce( 'wp_rest' ) : '',
'is_pro_active' => Helper::has_pro(),
'srfm_ai_usage_details' => AI_Helper::get_current_usage_details(),
+ 'has_ai_generated_forms' => AI_Helper::has_ai_generated_forms(),
'is_pro_license_active' => AI_Helper::is_pro_license_active(),
'srfm_ai_auth_user_email' => get_option( 'srfm_ai_auth_user_email' ),
'pricing_page_url' => Helper::get_sureforms_website_url( 'pricing' ),
diff --git a/inc/ai-form-builder/ai-helper.php b/inc/ai-form-builder/ai-helper.php
index 28c0fe265..824dd88cf 100644
--- a/inc/ai-form-builder/ai-helper.php
+++ b/inc/ai-form-builder/ai-helper.php
@@ -222,6 +222,36 @@ public static function is_pro_license_active() {
return $licensing->is_license_active();
}
+ /**
+ * Check whether the site has any AI-generated form, in any status.
+ *
+ * Used by the Generate with AI screen to collapse the starter examples once
+ * the site has made its first AI form.
+ *
+ * @since x.x.x
+ * @return bool
+ */
+ public static function has_ai_generated_forms() {
+ $ai_forms = get_posts(
+ [
+ 'post_type' => SRFM_FORMS_POST_TYPE,
+ 'post_status' => 'any',
+ 'posts_per_page' => 1,
+ 'fields' => 'ids',
+ 'no_found_rows' => true,
+ 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- One-row lookup, only on the Generate with AI screen.
+ [
+ 'key' => '_srfm_is_ai_generated',
+ 'value' => '',
+ 'compare' => '!=',
+ ],
+ ],
+ ]
+ );
+
+ return ! empty( $ai_forms );
+ }
+
/**
* Sanitize an upstream error message before returning it to the client.
*
diff --git a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
index 89fa8f39d..51fd8cb5d 100644
--- a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
+++ b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
@@ -120,6 +120,25 @@ export default ( props ) => {
const [ showLearnTip, setShowLearnTip ] = useState( false );
const [ showGenerateTip, setShowGenerateTip ] = useState( false );
+ // Starter examples are for getting started: collapse them once the site has
+ // an AI form, except in the Learn flow which is about the first form.
+ const [ showStarters, setShowStarters ] = useState(
+ () => ! srfm_admin?.has_ai_generated_forms || source === 'learn'
+ );
+ const [ focusStarters, setFocusStarters ] = useState( false );
+
+ // Move focus to the first example after "Show examples" is clicked.
+ useEffect( () => {
+ if ( focusStarters && showStarters ) {
+ document
+ .querySelector(
+ '[aria-labelledby="srfm-starter-prompts-label"] button'
+ )
+ ?.focus();
+ setFocusStarters( false );
+ }
+ }, [ focusStarters, showStarters ] );
+
const initSpeechRecognition = () => {
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
@@ -447,11 +466,43 @@ export default ( props ) => {
-
+ { showStarters ? (
+
+ ) : (
+ starters.length > 0 && (
+
+
+ { __(
+ 'Need ideas?',
+ 'sureforms'
+ ) }
+
+ {
+ setShowStarters( true );
+ setFocusStarters( true );
+ } }
+ >
+ { __(
+ 'Show examples',
+ 'sureforms'
+ ) }
+
+
+ )
+ ) }
{ showLearnTip && (
diff --git a/tests/play/specs/ai-form-builder.spec.js b/tests/play/specs/ai-form-builder.spec.js
index 173cadf4c..01055cf0a 100644
--- a/tests/play/specs/ai-form-builder.spec.js
+++ b/tests/play/specs/ai-form-builder.spec.js
@@ -23,10 +23,13 @@ const STARTERS = [
* Open the AI screen with usable AI credits and a stubbed generate endpoint.
*
* @param {import('@playwright/test').Page} page
+ * @param {Object} options
+ * @param {boolean} options.hasAiForms Pretend the site already has an AI form.
+ * @param {string} options.query Extra query string, e.g. '&source=learn'.
* @return {Promise
>} Request bodies sent to generate-form.
*/
-async function openAiScreen( page ) {
- await page.addInitScript( () => {
+async function openAiScreen( page, { hasAiForms = false, query = '' } = {} ) {
+ await page.addInitScript( ( siteHasAiForms ) => {
// srfm_admin is printed as `var srfm_admin = {...}`; intercept the
// assignment so the screen boots without the limit/verification popup.
let srfmAdmin;
@@ -41,10 +44,11 @@ async function openAiScreen( page ) {
remaining: 5,
limit: 10,
},
+ has_ai_generated_forms: siteHasAiForms,
};
},
} );
- } );
+ }, hasAiForms );
const generateRequests = [];
await page.route( GENERATE_FORM_ROUTE, async ( route ) => {
@@ -60,7 +64,7 @@ async function openAiScreen( page ) {
} );
await loginAsAdmin( page );
- await page.goto( AI_SCREEN );
+ await page.goto( AI_SCREEN + query );
await expect(
page.getByRole( 'heading', { name: 'What form should we build?' } )
).toBeVisible();
@@ -178,4 +182,27 @@ test.describe( 'Generate with AI โ starter cards', () => {
starterCard( page, 'Contact form' )
).toHaveAttribute( 'aria-pressed', 'false' );
} );
+
+ test( 'examples collapse once the site has an AI form and can be expanded', async ( {
+ page,
+ } ) => {
+ await openAiScreen( page, { hasAiForms: true } );
+
+ await expect( starterCard( page, 'Contact form' ) ).toHaveCount( 0 );
+ await page.getByRole( 'button', { name: 'Show examples' } ).click();
+
+ for ( const { title } of STARTERS ) {
+ await expect( starterCard( page, title ) ).toBeVisible();
+ }
+ await expect( starterCard( page, 'Contact form' ) ).toBeFocused();
+ } );
+
+ test( 'the Learn flow always shows the examples', async ( { page } ) => {
+ await openAiScreen( page, { hasAiForms: true, query: '&source=learn' } );
+
+ await expect( starterCard( page, 'Contact form' ) ).toBeVisible();
+ await expect(
+ page.getByRole( 'button', { name: 'Show examples' } )
+ ).toHaveCount( 0 );
+ } );
} );
diff --git a/tests/unit/inc/ai-form-builder/test-ai-helper.php b/tests/unit/inc/ai-form-builder/test-ai-helper.php
index 9dc1ef507..6ab0a7d17 100644
--- a/tests/unit/inc/ai-form-builder/test-ai-helper.php
+++ b/tests/unit/inc/ai-form-builder/test-ai-helper.php
@@ -292,4 +292,42 @@ public function test_sanitize_ai_error_message_does_not_log_when_endpoint_empty(
$result = AI_Helper::sanitize_ai_error_message( 'plain message' );
$this->assertSame( 'plain message', $result );
}
+
+ /**
+ * has_ai_generated_forms() is false until a form carries the AI meta.
+ */
+ public function test_has_ai_generated_forms_false_without_ai_forms() {
+ // This suite does not roll back between tests, so clear AI flags left by others.
+ delete_post_meta_by_key( '_srfm_is_ai_generated' );
+
+ $post_id = wp_insert_post(
+ [
+ 'post_type' => SRFM_FORMS_POST_TYPE,
+ 'post_title' => 'Manual form',
+ 'post_status' => 'publish',
+ ]
+ );
+
+ $this->assertFalse( AI_Helper::has_ai_generated_forms() );
+
+ wp_delete_post( $post_id, true );
+ }
+
+ /**
+ * has_ai_generated_forms() counts AI forms in any status, including drafts.
+ */
+ public function test_has_ai_generated_forms_true_for_draft_ai_form() {
+ $post_id = wp_insert_post(
+ [
+ 'post_type' => SRFM_FORMS_POST_TYPE,
+ 'post_title' => 'AI form',
+ 'post_status' => 'draft',
+ ]
+ );
+ update_post_meta( $post_id, '_srfm_is_ai_generated', true );
+
+ $this->assertTrue( AI_Helper::has_ai_generated_forms() );
+
+ wp_delete_post( $post_id, true );
+ }
}
From 17f1581cbfecee77317976b01a5a7c51645a779a Mon Sep 17 00:00:00 2001
From: Aditya Jain
Date: Tue, 29 Sep 2026 11:55:04 +0530
Subject: [PATCH 015/116] fix(ai-builder): deselecting an untouched example
restores the prompt it replaced
Edited prompts are still kept on deselect. Adds Playwright coverage for both.
---
.../AiFormBuilderForm.js | 7 +++--
tests/play/specs/ai-form-builder.spec.js | 28 +++++++++++++++++++
2 files changed, 32 insertions(+), 3 deletions(-)
diff --git a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
index 51fd8cb5d..afa77837c 100644
--- a/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
+++ b/src/admin/components/template-picker/ai-form-builder-components/AiFormBuilderForm.js
@@ -390,11 +390,12 @@ export default ( props ) => {
if ( selectedStarterId === starter.id ) {
setSelectedStarterId( null );
- setReplacedPrompt( null );
- // Only clear the prompt if the user hasn't edited it.
+ // An untouched example gives back whatever it replaced; an edited
+ // prompt belongs to the user now and is kept.
if ( text === starter.prompt ) {
- setPrompt( '' );
+ setPrompt( replacedPrompt ?? '' );
}
+ setReplacedPrompt( null );
speak( __( 'Example removed.', 'sureforms' ) );
return;
}
diff --git a/tests/play/specs/ai-form-builder.spec.js b/tests/play/specs/ai-form-builder.spec.js
index 01055cf0a..9a7a58823 100644
--- a/tests/play/specs/ai-form-builder.spec.js
+++ b/tests/play/specs/ai-form-builder.spec.js
@@ -205,4 +205,32 @@ test.describe( 'Generate with AI โ starter cards', () => {
page.getByRole( 'button', { name: 'Show examples' } )
).toHaveCount( 0 );
} );
+
+ test( 'deselecting an untouched card gives back the text it replaced', async ( {
+ page,
+ } ) => {
+ await openAiScreen( page );
+
+ await promptBox( page ).fill( 'Booking form for my yoga studio' );
+ await starterCard( page, 'Contact form' ).click();
+ await starterCard( page, 'Contact form' ).click();
+
+ await expect( promptBox( page ) ).toHaveValue(
+ 'Booking form for my yoga studio'
+ );
+ } );
+
+ test( 'deselecting an edited card keeps the edits', async ( { page } ) => {
+ await openAiScreen( page );
+
+ await starterCard( page, 'Event registration' ).click();
+ const edited = ( await promptBox( page ).inputValue() ) + ' Add a T-shirt size dropdown.';
+ await promptBox( page ).fill( edited );
+ await starterCard( page, 'Event registration' ).click();
+
+ await expect( promptBox( page ) ).toHaveValue( edited );
+ await expect(
+ starterCard( page, 'Event registration' )
+ ).toHaveAttribute( 'aria-pressed', 'false' );
+ } );
} );
From 44a4328055f5aa7204df5ecf6e80e45a4972129e Mon Sep 17 00:00:00 2001
From: Aditya Jain
Date: Tue, 29 Sep 2026 12:10:58 +0530
Subject: [PATCH 016/116] refactor(analytics): drop the _srfm_ai_starter form
meta
Starter usage is counted at creation time, so the per-form meta only fed a
one-time first_form_published property. Remove it (and its export/duplicate
side effects) and sanitize the starter id inside track_ai_starter_usage()
instead of relying on the caller.
---
admin/analytics.php | 7 +++++--
inc/create-new-form.php | 8 ++------
tests/unit/admin/test-analytics.php | 9 +++++++++
3 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/admin/analytics.php b/admin/analytics.php
index 98463c3df..a6f3710fa 100644
--- a/admin/analytics.php
+++ b/admin/analytics.php
@@ -732,7 +732,6 @@ public function track_first_form_published( $new_status, $old_status, $post ) {
'is_ai_generated' => (string) (int) $is_ai,
'block_count' => (string) $block_count,
'days_since_install' => (string) $days_since_install,
- 'ai_starter' => Helper::get_string_value( get_post_meta( $post->ID, '_srfm_ai_starter', true ) ),
]
);
}
@@ -750,10 +749,14 @@ public function track_first_form_published( $new_status, $old_status, $post ) {
* @return void
*/
public static function track_ai_starter_usage( $starter_id, $edited ) {
+ // The id becomes an option key and an event property key, so clean it here
+ // rather than trusting every caller.
+ $starter_id = substr( sanitize_key( Helper::get_string_value( $starter_id ) ), 0, 40 );
+
$usage = Helper::get_srfm_option( 'ai_starter_usage', [] );
$usage = is_array( $usage ) ? $usage : [];
- $counts = isset( $usage['starters'] ) && is_array( $usage['starters'] ) ? $usage['starters'] : [];
+ $counts = isset( $usage['starters'] ) && is_array( $usage['starters'] ) ? array_map( 'absint', $usage['starters'] ) : [];
$usage['total_ai_forms'] = absint( $usage['total_ai_forms'] ?? 0 ) + 1;
diff --git a/inc/create-new-form.php b/inc/create-new-form.php
index 457220789..8bf0255c2 100644
--- a/inc/create-new-form.php
+++ b/inc/create-new-form.php
@@ -183,14 +183,10 @@ public static function create_form( $data ) {
*/
update_post_meta( $post_id, '_srfm_is_ai_generated', true );
- // Record which starter card (if any) the AI prompt came from.
- $starter_id = isset( $form_info_obj->ai_starter ) && is_string( $form_info_obj->ai_starter ) ? substr( sanitize_key( $form_info_obj->ai_starter ), 0, 40 ) : '';
+ // Count the AI form against the starter card (if any) its prompt came from.
+ $starter_id = isset( $form_info_obj->ai_starter ) && is_string( $form_info_obj->ai_starter ) ? $form_info_obj->ai_starter : '';
$edited = ! empty( $form_info_obj->ai_starter_edited );
- if ( '' !== $starter_id ) {
- update_post_meta( $post_id, '_srfm_ai_starter', $starter_id );
- }
-
Analytics::track_ai_starter_usage( $starter_id, $edited );
return new WP_REST_Response(
diff --git a/tests/unit/admin/test-analytics.php b/tests/unit/admin/test-analytics.php
index 4d19dccf2..603b1e602 100644
--- a/tests/unit/admin/test-analytics.php
+++ b/tests/unit/admin/test-analytics.php
@@ -1596,4 +1596,13 @@ public function test_track_ai_starter_usage_caps_distinct_starter_keys() {
$this->assertCount( 20, $usage['starters'] );
$this->assertSame( 25, $usage['total_ai_forms'] );
}
+
+ public function test_track_ai_starter_usage_sanitizes_the_starter_id() {
+ Helper::update_srfm_option( 'ai_starter_usage', [] );
+
+ Analytics::track_ai_starter_usage( 'Contact'
- . ' ';
-
- $preserved = $this->call_protected( 'strip_form_for_preservation', [ $html ] );
-
- $this->assertStringContainsString( '
Free eBook ', $preserved );
- $this->assertStringContainsString( 'Thanks for signing up', $preserved );
- $this->assertStringNotContainsString( '
assertStringNotContainsString( ''
+ . ' ';
+
+ $preserved = $this->call_protected( 'strip_form_for_preservation', [ $html ] );
+
+ $this->assertStringContainsString( '
Free eBook ', $preserved );
+ $this->assertStringContainsString( 'Thanks for signing up', $preserved );
+ $this->assertStringNotContainsString( '
assertStringNotContainsString( ''
- . ' ';
-
- $preserved = $this->call_protected( 'strip_form_for_preservation', [ $html ] );
-
- $this->assertStringContainsString( 'Free eBook ', $preserved );
- $this->assertStringContainsString( 'Thanks for signing up', $preserved );
- $this->assertStringNotContainsString( 'assertStringNotContainsString( ''
+ . ' ';
+
+ $preserved = $this->call_protected( 'strip_form_for_preservation', [ $html ] );
+
+ $this->assertStringContainsString( 'Free eBook ', $preserved );
+ $this->assertStringContainsString( 'Thanks for signing up', $preserved );
+ $this->assertStringNotContainsString( 'assertStringNotContainsString( '',
- $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 */}
-
-
-
- {label}
- {required && * }
-
-
-
-
- {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}
- Save
-
- );
-}
-```
-
----
-
-## 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 (
-
- {label}
-
- );
-}
-
-// โ BAD - Access via props object
-function Button(props) {
- return {props.label} ;
-}
-```
-
-### Conditional Rendering
-
-```javascript
-function FormField({ label, required, error }) {
- return (
-
-
- {label}
- {/* Short-circuit evaluation */}
- {required && * }
-
-
- {/* 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 Click me ;
-}
-```
-
----
-
-## 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 */}
- Submit
-
- );
-}
-```
-
----
-
-## 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 (
- updateFormSettings({ key: 'newValue' })}>
- Update
-
- );
-}
-```
-
-## 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 869f850ca..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 โ decoded 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 '