Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ import org.wordpress.gutenberg.model.GBKitGlobal
import org.wordpress.gutenberg.services.EditorService
import java.util.Locale

const val ASSET_URL = "https://appassets.androidplatform.net/assets/index.html"
const val DEFAULT_ASSET_DOMAIN = "appassets.androidplatform.net"
const val ASSET_PATH_INDEX = "/assets/index.html"

/**
* A WebView-based Gutenberg block editor for Android.
Expand Down Expand Up @@ -85,9 +86,8 @@ const val ASSET_URL = "https://appassets.androidplatform.net/assets/index.html"
class GutenbergView : WebView {
private var isEditorLoaded = false
private var didFireEditorLoaded = false
private var assetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/", AssetsPathHandler(this.context))
.build()
private lateinit var assetLoader: WebViewAssetLoader
private lateinit var assetDomain: String
private val configuration: EditorConfiguration
private lateinit var dependencies: EditorDependencies

Expand Down Expand Up @@ -235,7 +235,7 @@ class GutenbergView : WebView {
): WebResourceResponse? {
if (request.url == null) {
return super.shouldInterceptRequest(view, request)
} else if (request.url.host?.contains("appassets.androidplatform.net") == true) {
} else if (request.url.host == assetDomain) {
return assetLoader.shouldInterceptRequest(request.url)
} else if (requestInterceptor.canIntercept(request)) {
return requestInterceptor.handleRequest(request)
Expand Down Expand Up @@ -268,7 +268,7 @@ class GutenbergView : WebView {
}

// Allow asset URLs
if (url.host == Uri.parse(ASSET_URL).host) {
if (url.host == assetDomain) {
return false
}

Expand Down Expand Up @@ -386,6 +386,15 @@ class GutenbergView : WebView {
private fun loadEditor(dependencies: EditorDependencies) {
this.dependencies = dependencies

// Set up asset loader domain
assetDomain = configuration.assetLoaderDomain ?: DEFAULT_ASSET_DOMAIN

// Initialize asset loader with configured domain
assetLoader = WebViewAssetLoader.Builder()
.setDomain(assetDomain)
.addPathHandler("/assets/", AssetsPathHandler(this.context))
.build()

// Set up asset caching
requestInterceptor = CachedAssetRequestInterceptor(
dependencies.assetBundle,
Expand All @@ -398,13 +407,13 @@ class GutenbergView : WebView {
initializeWebView()

val editorUrl = BuildConfig.GUTENBERG_EDITOR_URL.ifEmpty {
ASSET_URL
"https://$assetDomain$ASSET_PATH_INDEX"
}

WebStorage.getInstance().deleteAllData()
this.clearCache(true)
// All cookies are third-party cookies because the root of this document
// lives under `https://appassets.androidplatform.net`
// lives under the configured asset domain (e.g., `https://appassets.androidplatform.net`)
CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)

// Erase all local cookies before loading the URL – we don't want to persist
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ data class EditorConfiguration(
val editorAssetsEndpoint: String? = null,
val enableNetworkLogging: Boolean = false,
var enableOfflineMode: Boolean = false,
val assetLoaderDomain: String? = null
): Parcelable {

/**
Expand Down Expand Up @@ -73,6 +74,7 @@ data class EditorConfiguration(
private var editorAssetsEndpoint: String? = null
private var enableNetworkLogging: Boolean = false
private var enableOfflineMode: Boolean = false
private var assetLoaderDomain: String? = null

fun setTitle(title: String) = apply { this.title = title }
fun setContent(content: String) = apply { this.content = content }
Expand All @@ -95,6 +97,7 @@ data class EditorConfiguration(
fun setEditorAssetsEndpoint(editorAssetsEndpoint: String?) = apply { this.editorAssetsEndpoint = editorAssetsEndpoint }
fun setEnableNetworkLogging(enableNetworkLogging: Boolean) = apply { this.enableNetworkLogging = enableNetworkLogging }
fun setEnableOfflineMode(enableOfflineMode: Boolean) = apply { this.enableOfflineMode = enableOfflineMode }
fun setAssetLoaderDomain(assetLoaderDomain: String?) = apply { this.assetLoaderDomain = assetLoaderDomain }

fun build(): EditorConfiguration = EditorConfiguration(
title = title,
Expand All @@ -117,7 +120,8 @@ data class EditorConfiguration(
cachedAssetHosts = cachedAssetHosts,
editorAssetsEndpoint = editorAssetsEndpoint,
enableNetworkLogging = enableNetworkLogging,
enableOfflineMode = enableOfflineMode
enableOfflineMode = enableOfflineMode,
assetLoaderDomain = assetLoaderDomain
)
}

Expand Down Expand Up @@ -145,6 +149,7 @@ data class EditorConfiguration(
.setEditorAssetsEndpoint(editorAssetsEndpoint)
.setEnableNetworkLogging(enableNetworkLogging)
.setEnableOfflineMode(enableOfflineMode)
.setAssetLoaderDomain(assetLoaderDomain)

override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down Expand Up @@ -173,6 +178,7 @@ data class EditorConfiguration(
if (editorAssetsEndpoint != other.editorAssetsEndpoint) return false
if (enableNetworkLogging != other.enableNetworkLogging) return false
if (enableOfflineMode != other.enableOfflineMode) return false
if (assetLoaderDomain != other.assetLoaderDomain) return false
if (siteId != other.siteId) return false

return true
Expand Down Expand Up @@ -200,6 +206,7 @@ data class EditorConfiguration(
result = 31 * result + (editorAssetsEndpoint?.hashCode() ?: 0)
result = 31 * result + enableNetworkLogging.hashCode()
result = 31 * result + enableOfflineMode.hashCode()
result = 31 * result + (assetLoaderDomain?.hashCode() ?: 0)
result = 31 * result + siteId.hashCode()
return result
}
Expand Down
10 changes: 10 additions & 0 deletions docs/code/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,13 @@ The file does not exist at "[path]" which is in the optimize deps directory. The
- Deleting the `node_modules/.vite` directory (or `node_modules` entirely) and restarting the development server via `make dev-server`.

You may also need to clear your browser cache to ensure no stale files are used.

## AJAX requests fail with CORS errors

**Error:** `Access to XMLHttpRequest at 'https://example.com/wp-admin/admin-ajax.php' from origin 'http://localhost:5173' has been blocked by CORS policy`

This error occurs when the editor makes AJAX requests (e.g., from blocks that use `admin-ajax.php`) while running on the development server. The browser blocks these cross-origin requests because the editor runs on `localhost` while AJAX targets your WordPress site.

**Solution:** AJAX functionality requires a production bundle. Build the editor assets with `make build` and test AJAX features using the demo apps without using the `GUTENBERG_EDITOR_URL` environment variable.

For Android, you must also configure `assetLoaderDomain` to a domain allowed by your WordPress site's CORS policy. See the [AJAX Support section](../integration.md#ajax-support) in the Integration Guide for complete configuration details.
40 changes: 40 additions & 0 deletions docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,43 @@ val configuration = EditorConfiguration.builder()
.setEditorSettings(editorSettingsJSON)
.build()
```

### AJAX Support

Some Gutenberg blocks and features use WordPress AJAX (`admin-ajax.php`) for functionality like form submissions. GutenbergKit supports AJAX requests when properly configured.

**Requirements:**

1. **Production bundle required**: AJAX requests fail with CORS errors when using the development server because the editor runs on `localhost` while AJAX requests target your WordPress site. You must use a production bundle built with `make build`.

2. **Configure `siteURL`**: The `siteURL` configuration option must be set to your WordPress site URL. This is used to construct the AJAX endpoint (`{siteURL}/wp-admin/admin-ajax.php`).

3. **Set authentication header**: The `authHeader` configuration must be set. GutenbergKit injects this header into all AJAX requests since the WebView lacks WordPress authentication cookies.

4. **Android: Configure `assetLoaderDomain`**: On Android, you must set the `assetLoaderDomain` to a domain that your WordPress site/plugin allows. This is because Android's WebViewAssetLoader serves the editor from a configurable domain, and AJAX requests must pass CORS validation on your server.

For example, the Jetpack mobile plugin allows requests from `android-app-assets.jetpack.com`:

```swift
// iOS - siteURL and authHeader are required
let configuration = EditorConfigurationBuilder(
postType: "post",
siteURL: URL(string: "https://example.com")!,
siteApiRoot: URL(string: "https://example.com/wp-json")!
)
.setAuthHeader("Bearer your-token")
.build()
```

```kotlin
// Android - assetLoaderDomain is also required for AJAX
val configuration = EditorConfiguration.builder()
.setPostType("post")
.setSiteURL("https://example.com")
.setSiteApiRoot("https://example.com/wp-json")
.setAuthHeader("Bearer your-token")
.setAssetLoaderDomain("android-app-assets.jetpack.com") // Must be allowed by your WordPress site
.build()
```

**Server-side CORS configuration**: Your WordPress site must include the `assetLoaderDomain` in its CORS allowed origins. This is typically handled by your WordPress plugin (e.g., Jetpack) that integrates with the mobile app.
84 changes: 84 additions & 0 deletions src/utils/ajax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Internal dependencies
*/
import { getGBKit } from './bridge';
import { warn, debug } from './logger';

/**
* GutenbergKit lacks authentication cookies required for AJAX requests.
* This configures a root URL and authentication header for AJAX requests.
*
* @return {void}
*/
export function configureAjax() {
window.wp = window.wp || {};
window.wp.ajax = window.wp.ajax || {};
window.wp.ajax.settings = window.wp.ajax.settings || {};

const { siteURL, authHeader } = getGBKit();
configureAjaxUrl( siteURL );
configureAjaxAuth( authHeader );
}

function configureAjaxUrl( siteURL ) {
if ( ! siteURL ) {
warn( 'Unable to configure AJAX URL without siteURL' );
return;
}

// Global used within WordPress admin pages
window.ajaxurl = `${ siteURL }/wp-admin/admin-ajax.php`;
// Global used by WordPress' JavaScript API
window.wp.ajax.settings.url = `${ siteURL }/wp-admin/admin-ajax.php`;

debug( 'AJAX URL configured' );
}

function configureAjaxAuth( authHeader ) {
if ( ! authHeader ) {
warn( 'Unable to configure AJAX auth without authHeader' );
return;
}

window.jQuery?.ajaxSetup( {
headers: {
Authorization: authHeader,
},
} );

if ( typeof window.wp.ajax.send === 'function' ) {
const originalSend = window.wp.ajax.send;
window.wp.ajax.send = function ( options ) {
const originalBeforeSend = options.beforeSend;

options.beforeSend = function ( xhr ) {
xhr.setRequestHeader( 'Authorization', authHeader );

if ( typeof originalBeforeSend === 'function' ) {
originalBeforeSend( xhr );
}
};

return originalSend.call( this, options );
};
}

if ( typeof window.wp.ajax.post === 'function' ) {
const originalPost = window.wp.ajax.post;
window.wp.ajax.post = function ( options ) {
const originalBeforeSend = options.beforeSend;

options.beforeSend = function ( xhr ) {
xhr.setRequestHeader( 'Authorization', authHeader );

if ( typeof originalBeforeSend === 'function' ) {
originalBeforeSend( xhr );
}
};

return originalPost.call( this, options );
};
}

debug( 'AJAX auth configured' );
}
Loading
Loading