## Flow JavaScript API

Flow exposes its browser runtime under `globalThis.WpSuite.plugins.flow`.

The stable browser-level pieces are:

- `status`
- `availability()`
- `onReady(cb)`
- `features.store`
- `modals`
- DOM readiness/error events: `wpsuite:flow:ready`, `wpsuite:flow:error`

## Modal API

Flow also exposes a light-DOM modal controller under `WpSuite.plugins.flow.modals`.

Available methods:

- `register(modalElement, options?)`
- `unregister(modalOrId)`
- `open(modalId, options?)`
- `close(modalId, returnValue?)`
- `toggle(modalId, options?)`
- `closeAll(returnValue?)`
- `isOpen(modalId)`
- `get(modalId)`
- `registerAction(actionName, handler)`
- `unregisterAction(actionName)`

Action handlers registered through `registerAction(actionName, handler)` can be async. The modal runtime waits for the returned promise, keeps the modal open during the await, sets the modal into a busy state, disables the standard modal action triggers, and marks the currently running trigger element with `data-wps-flow-pending="true"` and `aria-busy="true"`. That pending marker is intentionally generic, so it works with core Button blocks as well as custom button markup.

If an async handler returns `false`, automatic close is skipped and the modal stays open. For dismiss-style close paths such as the built-in close button, backdrop clicks, Escape, or programmatic close calls, the runtime also emits `wps-flow-modal:dismiss`.

Example:

```js
const flow = globalThis.WpSuite.plugins.flow;

flow.modals.registerAction("submitLeadIntent", async ({ modalId, close }) => {
	await fetch("/wp-json/my-plugin/v1/leads/intent", { method: "POST" });
	close("submitted");
});

flow.modals.open("contact-intent-modal");
```

Example: open a Flow modal from a neighboring Gutenberg button, close it from another Gutenberg button inside the modal, and listen for the close event:

1. In the Flow Modal block inspector, set `modalId` to `newsletter-preferences`.
2. Add a core Button block next to the modal and set its `Advanced -> Additional CSS class(es)` to `wps-flow-modal-open--newsletter-preferences`.
3. Add another core Button block inside the modal content and set its `Advanced -> Additional CSS class(es)` to `wps-flow-modal-close`.
4. Listen for `wps-flow-modal:close` on `document`.

```html
<!-- Neighboring core/button block -->
<div class="wp-block-button wps-flow-modal-open--newsletter-preferences">
	<a class="wp-block-button__link wp-element-button">
		Open preferences
	</a>
</div>

<!-- Inside the Flow Modal block -->
<div class="wp-block-button wps-flow-modal-close">
	<a class="wp-block-button__link wp-element-button">
		Close popup
	</a>
</div>

<script>
	document.addEventListener("wps-flow-modal:close", (event) => {
		const detail = event.detail ?? {};

		if (detail.modalId !== "newsletter-preferences") {
			return;
		}

		console.log("Flow modal closed", {
			modalId: detail.modalId,
			returnValue: detail.returnValue,
		});

		// Example: reset host-page UI or fire analytics here.
	});
</script>
```

For a core Button block, the CSS class route is usually the simplest integration because the runtime already delegates clicks for `.wps-flow-modal-open--{modalId}` and `.wps-flow-modal-close`. WordPress applies those custom classes to the rendered `.wp-block-button` wrapper, so the Flow runtime resolves the clicked inner link or button back to that wrapper trigger. A `.wps-flow-modal-close` button closes the nearest Flow modal and emits `wps-flow-modal:close` with `detail.returnValue === "close"`.

The modal runtime is intentionally browser-friendly and static-export-friendly:

- it uses native `<dialog>` instead of the Flow shadow-root shell,
- it supports class-based triggers such as `wps-flow-modal-open--contact-intent-modal`,
- it supports attribute triggers such as `data-wps-flow-modal-open="contact-intent-modal"`,
- it can open from URL hashes when the modal block enables hash support,
- it emits bubbling modal lifecycle events on the dialog element itself, which also reach `document` listeners.

Supported lifecycle events:

- `wps-flow-modal:before-open`
- `wps-flow-modal:open`
- `wps-flow-modal:before-close`
- `wps-flow-modal:close`
- `wps-flow-modal:ok`
- `wps-flow-modal:cancel`
- `wps-flow-modal:dismiss`
- `wps-flow-modal:error`

## Form-scoped field defaults

Flow core also exposes helper methods on `WpSuite.plugins.flow` for form-scoped field default values:

- `setFormFieldDefaultValue(formId, fieldName, value)`
- `setFormFieldDefaultValues(formId, values)`
- `clearFormFieldDefaultValues(formId)`
- `getFormFieldDefaultValue(formId, fieldName)`
- `getFormFieldDefaultValues(formId)`

Each helper returns a Promise because Flow waits for its internal store before reading or writing values.

Example:

```js
await globalThis.WpSuite.plugins.flow.setFormFieldDefaultValues("newsletter-footer", {
	email: "user@example.com",
	utm_source: "spring-campaign",
});
```

If you prefer working with the store directly, use the same actions through `features.store` and `wp.data.dispatch(...)`:

```js
const store = await globalThis.WpSuite.plugins.flow.features.store;
const actions = globalThis.wp.data.dispatch(store);

actions.setFormFieldDefaultValue("newsletter-footer", "email", "user@example.com");
```

These APIs write form-scoped defaults into the shared Flow store. Rendered Flow forms now resolve and apply those defaults by `formId`.

## Runtime string interpolation

Some Flow runtime string settings support token interpolation. In practice that means Flow replaces `{{...}}` placeholders with values from the current page context before using the string.

This is useful for settings such as a per-form `endpointPath` or an API-backed options field's `apiEndpoint` when the final URL depends on the current page, query string, host-page globals, or current form values.

API-backed option fields can also derive initial selection state from each response item. Use `apiSelectedPath` to read a per-item flag or status field, and optionally `apiSelectedValue` when the item should count as selected only for a specific value such as `SUBSCRIBED`. This is especially useful for `checkbox-group`, and it also applies to `select`, `radio`, and `tags` fields.

For custom form submissions, the endpoint URL can be paired with an `endpointMethod` of `GET`, `POST`, `PUT` or `PATCH`. If no method is configured, Flow uses `POST`. When `GET` is selected, Flow sends the serialized field values as URL query parameters instead of a JSON request body. You can also attach additional browser-side request headers through `endpointHeaders` (stored as a JSON object string in block attributes); Flow merges them with the existing reCAPTCHA request header when present. Header values support the same runtime interpolation tokens as `endpointPath`, but they are resolved as plain strings instead of URL-encoded path segments.

When runtime tokens are used in `endpointPath` or an API-backed field's `apiEndpoint`, Flow URL-encodes dynamic field and query-string token values before inserting them into the final URL.

Supported token families are:

- `{{email}}`, `{{fullName}}`, `{{message}}`, etc. for current form field values by field name
- `{{query.foo}}` for query-string values
- `{{location.href}}`, `{{location.origin}}`, `{{location.pathname}}`, `{{location.search}}`, `{{location.hash}}`, etc.
- `{{wp.postId}}`, `{{wp.postSlug}}`, `{{wp.postType}}`, `{{wp.postTitle}}`, `{{wp.postUrl}}`
- `{{wpsuite.apiBaseUrl}}`, `{{wpsuite.siteSettings.siteId}}`, etc. as shorthand for the global `WpSuite.*` object
- `{{global.MyApp.config.formsBaseUrl}}` for any other primitive value reachable on `globalThis`
- `{{field.email}}`, `{{field.fullName}}`, etc. as an explicit alias for current form field values

Examples:

```text
{{location.origin}}/contact
https://api.example.com/forms?source={{location.pathname}}
{{wpsuite.apiBaseUrl}}/contact
{{global.MyApp.forms.submitUrl}}
{{wpsuite.contactApiBaseUrl}}/{{email}}
```

Important constraints:

- Interpolation is not arbitrary JavaScript execution. It only reads values from known runtime contexts or from property paths on `globalThis`.
- Function calls, expressions and conditionals are not supported.
- For `global.*` and `wpsuite.*`, the value must already exist on the page as a primitive (`string`, `number`, or `boolean`) when Flow resolves the setting.
- Current form field value tokens are only available in runtime surfaces that already have access to the form state, such as rendered form fields and API-backed option loaders.

The most important recent addition for host-page integrations is the form event layer. Rendered Flow forms now emit bubbling `CustomEvent`s such as:

- `smartcloud-flow:draft-saved`
- `smartcloud-flow:draft-loaded`
- `smartcloud-flow:draft-deleted`
- `smartcloud-flow:ai-suggestion-accepted`
- `smartcloud-flow:ai-suggestions-rejected`
- `smartcloud-flow:submit-after-ai-accepted`
- `smartcloud-flow:submit-success`
- `smartcloud-flow:success-state-shown`
- `smartcloud-flow:wizard-step-change`
- `smartcloud-flow:return-to-form`
- `smartcloud-flow:options-request-error`
- `smartcloud-flow:error`

For modal integrations there is also a compatibility alias for successful submission handling:

- `wps-flow-form:submit-success`

Each of those also emits a generic `smartcloud-flow:state-change` event with the original event name in `event.detail.event`.

Typical `detail` fields include `formId`, `action`, `submissionId`, `status`, `suggestionId`, and for wizard navigation `wizardPath`, `stepIndex`, `stepTitle` and `totalVisibleSteps`.

For API-backed field loading errors, `detail` can also include `fieldName`, `fieldType`, `requestMethod`, `errorType`, `message`, `status`, and `responseText`.

Example host-page handling for an API-backed options error:

```html
<div id="preferences-api-error" hidden></div>

<script>
	const errorBox = document.getElementById("preferences-api-error");

	document.addEventListener("smartcloud-flow:options-request-error", (event) => {
		const detail = event.detail ?? {};

		if (detail.formId && detail.formId !== "preferences-form") {
			return;
		}

		const message =
			typeof detail.message === "string" && detail.message.trim()
				? detail.message.trim()
				: "Unable to load options right now.";

		if (errorBox) {
			errorBox.textContent = message;
			errorBox.hidden = false;
		}

		console.warn("Flow options request error", {
			formId: detail.formId,
			fieldName: detail.fieldName,
			status: detail.status,
			errorType: detail.errorType,
			responseText: detail.responseText,
		});
	});

	document.addEventListener("smartcloud-flow:submit-success", () => {
		if (errorBox) {
			errorBox.hidden = true;
			errorBox.textContent = "";
		}
	});
</script>
```

Use `detail.message` for user-facing UI. Keep `detail.responseText` for diagnostics or logging rather than showing it directly to end users.

This makes it straightforward to wire analytics, redirects, host-page UI reactions or outer app state updates without modifying the Flow runtime itself.

In package-level integrations there are also helper exports such as `waitForFlowReady()`, `getStore()`, `decideCapability()` and `resolveBackend()`. Those are useful when you integrate against Flow core from JavaScript or TypeScript code instead of only from inline browser scripts.