Skip to content

Conversation

@dcalhoun
Copy link
Member

@dcalhoun dcalhoun commented Jan 16, 2026

What?

Adds a pull-based content recovery mechanism that allows the web editor to request the latest content from the native host during initialization.

Why?

Ref CMM-1123.

When the WebView reinitializes (due to OS memory pressure or page refresh), the editor previously loaded stale content from window.GBKit.post which was injected at original load time. The host apps have fresher content from autosave events (~1s intervals), but this wasn't being used on WebView recovery.

Related WordPress app PRs:

How?

Implements a pull model where the web editor always requests content from the native host during initialization:

  1. Web bridge (src/utils/bridge.js): Added requestLatestContent() function that calls the appropriate native bridge method
  2. iOS (EditorViewController.swift): Implements WKScriptMessageHandlerWithReply to respond to content requests via a new delegate method editorRequestsLatestContent
  3. Android (GutenbergView.kt): Adds LatestContentProvider interface and @JavascriptInterface method that returns JSON content
  4. Editor initialization (src/utils/editor.jsx): getPost() now requests content from native host first, falling back to window.GBKit.post only when bridge is unavailable
  5. Editor configuration: properly set overlooked post status and ID to avoid unexpectedly emptying post titles.

Alternatives considered:

  • Persisting transient content in the WebView via local/session storage: creates multiple sources of truth for the state of the content—web vs native host.
  • Recreating the GutenbergKit view with new configuration: incurs unnecessary overhead of recreating the entire configuration for simple vents like page refreshes.

Testing Instructions

  1. Open a post with existing content in the editor
  2. Make edits to the content (wait 2+ seconds for autosave)
  3. Trigger a WebView refresh:
    • Safari DevTools: Cmd+R while WebView is inspected via the Safari Inspector
    • iOS Simulator: Open Activity Monitor, force quit "com.apple.WebKit.WebContent"
    • Android: Cmd+R while WebView is inspected via the Chrome Inspector by visiting chrome://inspect
  4. Verify the editor recovers with the edited content, not the original

Accessibility Testing Instructions

No UI changes - this is a data recovery mechanism that happens during initialization.

Screenshots or screencast

N/A - No visual changes

🤖 Generated with Claude Code

@dcalhoun
Copy link
Member Author

@claude

@claude
Copy link

claude bot commented Jan 16, 2026

Claude finished @dcalhoun's task in 1m 53s —— View job


Code Review Complete ✅

This PR implements a solid pull-based content recovery mechanism. The implementation is well-structured across all three platforms with comprehensive test coverage. Below is my detailed review.


Overall Assessment

Strengths:

  • ✅ Clear architecture with consistent patterns across iOS, Android, and web
  • ✅ Excellent test coverage (406 lines of new tests)
  • ✅ Well-documented code with helpful comments
  • ✅ Proper fallback behavior when bridge is unavailable
  • ✅ Demo implementations in both sample apps

Architecture: The pull model is the right approach here. Having the web layer request content rather than push it eliminates timing issues and makes the recovery mechanism more reliable.


Detailed Findings

1. Code Quality ⚠️

JavaScript (bridge.js:256-278)

  • Error handling uses debug() instead of error() for actual failures (violates CLAUDE.md logging guidelines)
  • iOS bridge check could be more defensive with runtime type validation
  • See inline comments for suggested improvements

Swift (EditorViewController.swift:665, 749)

  • Delegate method return is optional, which could mask integration issues in production
  • Return type doesn't distinguish between "no content" vs "error" scenarios
  • Consider adding debug logging when delegate is nil

Kotlin (GutenbergView.kt:672)

  • JSON serialization lacks error handling for edge cases
  • Could crash if content contains problematic characters
  • Should wrap in try-catch (see inline comment)

2. Security ✅

No security concerns identified:

  • ✅ Content is properly URL-encoded on both platforms
  • ✅ No injection vulnerabilities in bridge communication
  • ✅ JSON parsing has appropriate error handling (except one case noted above)
  • ✅ No authentication/authorization changes

3. Performance ✅

Performance impact is minimal and positive:

  • ✅ Async/await pattern prevents blocking UI thread
  • ✅ Single content request during initialization (no polling)
  • ✅ Fallback to cached GBKit.post is instant when bridge unavailable
  • ✅ No additional network requests

Measurement suggestion: Consider adding timing metrics to measure recovery speed in production.

4. Testing ⭐

Excellent test coverage:

  • ✅ 406 lines of new tests in bridge.test.js
  • ✅ Tests cover iOS bridge, Android bridge, fallbacks, and priority
  • ✅ Edge cases well covered (null, empty string, malformed JSON, errors)
  • ✅ Test for iOS priority over Android when both available

Minor suggestion: The priority test at line 182 is great but could use a comment explaining why iOS has priority.

5. Documentation ✅

Documentation is thorough:

  • ✅ JSDoc comments on new functions
  • ✅ Swift documentation comments on delegate method
  • ✅ Kotlin interface documentation
  • ✅ PR description explains the "why" clearly
  • ✅ Testing instructions are comprehensive

Recommendations

High Priority

  1. Fix logging per CLAUDE.md guidelines - Use error() for actual errors in bridge.js:262, 272
  2. Add error handling to Android JSON serialization - See inline comment on GutenbergView.kt:672

Medium Priority

  1. Improve error observability - Distinguish between "no content" vs "error" in iOS implementation
  2. Add resilience to editor initialization - Wrap getPost() in try-catch in editor.jsx:46

Low Priority

  1. Add debug logging - Log when delegate is nil to help integrators discover missing implementation
  2. Add runtime type checks - More defensive iOS bridge validation

Pre-Merge Checklist

Before merging, please ensure:

  • Run make format to format JavaScript code
  • Run make lint-js-fix to auto-fix linting errors
  • Run make lint-js to verify linting passes
  • Run make test-js to ensure all tests pass

(Per CLAUDE.md pre-commit checklist)


Summary

This is a well-implemented feature that solves a real problem. The architecture is sound, test coverage is excellent, and the code is generally high quality. The issues identified are mostly minor improvements around error handling and observability.

Recommendation: Approve with suggested improvements for error handling.


@dcalhoun dcalhoun added the [Type] Enhancement A suggestion for improvement. label Jan 16, 2026
@dcalhoun dcalhoun marked this pull request as draft January 19, 2026 14:55
@dcalhoun dcalhoun force-pushed the feat/restore-content-via-pull branch from 55471c3 to e8dd9ff Compare January 20, 2026 18:38
Comment on lines -264 to -265
// Since we don't use the auto-save functionality, draft posts need to have an ID.
// We assign a temporary ID of -1.
Copy link
Member Author

Choose a reason for hiding this comment

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

It remains true we need a -1 fallback ID for draft posts, but this appears unrelated to auto-save. We now use the AutosaveMonitor in the GutenbergKit project.

For posterity, this comment was first added in 444de49.

@dcalhoun dcalhoun force-pushed the feat/restore-content-via-pull branch from 3703a0c to dd58448 Compare January 21, 2026 15:29
return {
id: -1,
type: 'post',
status: 'auto-draft',
Copy link
Member Author

@dcalhoun dcalhoun Jan 21, 2026

Choose a reason for hiding this comment

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

The auto-draft post status is specific to the WordPress server and not recognized/utilized by the WordPress mobile apps. Also, using auto-draft results in emptying populated post titles. Therefore, a more sensible default is draft.

@dcalhoun dcalhoun marked this pull request as ready for review January 21, 2026 17:29
@dcalhoun dcalhoun requested review from kean and nbradbury January 21, 2026 17:56
@nbradbury
Copy link
Contributor

@dcalhoun I can approve this from the Android site of things once the conflicts are resolved.

Expose function for retrieving the latest content from the native host.
This is useful for ensuring the content is updated after WebView
refresh or re-initialization.
Enable the editor to request the latest content from the host.
Enable the editor to request the latest content from the host.
Ensure the editor always uses the latest content. This particularly
important for subsequent initialization events--e.g., WebView refresh.
When crossing actor boundaries, Swift requires type to conform to
`Sendable`, which `Any?` cannot. Constructing the dictionary outside of
the `MainActor` run avoids this incompatibility.
Improve visibility of critical errors.
Mitigate crashes from unexpected characters.
There isn't an explicit preference, but a result of arbitrary code
ordering.
Note the draft post fallback.
Ensure the fallback status matches across the various scenarios for
sourcing post content.
The `auto-draft` post status is unique to the WordPress server; it
represents saves not performed by the user, but by the auto-save
functionality. This concept is not present in the WordPress mobile apps.
Using auto-draft resulted in emptying of post title content due to
client-side filters in Gutenberg core.
The name is more explicit and matches other fields like `postId`. This
also improves cross-platform alignment.
Structure the `postStatus` field in the same manner across platforms for
consistency.
@dcalhoun dcalhoun force-pushed the feat/restore-content-via-pull branch from 3110b00 to fa4ac86 Compare January 22, 2026 16:52
@dcalhoun
Copy link
Member Author

@nbradbury I rebased this addressing merge conflicts and added three additional commits to update this branch to match the latest GBKit configuration structure and improve alignment across platforms:

Copy link
Contributor

@kean kean left a comment

Choose a reason for hiding this comment

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

Seems reasonable.

How does the web view know when to reload if the process crashes? The other parts of the app use the following delegate method: webViewWebContentProcessDidTerminate in which the native sides triggers the reload and has control over it. There is no other ways to trigger the page refresh in production AFAIK.


/// Hiding the conformances, and breaking retain cycles.
private final class GutenbergEditorController: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
private final class GutenbergEditorController: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKScriptMessageHandlerWithReply {
Copy link
Contributor

Choose a reason for hiding this comment

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

TIL WKScriptMessageHandlerWithReply – its' really nice.

Copy link
Contributor

@nbradbury nbradbury left a comment

Choose a reason for hiding this comment

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

:shipit:

Copy link
Contributor

@jkmassel jkmassel left a comment

Choose a reason for hiding this comment

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

Tested this alongside iOS and it worked great.

@dcalhoun
Copy link
Member Author

How does the web view know when to reload if the process crashes? The other parts of the app use the following delegate method: webViewWebContentProcessDidTerminate in which the native sides triggers the reload and has control over it.

@kean you are correct. GutenbergKit doesn't currently implement webViewWebContentProcessDidTerminate. In my testing of this current implementation, the recovery worked when terminating the com.apple.WebKit.WebContent process. Possibly due to iOS automatic recovery or view recreation?

Do you feel we should also add webViewWebContentProcessDidTerminate to GutenbergKit's GutenbergEditorController to call webView.reload()?

There is no other ways to trigger the page refresh in production AFAIK.

JavaScript from plugins could call window.reload().

@kean
Copy link
Contributor

kean commented Jan 22, 2026

the recovery worked when terminating the com.apple.WebKit.WebContent process.

I'm not sure if that's what happens when WKWebView decides to terminate itself due to the memory pressure. It often happens when you background the app and do some other memory-intensive work. You come back to the app, and all WKWebView instances are blank (the app receives webViewWebContentProcessDidTerminate). In my experience, it requires "manual" reload of the web view. The comment rendered in the app has the following simply heuristic for that: WebCommentContentRenderer.swift#L127.

Possibly due to iOS automatic recovery or view recreation?

It might happen automatically if com.apple.WebKit.WebContent is terminated under the WKWebView instance. I'm not sure that's the case when WKWebView decides to terminate itself. I'd suggest testing this scenario. My expectation that it would result in a blank screen.

JavaScript from plugins could call window.reload().

That's a fair point. Doesn't web view retain the memory state in the case? Does it need to call the native side?

Align with Swift naming practices.
Improve the robustness of content restoration in the event the
`WKWebView` is terminated on iOS due to memory pressure.

In testing by manually terminating the `com.apple.WebKit.WebContent`
task in the Activity Monitor, the editor recovered without these
changes, at least the first time. However, terminating the task more
than once led to an empty WebView for the editor.

Implementing `webViewWebContentProcessDidTerminate` seems to improve the
robustness. Terminating the `com.apple.WebKit.WebContent` task multiple
times now always leads to the editor restoring the latest content.
@dcalhoun
Copy link
Member Author

It might happen automatically if com.apple.WebKit.WebContent is terminated under the WKWebView instance. I'm not sure that's the case when WKWebView decides to terminate itself. I'd suggest testing this scenario. My expectation that it would result in a blank screen.

I've been unable to identify an approach to test that specific scenario. That is why I used the terminate the com.apple.WebKit.WebContent process approach.

That said, I did find now that terminating the process multiple times will lead to an empty WebView. Implementing webViewWebContentProcessDidTerminate in ba98f11 seems to address that.

That's a fair point. Doesn't web view retain the memory state in the case [window.reload()]? Does it need to call the native side?

No. It seems the React app runs again, populating the editor with the stale window.GBKit global values. It could be the browser does retain memory state, but it's overwritten by how React functions; I'm not sure.

@dcalhoun
Copy link
Member Author

How does the web view know when to reload if the process crashes? The other parts of the app use the following delegate method: webViewWebContentProcessDidTerminate in which the native sides triggers the reload and has control over it. There is no other ways to trigger the page refresh in production AFAIK.

Given the two approvals received and having addressed @kean's initial feedback quoted above in ba98f11, I plan to merge this and publish a release for integrating into the next WordPress-iOS release.

I'm happy to follow up on further feedback in new PRs. 🙇🏻‍♂️

@dcalhoun dcalhoun merged commit 36ad4ef into trunk Jan 22, 2026
12 checks passed
@dcalhoun dcalhoun deleted the feat/restore-content-via-pull branch January 22, 2026 21:28
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.

5 participants