Skip to content

Integrate WPAndroid#316

Open
jkmassel wants to merge 23 commits intotrunkfrom
integrate/wp-android
Open

Integrate WPAndroid#316
jkmassel wants to merge 23 commits intotrunkfrom
integrate/wp-android

Conversation

@jkmassel
Copy link
Contributor

@jkmassel jkmassel commented Feb 10, 2026

What?

Integrate GutenbergKit with WordPress.com (WP Android) by moving editor loading UI into GutenbergView, adding WP.com REST API namespace support, adding native CORS proxy for Android, improving HTTP logging on both platforms, and fixing falsy post ID handling.

Why?

This branch prepares GutenbergKit for use in WP Android, which requires several changes:

  1. Internal loading UI: Host apps (like WP Android) shouldn't need to implement their own loading/progress/error UI — GutenbergView should manage this internally, matching how iOS already works.
  2. WP.com namespace support: WP.com's REST API uses a sites/{id}/ namespace segment (e.g., /wp/v2/sites/123/posts) that RESTAPIRepository didn't support.
  3. CORS proxy: The Android WebView serves pages from appassets.androidplatform.net, making every fetch() to the WordPress REST API cross-origin. A native request proxy bypasses CORS without requiring server-side changes.
  4. Post ID = 0 handling: Both platforms and the JS bridge treated 0 as a valid post ID, which caused the editor to attempt fetching a non-existent post.
  5. Verbose HTTP logging: Debugging integration issues requires detailed request/response logging on both iOS and Android EditorHTTPClient.

How?

Android: Move loading UI into GutenbergView (refactor)

  • Changed GutenbergView from extending WebView to extending FrameLayout, containing an internal WebView plus overlay views (EditorProgressView, ProgressBar, EditorErrorView).
  • Removed the EditorLoadingListener interface and all consumer-side loading UI from the demo app's EditorActivity.
  • Loading states (PROGRESSSPINNERREADY / ERROR) are managed internally with animated transitions.

Android: CORS proxy in shouldInterceptRequest

  • Added proxyRequest() to GutenbergView that intercepts non-asset, non-cached requests and performs them via HttpURLConnection, bypassing CORS.
  • Handles OPTIONS preflight requests with a synthetic 204 response.
  • Forwards cookies from CookieManager and adds Access-Control-Allow-* headers to responses.

WP.com namespace support (iOS + Android)

  • Added buildNamespacedURL() to RESTAPIRepository (Android) and equivalent logic on iOS that inserts the namespace (e.g., sites/123/) after the API version segment in REST paths.
  • Added tests for namespaced URL building on both platforms.

Post ID = 0 → nil

  • EditorConfiguration on both platforms now coerces postId = 0 to nil.
  • JS bridge uses || instead of ?? for post.id so falsy values (0, null, undefined) fall back to -1.
  • Added unit tests on both platforms.

Verbose HTTP logging (iOS + Android)

  • EditorHTTPClient on both platforms now logs full request/response details (method, URL, headers, status code, body on error, WP error codes) at debug/error levels.

Other

  • Updated AGP to 8.10.1 to match WP Android.
  • Skip theme style loading in EditorService when themeStyles is disabled.
  • Deleted unused activity_editor.xml layout file.

Testing Instructions

  1. Open the Android demo app and verify the editor loads with built-in progress bar → spinner → editor transitions.
  2. Verify no loading UI code is needed in the host activity.
  3. Test with a WP.com site using siteApiNamespace: ["sites/{id}/"] and confirm REST API calls include the namespace.
  4. Create a new post (post ID = 0) and confirm the editor renders without trying to fetch post 0.
  5. Check logcat/console for detailed HTTP request/response logs during editor initialization.

Accessibility Testing Instructions

Loading states use standard Android ProgressBar and TextView components with system accessibility defaults. No custom accessibility handling was added — verify screen readers announce the progress bar and error state text.

Screenshots or screencast

N/A – no visual design changes beyond moving existing loading UI into the library.

jkmassel and others added 10 commits February 7, 2026 11:10
GutenbergView previously extended WebView directly and delegated all
loading UI (progress bar, spinner, error states) to consumers via the
EditorLoadingListener interface. This forced every app embedding the
editor to implement its own loading UI boilerplate.

This change makes GutenbergView extend FrameLayout instead, containing
an internal WebView plus overlay views for loading states:

- EditorProgressView (progress bar + label) during dependency fetching
- ProgressBar (circular/indeterminate) during WebView initialization
- EditorErrorView (new) for error states

The view manages its own state transitions with 200ms fade animations,
matching the iOS EditorViewController pattern. The EditorLoadingListener
interface is removed entirely — consumers no longer need loading UI code.

Changes:
- GutenbergView: WebView -> FrameLayout with internal WebView child
- New EditorErrorView for displaying load failures
- Delete EditorLoadingListener (no longer needed)
- Simplify demo EditorActivity by removing ~90 lines of loading UI
- Update tests to use editorWebView accessor for WebView properties
- Delete unused activity_editor.xml layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… render (#317)

* fix: Use dev server origin for CORS headers when configured

The CORS proxy in `addCorsHeaders` was hardcoded to
`https://appassets.androidplatform.net`, which is correct for
bundled assets but causes CORS failures when using a local dev
server via `GUTENBERG_EDITOR_URL`. The browser rejects the
proxied responses because `Access-Control-Allow-Origin` doesn't
match the dev server origin.

Derive the origin from `GUTENBERG_EDITOR_URL` when it's set,
falling back to the bundled asset origin otherwise.

* fix: Ensure post id is truthy so editor becomes ready

`__unstableIsEditorReady()` returns `!!state.postId`, so a post
id of `0` keeps the editor permanently in a "not ready" state.

Use `||` instead of `??` to fall back to `-1` for all falsy ids
(not just null/undefined), matching the existing fallback path.
Logs request method, URL, headers, response status, size, and headers
for every HTTP call. Network errors, HTTP errors, response bodies, and
parsed WP error details are logged at the error level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Android RESTAPIRepository was building API URLs by simple string
concatenation, ignoring siteApiNamespace. WP.com sites need a namespace
like `sites/123/` inserted after the version segment in API paths
(e.g., `/wp/v2/sites/123/posts`). This matches the existing iOS
buildNamespacedURL logic.

Also adds matching namespace URL tests to both iOS and Android.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jkmassel jkmassel added the [Type] Enhancement A suggestion for improvement. label Feb 11, 2026
jkmassel and others added 13 commits February 11, 2026 12:50
Authorization, Cookie, and Set-Cookie header values are now replaced
with <redacted> in debug logs to prevent credential leakage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The native CORS proxy in GutenbergView now only forwards the
Authorization header and cookies to known trusted hosts (site API,
WP.com API, Photon CDN, and configured cached asset hosts). Requests
to other domains are still proxied but without credentials, preventing
token leakage if injected content triggers fetches to untrusted origins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use a shared OkHttpClient for proxying WebView requests, replacing the
raw HttpURLConnection. OkHttp's connection pool automatically reclaims
connections when the response stream is closed, eliminating the
potential connection leak.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The WordPress REST API should never include sensitive information in
responses, so logging the raw body on HTTP errors is acceptable and
useful for debugging unexpected error formats.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This flag enables the JavaScript editor to surface network details to
the native host app via the bridge. It does not control the native
EditorHTTPClient's own debug-level logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The early return in prepareEditorSettings() skipped fetching editor
settings whenever themeStyles was false, even when plugins was true.
This diverged from iOS, which always delegates to the repository.

The repository already has the correct guard: skip only when both
plugins and themeStyles are disabled. Host apps that don't support
the editor settings endpoint should set both flags to false.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…orms

Ensures namespaces like "sites/123" (without trailing slash) produce
the same URLs as "sites/123/" so callers don't need to worry about
the convention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents withEndAction callbacks from firing on detached views
if the editor is closed mid-animation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
iOS calls the delegate for both perform and download, but Android
only called it for perform. This adds EditorResponseData (matching
iOS's enum) and calls the delegate after downloads complete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces @unchecked Sendable with proper actor isolation so
capturedURLs is synchronized.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…edFromWindow

These two listeners were not being nulled out during teardown,
inconsistent with all other listener cleanup in the same method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jkmassel jkmassel requested a review from dcalhoun February 12, 2026 19:22
@jkmassel jkmassel self-assigned this Feb 12, 2026
@jkmassel jkmassel marked this pull request as ready for review February 12, 2026 19:22
Copy link
Member

@dcalhoun dcalhoun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for implementing this! 🙇🏻‍♂️

The overarching approach looks sound to me; it's aligned with much of the current iOS implementation.

While testing, I encountered a few critical issues that I noted in inline comments. We should discuss and address those.

// it's not actually a native call – the editor is building the request so we don't
// want to modify it in any way.
Log.d(TAG, "shouldInterceptRequest: proxying request: ${request.url}")
return proxyRequest(request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I encounter plugin load failures from CORS errors when launching the editor for a WoW/Atomic site or jetpack.wpmt.co.

Plugin load failure

Image

CORS errors

Image

Disabling this proxy by restate the previous implementation resolves the issue.

Example revert diff
diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt
index 32c10632..78ec151d 100644
--- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt
+++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt
@@ -414,6 +414,8 @@ class GutenbergView : FrameLayout {
                     // Cache miss – fall through to proxy below
                 }
 
+                return super.shouldInterceptRequest(view, request)
+
                 // Proxy all other requests natively so they bypass CORS
                 // (the editor page is served from appassets.androidplatform.net,
                 // making every API call cross-origin). This doesn't use `EditorHTTPClient` because

There appears to be something off with the new proxy logic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This same issue appears to cause all requests to fail with CORS errors. E.g., uploading an image fails.

* OkHttp, which is not subject to CORS. OkHttp's connection pool handles cleanup
* automatically when the response stream is closed by the WebView.
*/
private fun proxyRequest(request: WebResourceRequest): WebResourceResponse? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably, this would negate the need for server logic like Automattic/jetpack#45292. Correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to avoid requiring Jetpack for this feature to work, but WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agreed. If we can avoid the need for Jetpack plugin code for admin-ajax support that'd be great.

However, the CORS fix is only one facet of that support. For admin-ajax support, we still must enable authenticating those requests with app passwords with the application_password_is_api_request hook we use in Automattic/jetpack#45220.

I.e., if we solve the CORS problem in the client, Jetpack is still required to enable application password authentication of admin-ajax (VideoPress only). If other plugins want that support, they need to opt in for themselves via the hook.


// OkHttp requires a body for POST/PUT/PATCH even if empty
val requiresBody = method in listOf("POST", "PUT", "PATCH")
val body = if (requiresBody) "".toRequestBody(null) else null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably, this (erroneously?) empty body is the reason image uploads fail.

From researching, it seems you cannot retrieve/pass along body values from WebResourceRequest. Is proxying mutating requests actually possible in this manner or do we need to abandon this newly added proxy?

Comment on lines +597 to +599
response.headers.forEach { (key, value) ->
responseHeaders[key] = value
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude noted this may erroneously discard duplicate headers (e.g., multiple Set-Cookie). Should we join duplicates?

Comment on lines +108 to +113
add("public-api.wordpress.com")
// WordPress.com CDN hosts (Photon image proxy)
add("i0.wp.com")
add("i1.wp.com")
add("i2.wp.com")
add("i3.wp.com")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it feasible for us to hoist these WP.com references to host app (Wordpress and/or Demo) to keep GutenbergKit free of these references?

}
gravity = Gravity.CENTER
TextViewCompat.setTextAppearance(this, android.R.style.TextAppearance_Material_Subhead)
text = "Failed to load editor"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a future PR, we should likely localize strings like this in a manner similar to iOS.

public final class EditorLocalization {
/// This is designed to be overridden by the host app to provide translations.
public static var localize: (EditorLocalizableString) -> String = { key in
switch key {
case .showMore: "Show More"
case .showLess: "Show Less"
case .search: "Search"
case .insertBlock: "Insert Block"
case .failedToInsertMedia: "Failed to insert media"
case .patterns: "Patterns"
case .noPatternsFound: "No Patterns Found"
case .insertPattern: "Insert Pattern"
case .patternsCategoryUncategorized: "Uncategorized"
case .patternsCategoryAll: "All"
case .loadingEditor: "Loading Editor"
case .editorError: "Editor Error"
}
}
/// Convenience subscript for accessing localized strings.
public static subscript(key: EditorLocalizableString) -> String {
localize(key)
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments